Skip to content

Commit f235f7e

Browse files
authored
Merge pull request #6761 from plotly/rounded-bars
Add option for rounded corners on bar charts
2 parents 96aa46e + 8c45d15 commit f235f7e

32 files changed

+1033
-50
lines changed

draftlogs/6761_add.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add `layout.barcornerradius` and `trace.marker.cornerradius` properties to support rounding the corners of bar traces [[#6761](https://github.com/plotly/plotly.js/pull/6761)], with thanks to [Displayr](https://www.displayr.com) for sponsoring development!

src/components/legend/style.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -334,14 +334,19 @@ module.exports = function style(s, gd, legend) {
334334
var marker = trace.marker || {};
335335
var markerLine = marker.line || {};
336336

337+
// If bar has rounded corners, round corners of legend icon
338+
var pathStr = marker.cornerradius ?
339+
'M6,3a3,3,0,0,1-3,3H-3a3,3,0,0,1-3-3V-3a3,3,0,0,1,3-3H3a3,3,0,0,1,3,3Z' : // Square with rounded corners
340+
'M6,6H-6V-6H6Z'; // Normal square
341+
337342
var isVisible = (!desiredType) ? Registry.traceIs(trace, 'bar') :
338343
(trace.visible && trace.type === desiredType);
339344

340345
var barpath = d3.select(lThis).select('g.legendpoints')
341346
.selectAll('path.legend' + desiredType)
342347
.data(isVisible ? [d] : []);
343348
barpath.enter().append('path').classed('legend' + desiredType, true)
344-
.attr('d', 'M6,6H-6V-6H6Z')
349+
.attr('d', pathStr)
345350
.attr('transform', centerTransform);
346351
barpath.exit().remove();
347352

src/traces/bar/attributes.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,16 @@ var marker = extendFlat({
4242
editType: 'style',
4343
description: 'Sets the opacity of the bars.'
4444
},
45-
pattern: pattern
45+
pattern: pattern,
46+
cornerradius: {
47+
valType: 'any',
48+
editType: 'calc',
49+
description: [
50+
'Sets the rounding of corners. May be an integer number of pixels,',
51+
'or a percentage of bar width (as a string ending in %). Defaults to `layout.barcornerradius`.',
52+
'In stack or relative barmode, the first trace to set cornerradius is used for the whole stack.'
53+
].join(' ')
54+
},
4655
});
4756

4857
module.exports = {

src/traces/bar/cross_trace_calc.js

+92-1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ function setGroupPositions(gd, pa, sa, calcTraces, opts) {
111111
else excluded.push(calcTrace);
112112
}
113113

114+
// If any trace in `included` has a cornerradius, set cornerradius of all bars
115+
// in `included` to match the first trace which has a cornerradius
116+
standardizeCornerradius(included);
117+
114118
if(included.length) {
115119
setGroupPositionsInStackOrRelativeMode(gd, pa, sa, included, opts);
116120
}
@@ -119,10 +123,57 @@ function setGroupPositions(gd, pa, sa, calcTraces, opts) {
119123
}
120124
break;
121125
}
122-
126+
setCornerradius(calcTraces);
123127
collectExtents(calcTraces, pa);
124128
}
125129

130+
// Set cornerradiusvalue and cornerradiusform in calcTraces[0].t
131+
function setCornerradius(calcTraces) {
132+
var i, calcTrace, fullTrace, t, cr, crValue, crForm;
133+
134+
for(i = 0; i < calcTraces.length; i++) {
135+
calcTrace = calcTraces[i];
136+
fullTrace = calcTrace[0].trace;
137+
t = calcTrace[0].t;
138+
139+
if(t.cornerradiusvalue === undefined) {
140+
cr = fullTrace.marker ? fullTrace.marker.cornerradius : undefined;
141+
if(cr !== undefined) {
142+
crValue = isNumeric(cr) ? +cr : +cr.slice(0, -1);
143+
crForm = isNumeric(cr) ? 'px' : '%';
144+
t.cornerradiusvalue = crValue;
145+
t.cornerradiusform = crForm;
146+
}
147+
}
148+
}
149+
}
150+
151+
// Make sure all traces in a stack use the same cornerradius
152+
function standardizeCornerradius(calcTraces) {
153+
if(calcTraces.length < 2) return;
154+
var i, calcTrace, fullTrace, t;
155+
var cr, crValue, crForm;
156+
for(i = 0; i < calcTraces.length; i++) {
157+
calcTrace = calcTraces[i];
158+
fullTrace = calcTrace[0].trace;
159+
cr = fullTrace.marker ? fullTrace.marker.cornerradius : undefined;
160+
if(cr !== undefined) break;
161+
}
162+
// If any trace has cornerradius, store first cornerradius
163+
// in calcTrace[0].t so that all traces in stack use same cornerradius
164+
if(cr !== undefined) {
165+
crValue = isNumeric(cr) ? +cr : +cr.slice(0, -1);
166+
crForm = isNumeric(cr) ? 'px' : '%';
167+
for(i = 0; i < calcTraces.length; i++) {
168+
calcTrace = calcTraces[i];
169+
t = calcTrace[0].t;
170+
171+
t.cornerradiusvalue = crValue;
172+
t.cornerradiusform = crForm;
173+
}
174+
}
175+
}
176+
126177
function initBase(sa, calcTraces) {
127178
var i, j;
128179

@@ -713,6 +764,23 @@ function normalizeBars(sa, sieve, opts) {
713764
}
714765
}
715766

767+
// Add an `_sMin` and `_sMax` value for each bar representing the min and max size value
768+
// across all bars sharing the same position as that bar. These values are used for rounded
769+
// bar corners, to carry rounding down to lower bars in the stack as needed.
770+
function setHelperValuesForRoundedCorners(calcTraces, sMinByPos, sMaxByPos, pa) {
771+
var pLetter = getAxisLetter(pa);
772+
// Set `_sMin` and `_sMax` value for each bar
773+
for(var i = 0; i < calcTraces.length; i++) {
774+
var calcTrace = calcTraces[i];
775+
for(var j = 0; j < calcTrace.length; j++) {
776+
var bar = calcTrace[j];
777+
var pos = bar[pLetter];
778+
bar._sMin = sMinByPos[pos];
779+
bar._sMax = sMaxByPos[pos];
780+
}
781+
}
782+
}
783+
716784
// find the full position span of bars at each position
717785
// for use by hover, to ensure labels move in if bars are
718786
// narrower than the space they're in.
@@ -745,6 +813,18 @@ function collectExtents(calcTraces, pa) {
745813
return String(Math.round(roundFactor * (p - pMin)));
746814
};
747815

816+
// Find min and max size axis extent for each position
817+
// This is used for rounded bar corners, to carry rounding
818+
// down to lower bars in the case of stacked bars
819+
var sMinByPos = {};
820+
var sMaxByPos = {};
821+
822+
// Check whether any trace has rounded corners
823+
var anyTraceHasCornerradius = calcTraces.some(function(x) {
824+
var trace = x[0].trace;
825+
return 'marker' in trace && trace.marker.cornerradius;
826+
});
827+
748828
for(i = 0; i < calcTraces.length; i++) {
749829
cd = calcTraces[i];
750830
cd[0].t.extents = extents;
@@ -770,8 +850,19 @@ function collectExtents(calcTraces, pa) {
770850
di.p1 = di.p0 + di.w;
771851
di.s0 = di.b;
772852
di.s1 = di.s0 + di.s;
853+
854+
if(anyTraceHasCornerradius) {
855+
var sMin = Math.min(di.s0, di.s1) || 0;
856+
var sMax = Math.max(di.s0, di.s1) || 0;
857+
var pos = di[pLetter];
858+
sMinByPos[pos] = (pos in sMinByPos) ? Math.min(sMinByPos[pos], sMin) : sMin;
859+
sMaxByPos[pos] = (pos in sMaxByPos) ? Math.max(sMaxByPos[pos], sMax) : sMax;
860+
}
773861
}
774862
}
863+
if(anyTraceHasCornerradius) {
864+
setHelperValuesForRoundedCorners(calcTraces, sMinByPos, sMaxByPos, pa);
865+
}
775866
}
776867

777868
function getAxisLetter(ax) {

src/traces/bar/defaults.js

+39-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
var isNumeric = require('fast-isnumeric');
4+
35
var Lib = require('../../lib');
46
var Color = require('../../components/color');
57
var Registry = require('../../registry');
@@ -47,7 +49,6 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
4749
});
4850

4951
handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout);
50-
5152
var lineColor = (traceOut.marker.line || {}).color;
5253

5354
// override defaultColor for error bars with defaultLine
@@ -61,22 +62,50 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
6162
function crossTraceDefaults(fullData, fullLayout) {
6263
var traceIn, traceOut;
6364

64-
function coerce(attr) {
65-
return Lib.coerce(traceOut._input, traceOut, attributes, attr);
65+
function coerce(attr, dflt) {
66+
return Lib.coerce(traceOut._input, traceOut, attributes, attr, dflt);
6667
}
6768

68-
if(fullLayout.barmode === 'group') {
69-
for(var i = 0; i < fullData.length; i++) {
70-
traceOut = fullData[i];
69+
for(var i = 0; i < fullData.length; i++) {
70+
traceOut = fullData[i];
71+
72+
if(traceOut.type === 'bar') {
73+
traceIn = traceOut._input;
74+
// `marker.cornerradius` needs to be coerced here rather than in handleStyleDefaults()
75+
// because it needs to happen after `layout.barcornerradius` has been coerced
76+
var r = coerce('marker.cornerradius', fullLayout.barcornerradius);
77+
if(traceOut.marker) {
78+
traceOut.marker.cornerradius = validateCornerradius(r);
79+
}
7180

72-
if(traceOut.type === 'bar') {
73-
traceIn = traceOut._input;
81+
if(fullLayout.barmode === 'group') {
7482
handleGroupingDefaults(traceIn, traceOut, fullLayout, coerce);
7583
}
7684
}
7785
}
7886
}
7987

88+
// Returns a value equivalent to the given cornerradius value, if valid;
89+
// otherwise returns`undefined`.
90+
// Valid cornerradius values must be either:
91+
// - a numeric value (string or number) >= 0, or
92+
// - a string consisting of a number >= 0 followed by a % sign
93+
// If the given cornerradius value is a numeric string, it will be converted
94+
// to a number.
95+
function validateCornerradius(r) {
96+
if(isNumeric(r)) {
97+
r = +r;
98+
if(r >= 0) return r;
99+
} else if(typeof r === 'string') {
100+
r = r.trim();
101+
if(r.slice(-1) === '%' && isNumeric(r.slice(0, -1))) {
102+
r = +r.slice(0, -1);
103+
if(r >= 0) return r + '%';
104+
}
105+
}
106+
return undefined;
107+
}
108+
80109
function handleText(traceIn, traceOut, layout, coerce, textposition, opts) {
81110
opts = opts || {};
82111
var moduleHasSelected = !(opts.moduleHasSelected === false);
@@ -133,5 +162,6 @@ function handleText(traceIn, traceOut, layout, coerce, textposition, opts) {
133162
module.exports = {
134163
supplyDefaults: supplyDefaults,
135164
crossTraceDefaults: crossTraceDefaults,
136-
handleText: handleText
165+
handleText: handleText,
166+
validateCornerradius: validateCornerradius,
137167
};

src/traces/bar/layout_attributes.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,13 @@ module.exports = {
5151
'Sets the gap (in plot fraction) between bars of',
5252
'the same location coordinate.'
5353
].join(' ')
54-
}
54+
},
55+
barcornerradius: {
56+
valType: 'any',
57+
editType: 'calc',
58+
description: [
59+
'Sets the rounding of bar corners. May be an integer number of pixels,',
60+
'or a percentage of bar width (as a string ending in %).'
61+
].join(' ')
62+
},
5563
};

src/traces/bar/layout_defaults.js

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ var Axes = require('../../plots/cartesian/axes');
55
var Lib = require('../../lib');
66

77
var layoutAttributes = require('./layout_attributes');
8+
var validateCornerradius = require('./defaults').validateCornerradius;
9+
810

911
module.exports = function(layoutIn, layoutOut, fullData) {
1012
function coerce(attr, dflt) {
@@ -47,4 +49,6 @@ module.exports = function(layoutIn, layoutOut, fullData) {
4749

4850
coerce('bargap', (shouldBeGapless && !gappedAnyway) ? 0 : 0.2);
4951
coerce('bargroupgap');
52+
var r = coerce('barcornerradius');
53+
layoutOut.barcornerradius = validateCornerradius(r);
5054
};

0 commit comments

Comments
 (0)