-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Add option for rounded corners on bar charts #6761
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
024f05f
31c3ac2
b0c6258
d71fe49
bb78c78
5dfd686
64912d4
cc1e6ff
dc2c54c
3b725fa
870d4c9
aa8ba1e
5845e0e
85f47c6
578712a
8f77cde
45689ae
137e185
795235b
dd717b6
35e0c8f
7aa7496
250bf31
495d950
be0ddff
819e8bf
ed69422
4a44291
65d3b55
e8cdde1
9c4227e
dabcaf0
701fd8c
29589e8
d0b9a90
3e5a957
582ce13
a238c87
ac329f7
8a607e6
338b4e6
60c918e
9dd0896
1628b5c
bfbd397
71e4896
4e794d0
dedce2a
b137b25
fdd6d03
8fcc3a5
71b53c3
90cf5c3
510c66b
5599ea6
8dffbbe
c165f6a
ee2451a
256a87e
b122521
015b881
8b57fb5
ec44d87
2913c6e
9e3c249
f986349
99352d0
6f685c0
f093ee7
e3cd5a5
98356b4
a2befea
781fbe2
97f0b0e
c52890c
10e38f4
f7d4b4a
4335ba0
e9043e9
4624132
8c45d15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -119,7 +119,6 @@ function setGroupPositions(gd, pa, sa, calcTraces, opts) { | |
} | ||
break; | ||
} | ||
|
||
collectExtents(calcTraces, pa); | ||
} | ||
|
||
|
@@ -713,6 +712,21 @@ function normalizeBars(sa, sieve, opts) { | |
} | ||
} | ||
|
||
// Add an `_sMin` and `_sMax` value for each bar representing the min and max size value | ||
// across all bars sharing the same position as that bar. These values are used for rounded | ||
// bar corners, to carry rounding down to lower bars in the stack as needed. | ||
function setHelperValuesForRoundedCorners(calcTraces, sMinByPos, sMaxByPos) { | ||
// Set `_sMin` and `_sMax` value for each bar | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We only need these when having bars with rounded corners. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @archmoj Yes good point. Would it make sense then to check whether any of the traces in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes that's a good idea. |
||
for(var i = 0; i < calcTraces.length; i++) { | ||
var calcTrace = calcTraces[i]; | ||
for(var j = 0; j < calcTrace.length; j++) { | ||
var bar = calcTrace[j]; | ||
bar._sMin = sMinByPos[bar.p]; | ||
bar._sMax = sMaxByPos[bar.p]; | ||
} | ||
} | ||
} | ||
|
||
// find the full position span of bars at each position | ||
// for use by hover, to ensure labels move in if bars are | ||
// narrower than the space they're in. | ||
|
@@ -745,6 +759,12 @@ function collectExtents(calcTraces, pa) { | |
return String(Math.round(roundFactor * (p - pMin))); | ||
}; | ||
|
||
// Find min and max size axis extent for each position | ||
// This is used for rounded bar corners, to carry rounding | ||
// down to lower bars in the case of stacked bars | ||
var sMinByPos = {}; | ||
var sMaxByPos = {}; | ||
|
||
for(i = 0; i < calcTraces.length; i++) { | ||
cd = calcTraces[i]; | ||
cd[0].t.extents = extents; | ||
|
@@ -770,8 +790,15 @@ function collectExtents(calcTraces, pa) { | |
di.p1 = di.p0 + di.w; | ||
di.s0 = di.b; | ||
di.s1 = di.s0 + di.s; | ||
|
||
var sMin = Math.min(di.s0, di.s1); | ||
var sMax = Math.max(di.s0, di.s1); | ||
|
||
sMinByPos[di.p] = (di.p in sMinByPos) ? Math.min(sMinByPos[di.p], sMin) : sMin; | ||
archmoj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sMaxByPos[di.p] = (di.p in sMaxByPos) ? Math.max(sMaxByPos[di.p], sMax) : sMax; | ||
} | ||
} | ||
setHelperValuesForRoundedCorners(calcTraces, sMinByPos, sMaxByPos); | ||
} | ||
|
||
function getAxisLetter(ax) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,7 +47,6 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { | |
}); | ||
|
||
handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); | ||
|
||
var lineColor = (traceOut.marker.line || {}).color; | ||
|
||
// override defaultColor for error bars with defaultLine | ||
|
@@ -61,16 +60,19 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { | |
function crossTraceDefaults(fullData, fullLayout) { | ||
var traceIn, traceOut; | ||
|
||
function coerce(attr) { | ||
return Lib.coerce(traceOut._input, traceOut, attributes, attr); | ||
function coerce(attr, dflt) { | ||
return Lib.coerce(traceOut._input, traceOut, attributes, attr, dflt); | ||
} | ||
|
||
if(fullLayout.barmode === 'group') { | ||
for(var i = 0; i < fullData.length; i++) { | ||
traceOut = fullData[i]; | ||
for(var i = 0; i < fullData.length; i++) { | ||
traceOut = fullData[i]; | ||
|
||
if(traceOut.type === 'bar') { | ||
traceIn = traceOut._input; | ||
if(traceOut.type === 'bar') { | ||
traceIn = traceOut._input; | ||
// This needs to happen here rather than in handleStyleDefaults() because | ||
// it needs to happen after `layout.barcornerradius` has been coerced | ||
coerce('marker.cornerradius', fullLayout.barcornerradius); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's validate inputs here. Something like: var r = coerce('marker.cornerradius', fullLayout.barcornerradius);
var validR = false;
if(isNumeric(r)) {
r = +r;
if(r >= 0) validR = true;
} else if(typeof r === 'string' && r.slice(-1) === '%') {
r = +r.slice(0, -1);
if(r >= 0 && r <= 50) {
r += '%';
validR = true;
}
}
traceOut.marker.cornerradius = validR ? r : undefined; We need similar logic for histogram as well as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks good -- I think there should be no upper bound on the percentage though; it will be limited to 50% at the plotting step anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @alexcjohnson what do you think here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds reasonable to not put an upper bound - say someone uses a calculation for this, and the calculation accidentally goes over 50% (whether to 50.1% or to 200%), I think it'd be friendlier to result in maximum rounding rather than discard it and give no rounding. |
||
if(fullLayout.barmode === 'group') { | ||
handleGroupingDefaults(traceIn, traceOut, fullLayout, coerce); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,4 +47,5 @@ module.exports = function(layoutIn, layoutOut, fullData) { | |
|
||
coerce('bargap', (shouldBeGapless && !gappedAnyway) ? 0 : 0.2); | ||
coerce('bargroupgap'); | ||
coerce('barcornerradius'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that necessary? So the only reason to validate it would be for internal consistency within the plot object, which I guess is a good enough reason if that's consistent with what we do elsewhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are right. It does not seem necessary. |
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -98,6 +98,7 @@ function plot(gd, plotinfo, cdModule, traceLayer, opts, makeOnCompleteCallback) | |
var trace = cd[0].trace; | ||
var isWaterfall = (trace.type === 'waterfall'); | ||
var isFunnel = (trace.type === 'funnel'); | ||
var isHistogram = (trace.type === 'histogram'); | ||
var isBar = (trace.type === 'bar'); | ||
var shouldDisplayZeros = (isBar || isFunnel); | ||
|
||
|
@@ -215,27 +216,140 @@ function plot(gd, plotinfo, cdModule, traceLayer, opts, makeOnCompleteCallback) | |
(v > vc ? Math.ceil(v) : Math.floor(v)); | ||
} | ||
|
||
var op = Color.opacity(mc); | ||
var fixpx = (op < 1 || lw > 0.01) ? roundWithLine : expandToVisible; | ||
if(!gd._context.staticPlot) { | ||
// if bars are not fully opaque or they have a line | ||
// around them, round to integer pixels, mainly for | ||
// safari so we prevent overlaps from its expansive | ||
// pixelation. if the bars ARE fully opaque and have | ||
// no line, expand to a full pixel to make sure we | ||
// can see them | ||
|
||
var op = Color.opacity(mc); | ||
var fixpx = (op < 1 || lw > 0.01) ? roundWithLine : expandToVisible; | ||
|
||
x0 = fixpx(x0, x1, isHorizontal); | ||
x1 = fixpx(x1, x0, isHorizontal); | ||
y0 = fixpx(y0, y1, !isHorizontal); | ||
y1 = fixpx(y1, y0, !isHorizontal); | ||
} | ||
|
||
// Function to convert from size axis values to pixels | ||
var c2p = isHorizontal ? xa.c2p : ya.c2p; | ||
|
||
// Calculate corner radius of bar in pixels | ||
function calcCornerRadius(radiusParam) { | ||
var barWidth = isHorizontal ? Math.abs(y1 - y0) : Math.abs(x1 - x0); | ||
var barLength = isHorizontal ? Math.abs(x1 - x0) : Math.abs(y1 - y0); | ||
var stackedBarTotalLength = fixpx(Math.abs( | ||
di.s > 0 ? c2p(di._sMax, true) - c2p(0, true) : c2p(di._sMin, true) - c2p(0, true) | ||
)); | ||
var maxRadius = di.hasB ? Math.min(barWidth / 2, barLength / 2) : Math.min(barWidth / 2, stackedBarTotalLength); | ||
var rPx; | ||
if(!radiusParam) { | ||
return 0; | ||
archmoj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else if(typeof radiusParam === 'string') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't get into this if statement, if the value is presented in a string but not having a percentage at the end. } else if(typeof radiusParam === 'string' && radiusParam.slice(-1) === '%') {
// If radius is given as a percentage string, convert to number of pixels
var rPercent = Math.min(50, +radiusParam.slice(0, -1)); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better to use if(!radiusParam) {
return 0;
else if(isNumeric(radiusParam)) {
rPx = +radiusParam;
else {
var rPercent = ...
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ignore if it’s invalid. Plotlyjs has a separate validation utility that will alert you if any attributes you provide were not accepted, but if you don’t explicitly call that our convention is to silently ignore invalid items. |
||
// If radius is given as a percentage string, convert to number of pixels | ||
var rPercent = Math.min(parseFloat(radiusParam.replace('%', '')), 50); | ||
rPx = barWidth * (rPercent / 100); | ||
} else { | ||
// Otherwise, it's already a number of pixels | ||
rPx = radiusParam; | ||
} | ||
return fixpx(Math.max(Math.min(rPx, maxRadius), 0)); | ||
} | ||
// Exclude waterfall and funnel charts from rounding | ||
// Could potentially support rounded waterfall charts in the future, | ||
// but need to make sure trace.marker.cornerradius is set in the defaults | ||
// and also check visuals with respect to the lines connecting the waterfall bars | ||
var r = (isBar || isHistogram) ? calcCornerRadius(trace.marker.cornerradius) : 0; | ||
|
||
// Construct path string for bar | ||
var path, h; | ||
if(r && di.s) { | ||
// Bar has cornerradius | ||
// Check amount of 'overhead' (bars stacked above this one) | ||
// to see whether we need to round or not | ||
var overhead = fixpx(Math.abs( | ||
di.s > 0 ? c2p(di._sMax, true) - c2p(di.s1, true) : c2p(di._sMin, true) - c2p(di.s1, true) | ||
)); | ||
|
||
if(overhead < r) { | ||
// Calculate parameters for rounded corners | ||
var xdir = dirSign(x0, x1); | ||
var ydir = dirSign(y0, y1); | ||
// Sweep direction for rounded corner arcs | ||
var cornersweep = (xdir === -ydir) ? 1 : 0; | ||
if(isHorizontal) { | ||
// Horizontal bars | ||
if(di.hasB) { | ||
// Floating base: Round 1st & 2nd, and 3rd & 4th corners | ||
path = 'M' + (x0 + r * xdir) + ',' + y0 + | ||
'a ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + r * -xdir + ',' + r * ydir + | ||
'V' + (y1 - r * ydir) + | ||
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + (x0 + r * xdir) + ',' + y1 + | ||
'H' + (x1 - r * xdir) + | ||
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + x1 + ',' + (y1 - r * ydir) + | ||
'V' + (y0 + r * ydir) + | ||
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + (x1 - r * xdir) + ',' + y0 + | ||
'Z'; | ||
} else { | ||
// Base on axis: Round 3rd and 4th corners | ||
|
||
// Helper variables to help with extending rounding down to lower bars | ||
h = Math.abs(x1 - x0) + overhead; | ||
var dy1 = (h < r) ? r - Math.sqrt(h * (2 * r - h)) : 0; | ||
var dy2 = (overhead > 0) ? Math.sqrt(overhead * (2 * r - overhead)) : 0; | ||
var xminfunc = xdir > 0 ? Math.max : Math.min; | ||
|
||
path = 'M' + x0 + ',' + y0 + | ||
'V' + (y1 - dy1 * ydir) + | ||
'H' + xminfunc(x1 - (r - overhead) * xdir, x0) + | ||
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + x1 + ',' + (y1 - r * ydir - dy2) + | ||
'V' + (y0 + r * ydir + dy2) + | ||
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + xminfunc(x1 - (r - overhead) * xdir, x0) + ',' + (y0 + dy1 * ydir) + | ||
'Z'; | ||
} | ||
} else { | ||
// Vertical bars | ||
if(di.hasB) { | ||
// Floating base: Round 1st & 4th, and 2nd & 3rd corners | ||
path = 'M' + (x0 + r * xdir) + ',' + y0 + | ||
'a ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + r * -xdir + ',' + r * ydir + | ||
'V' + (y1 - r * ydir) + | ||
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + (x0 + r * xdir) + ',' + y1 + | ||
'H' + (x1 - r * xdir) + | ||
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + x1 + ',' + (y1 - r * ydir) + | ||
'V' + (y0 + r * ydir) + | ||
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + (x1 - r * xdir) + ',' + y0 + | ||
'Z'; | ||
} else { | ||
// Base on axis: Round 2nd and 3rd corners | ||
|
||
// Helper variables to help with extending rounding down to lower bars | ||
h = Math.abs(y1 - y0) + overhead; | ||
var dx1 = (h < r) ? r - Math.sqrt(h * (2 * r - h)) : 0; | ||
var dx2 = (overhead > 0) ? Math.sqrt(overhead * (2 * r - overhead)) : 0; | ||
var yminfunc = ydir > 0 ? Math.max : Math.min; | ||
|
||
path = 'M' + (x0 + dx1 * xdir) + ',' + y0 + | ||
'V' + yminfunc(y1 - (r - overhead) * ydir, y0) + | ||
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + (x0 + r * xdir - dx2) + ',' + y1 + | ||
'H' + (x1 - r * xdir + dx2) + | ||
'A ' + r + ',' + r + ' 0 0 ' + cornersweep + ' ' + (x1 - dx1 * xdir) + ',' + yminfunc(y1 - (r - overhead) * ydir, y0) + | ||
'V' + y0 + 'Z'; | ||
} | ||
} | ||
} else { | ||
// There is a cornerradius, but bar is too far down the stack to be rounded; just draw a rectangle | ||
path = 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z'; | ||
} | ||
} else { | ||
// No cornerradius, just draw a rectangle | ||
path = 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z'; | ||
} | ||
|
||
var sel = transition(Lib.ensureSingle(bar, 'path'), fullLayout, opts, makeOnCompleteCallback); | ||
sel | ||
.style('vector-effect', isStatic ? 'none' : 'non-scaling-stroke') | ||
.attr('d', (isNaN((x1 - x0) * (y1 - y0)) || (isBlank && gd._context.staticPlot)) ? 'M0,0Z' : 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z') | ||
.attr('d', (isNaN((x1 - x0) * (y1 - y0)) || (isBlank && gd._context.staticPlot)) ? 'M0,0Z' : path) | ||
.call(Drawing.setClipUrl, plotinfo.layerClipId, gd); | ||
|
||
if(!fullLayout.uniformtext.mode && withTransition) { | ||
|
@@ -350,6 +464,7 @@ function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, opts, makeOnCom | |
if(textPosition === 'auto') { | ||
if(isOutmostBar) { | ||
// draw text using insideTextFont and check if it fits inside bar | ||
// TODO: Need to consider `cornerradius` here | ||
textPosition = 'inside'; | ||
|
||
font = Lib.ensureUniformFontSize(gd, insideTextFont); | ||
|
Uh oh!
There was an error while loading. Please reload this page.