diff --git a/draftlogs/6761_add.md b/draftlogs/6761_add.md
new file mode 100644
index 00000000000..1aee00721dd
--- /dev/null
+++ b/draftlogs/6761_add.md
@@ -0,0 +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!
\ No newline at end of file
diff --git a/src/components/legend/style.js b/src/components/legend/style.js
index 8a15c6db0ea..774d269d097 100644
--- a/src/components/legend/style.js
+++ b/src/components/legend/style.js
@@ -334,6 +334,11 @@ module.exports = function style(s, gd, legend) {
var marker = trace.marker || {};
var markerLine = marker.line || {};
+ // If bar has rounded corners, round corners of legend icon
+ var pathStr = marker.cornerradius ?
+ '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
+ 'M6,6H-6V-6H6Z'; // Normal square
+
var isVisible = (!desiredType) ? Registry.traceIs(trace, 'bar') :
(trace.visible && trace.type === desiredType);
@@ -341,7 +346,7 @@ module.exports = function style(s, gd, legend) {
.selectAll('path.legend' + desiredType)
.data(isVisible ? [d] : []);
barpath.enter().append('path').classed('legend' + desiredType, true)
- .attr('d', 'M6,6H-6V-6H6Z')
+ .attr('d', pathStr)
.attr('transform', centerTransform);
barpath.exit().remove();
diff --git a/src/traces/bar/attributes.js b/src/traces/bar/attributes.js
index 12d909f4c95..092d9fe52bf 100644
--- a/src/traces/bar/attributes.js
+++ b/src/traces/bar/attributes.js
@@ -42,7 +42,16 @@ var marker = extendFlat({
editType: 'style',
description: 'Sets the opacity of the bars.'
},
- pattern: pattern
+ pattern: pattern,
+ cornerradius: {
+ valType: 'any',
+ editType: 'calc',
+ description: [
+ 'Sets the rounding of corners. May be an integer number of pixels,',
+ 'or a percentage of bar width (as a string ending in %). Defaults to `layout.barcornerradius`.',
+ 'In stack or relative barmode, the first trace to set cornerradius is used for the whole stack.'
+ ].join(' ')
+ },
});
module.exports = {
diff --git a/src/traces/bar/cross_trace_calc.js b/src/traces/bar/cross_trace_calc.js
index 6af4eb73a41..61df5cf4d23 100644
--- a/src/traces/bar/cross_trace_calc.js
+++ b/src/traces/bar/cross_trace_calc.js
@@ -111,6 +111,10 @@ function setGroupPositions(gd, pa, sa, calcTraces, opts) {
else excluded.push(calcTrace);
}
+ // If any trace in `included` has a cornerradius, set cornerradius of all bars
+ // in `included` to match the first trace which has a cornerradius
+ standardizeCornerradius(included);
+
if(included.length) {
setGroupPositionsInStackOrRelativeMode(gd, pa, sa, included, opts);
}
@@ -119,10 +123,57 @@ function setGroupPositions(gd, pa, sa, calcTraces, opts) {
}
break;
}
-
+ setCornerradius(calcTraces);
collectExtents(calcTraces, pa);
}
+// Set cornerradiusvalue and cornerradiusform in calcTraces[0].t
+function setCornerradius(calcTraces) {
+ var i, calcTrace, fullTrace, t, cr, crValue, crForm;
+
+ for(i = 0; i < calcTraces.length; i++) {
+ calcTrace = calcTraces[i];
+ fullTrace = calcTrace[0].trace;
+ t = calcTrace[0].t;
+
+ if(t.cornerradiusvalue === undefined) {
+ cr = fullTrace.marker ? fullTrace.marker.cornerradius : undefined;
+ if(cr !== undefined) {
+ crValue = isNumeric(cr) ? +cr : +cr.slice(0, -1);
+ crForm = isNumeric(cr) ? 'px' : '%';
+ t.cornerradiusvalue = crValue;
+ t.cornerradiusform = crForm;
+ }
+ }
+ }
+}
+
+// Make sure all traces in a stack use the same cornerradius
+function standardizeCornerradius(calcTraces) {
+ if(calcTraces.length < 2) return;
+ var i, calcTrace, fullTrace, t;
+ var cr, crValue, crForm;
+ for(i = 0; i < calcTraces.length; i++) {
+ calcTrace = calcTraces[i];
+ fullTrace = calcTrace[0].trace;
+ cr = fullTrace.marker ? fullTrace.marker.cornerradius : undefined;
+ if(cr !== undefined) break;
+ }
+ // If any trace has cornerradius, store first cornerradius
+ // in calcTrace[0].t so that all traces in stack use same cornerradius
+ if(cr !== undefined) {
+ crValue = isNumeric(cr) ? +cr : +cr.slice(0, -1);
+ crForm = isNumeric(cr) ? 'px' : '%';
+ for(i = 0; i < calcTraces.length; i++) {
+ calcTrace = calcTraces[i];
+ t = calcTrace[0].t;
+
+ t.cornerradiusvalue = crValue;
+ t.cornerradiusform = crForm;
+ }
+ }
+}
+
function initBase(sa, calcTraces) {
var i, j;
@@ -713,6 +764,23 @@ 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, pa) {
+ var pLetter = getAxisLetter(pa);
+ // Set `_sMin` and `_sMax` value for each bar
+ for(var i = 0; i < calcTraces.length; i++) {
+ var calcTrace = calcTraces[i];
+ for(var j = 0; j < calcTrace.length; j++) {
+ var bar = calcTrace[j];
+ var pos = bar[pLetter];
+ bar._sMin = sMinByPos[pos];
+ bar._sMax = sMaxByPos[pos];
+ }
+ }
+}
+
// 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 +813,18 @@ 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 = {};
+
+ // Check whether any trace has rounded corners
+ var anyTraceHasCornerradius = calcTraces.some(function(x) {
+ var trace = x[0].trace;
+ return 'marker' in trace && trace.marker.cornerradius;
+ });
+
for(i = 0; i < calcTraces.length; i++) {
cd = calcTraces[i];
cd[0].t.extents = extents;
@@ -770,8 +850,19 @@ function collectExtents(calcTraces, pa) {
di.p1 = di.p0 + di.w;
di.s0 = di.b;
di.s1 = di.s0 + di.s;
+
+ if(anyTraceHasCornerradius) {
+ var sMin = Math.min(di.s0, di.s1) || 0;
+ var sMax = Math.max(di.s0, di.s1) || 0;
+ var pos = di[pLetter];
+ sMinByPos[pos] = (pos in sMinByPos) ? Math.min(sMinByPos[pos], sMin) : sMin;
+ sMaxByPos[pos] = (pos in sMaxByPos) ? Math.max(sMaxByPos[pos], sMax) : sMax;
+ }
}
}
+ if(anyTraceHasCornerradius) {
+ setHelperValuesForRoundedCorners(calcTraces, sMinByPos, sMaxByPos, pa);
+ }
}
function getAxisLetter(ax) {
diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js
index 3ed0e57ddea..bfa15379cba 100644
--- a/src/traces/bar/defaults.js
+++ b/src/traces/bar/defaults.js
@@ -1,5 +1,7 @@
'use strict';
+var isNumeric = require('fast-isnumeric');
+
var Lib = require('../../lib');
var Color = require('../../components/color');
var Registry = require('../../registry');
@@ -47,7 +49,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,22 +62,50 @@ 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;
+ // `marker.cornerradius` needs to be coerced here rather than in handleStyleDefaults()
+ // because it needs to happen after `layout.barcornerradius` has been coerced
+ var r = coerce('marker.cornerradius', fullLayout.barcornerradius);
+ if(traceOut.marker) {
+ traceOut.marker.cornerradius = validateCornerradius(r);
+ }
- if(traceOut.type === 'bar') {
- traceIn = traceOut._input;
+ if(fullLayout.barmode === 'group') {
handleGroupingDefaults(traceIn, traceOut, fullLayout, coerce);
}
}
}
}
+// Returns a value equivalent to the given cornerradius value, if valid;
+// otherwise returns`undefined`.
+// Valid cornerradius values must be either:
+// - a numeric value (string or number) >= 0, or
+// - a string consisting of a number >= 0 followed by a % sign
+// If the given cornerradius value is a numeric string, it will be converted
+// to a number.
+function validateCornerradius(r) {
+ if(isNumeric(r)) {
+ r = +r;
+ if(r >= 0) return r;
+ } else if(typeof r === 'string') {
+ r = r.trim();
+ if(r.slice(-1) === '%' && isNumeric(r.slice(0, -1))) {
+ r = +r.slice(0, -1);
+ if(r >= 0) return r + '%';
+ }
+ }
+ return undefined;
+}
+
function handleText(traceIn, traceOut, layout, coerce, textposition, opts) {
opts = opts || {};
var moduleHasSelected = !(opts.moduleHasSelected === false);
@@ -133,5 +162,6 @@ function handleText(traceIn, traceOut, layout, coerce, textposition, opts) {
module.exports = {
supplyDefaults: supplyDefaults,
crossTraceDefaults: crossTraceDefaults,
- handleText: handleText
+ handleText: handleText,
+ validateCornerradius: validateCornerradius,
};
diff --git a/src/traces/bar/layout_attributes.js b/src/traces/bar/layout_attributes.js
index 8ef1fbab32e..bdbb12605bc 100644
--- a/src/traces/bar/layout_attributes.js
+++ b/src/traces/bar/layout_attributes.js
@@ -51,5 +51,13 @@ module.exports = {
'Sets the gap (in plot fraction) between bars of',
'the same location coordinate.'
].join(' ')
- }
+ },
+ barcornerradius: {
+ valType: 'any',
+ editType: 'calc',
+ description: [
+ 'Sets the rounding of bar corners. May be an integer number of pixels,',
+ 'or a percentage of bar width (as a string ending in %).'
+ ].join(' ')
+ },
};
diff --git a/src/traces/bar/layout_defaults.js b/src/traces/bar/layout_defaults.js
index 99324539921..bebe022d8a6 100644
--- a/src/traces/bar/layout_defaults.js
+++ b/src/traces/bar/layout_defaults.js
@@ -5,6 +5,8 @@ var Axes = require('../../plots/cartesian/axes');
var Lib = require('../../lib');
var layoutAttributes = require('./layout_attributes');
+var validateCornerradius = require('./defaults').validateCornerradius;
+
module.exports = function(layoutIn, layoutOut, fullData) {
function coerce(attr, dflt) {
@@ -47,4 +49,6 @@ module.exports = function(layoutIn, layoutOut, fullData) {
coerce('bargap', (shouldBeGapless && !gappedAnyway) ? 0 : 0.2);
coerce('bargroupgap');
+ var r = coerce('barcornerradius');
+ layoutOut.barcornerradius = validateCornerradius(r);
};
diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js
index 260ca99cf28..485d9a3dc55 100644
--- a/src/traces/bar/plot.js
+++ b/src/traces/bar/plot.js
@@ -34,6 +34,13 @@ function getKeyFunc(trace) {
}
}
+// Returns -1 if v < 0, 1 if v > 0, and 0 if v == 0
+function sign(v) {
+ return (v > 0) - (v < 0);
+}
+
+// Returns 1 if a < b and -1 otherwise
+// (For the purposes of this module we don't care about the case where a == b)
function dirSign(a, b) {
return (a < b) ? 1 : -1;
}
@@ -96,11 +103,12 @@ function plot(gd, plotinfo, cdModule, traceLayer, opts, makeOnCompleteCallback)
var bartraces = Lib.makeTraceGroups(traceLayer, cdModule, 'trace bars').each(function(cd) {
var plotGroup = d3.select(this);
var trace = cd[0].trace;
+ var t = cd[0].t;
var isWaterfall = (trace.type === 'waterfall');
var isFunnel = (trace.type === 'funnel');
+ var isHistogram = (trace.type === 'histogram');
var isBar = (trace.type === 'bar');
var shouldDisplayZeros = (isBar || isFunnel);
-
var adjustPixel = 0;
if(isWaterfall && trace.connector.visible && trace.connector.mode === 'between') {
adjustPixel = trace.connector.line.width / 2;
@@ -215,6 +223,8 @@ 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
@@ -222,20 +232,138 @@ function plot(gd, plotinfo, cdModule, traceLayer, opts, makeOnCompleteCallback)
// 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;
+
+ // Decide whether to use upper or lower bound of current bar stack
+ // as reference point for rounding
+ var outerBound;
+ if(di.s0 > 0) {
+ outerBound = di._sMax;
+ } else if(di.s0 < 0) {
+ outerBound = di._sMin;
+ } else {
+ outerBound = di.s1 > 0 ? di._sMax : di._sMin;
+ }
+
+ // Calculate corner radius of bar in pixels
+ function calcCornerRadius(crValue, crForm) {
+ if(!crValue) return 0;
+
+ 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(c2p(outerBound, true) - c2p(0, true)));
+ var maxRadius = di.hasB ? Math.min(barWidth / 2, barLength / 2) : Math.min(barWidth / 2, stackedBarTotalLength);
+ var crPx;
+
+ if(crForm === '%') {
+ // If radius is given as a % string, convert to number of pixels
+ var crPercent = Math.min(50, crValue);
+ crPx = barWidth * (crPercent / 100);
+ } else {
+ // Otherwise, it's already a number of pixels, use the given value
+ crPx = crValue;
+ }
+ return fixpx(Math.max(Math.min(crPx, maxRadius), 0));
+ }
+ // Exclude anything which is not explicitly a bar or histogram chart from rounding
+ var r = (isBar || isHistogram) ? calcCornerRadius(t.cornerradiusvalue, t.cornerradiusform) : 0;
+ // Construct path string for bar
+ var path, h;
+ // Default rectangular path (used if no rounding)
+ var rectanglePath = 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z';
+ var overhead = 0;
+ if(r && di.s) {
+ // Bar has cornerradius, and nonzero size
+ // Check amount of 'overhead' (bars stacked above this one)
+ // to see whether we need to round or not
+ var refPoint = sign(di.s0) === 0 || sign(di.s) === sign(di.s0) ? di.s1 : di.s0;
+ overhead = fixpx(!di.hasB ? Math.abs(c2p(outerBound, true) - c2p(refPoint, true)) : 0);
+ 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 + ' ' + x0 + ',' + (y0 + 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 + ' ' + x0 + ',' + (y0 + 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 = rectanglePath;
+ }
+ } else {
+ // No cornerradius, just draw a rectangle
+ path = rectanglePath;
+ }
+
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) {
@@ -243,7 +371,7 @@ function plot(gd, plotinfo, cdModule, traceLayer, opts, makeOnCompleteCallback)
Drawing.singlePointStyle(di, sel, trace, styleFns, gd);
}
- appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, opts, makeOnCompleteCallback);
+ appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, r, overhead, opts, makeOnCompleteCallback);
if(plotinfo.layerClipId) {
Drawing.hideOutsideRangePoint(di, bar.select('text'), xa, ya, trace.xcalendar, trace.ycalendar);
@@ -260,7 +388,7 @@ function plot(gd, plotinfo, cdModule, traceLayer, opts, makeOnCompleteCallback)
Registry.getComponentMethod('errorbars', 'plot')(gd, bartraces, plotinfo, opts);
}
-function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, opts, makeOnCompleteCallback) {
+function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, r, overhead, opts, makeOnCompleteCallback) {
var xa = plotinfo.xaxis;
var ya = plotinfo.yaxis;
@@ -288,6 +416,7 @@ function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, opts, makeOnCom
var isHorizontal = (trace.orientation === 'h');
var text = getText(fullLayout, cd, i, xa, ya);
+
textPosition = getTextPosition(trace, i);
// compute text position
@@ -297,6 +426,8 @@ function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, opts, makeOnCom
var calcBar = cd[i];
var isOutmostBar = !inStackOrRelativeMode || calcBar._outmost;
+ var hasB = calcBar.hasB;
+ var barIsRounded = r && (r - overhead) > TEXTPAD;
if(!text ||
textPosition === 'none' ||
@@ -311,6 +442,7 @@ function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, opts, makeOnCom
var barColor = style.getBarColor(cd[i], trace);
var insideTextFont = style.getInsideTextFont(trace, i, layoutFont, barColor);
var outsideTextFont = style.getOutsideTextFont(trace, i, layoutFont);
+ var insidetextanchor = trace.insidetextanchor || 'end';
// Special case: don't use the c2p(v, true) value on log size axes,
// so that we can get correctly inside text scaling
@@ -333,11 +465,16 @@ function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, opts, makeOnCom
}
}
+ // Compute width and height of bar
+ var lx = Math.abs(x1 - x0);
+ var ly = Math.abs(y1 - y0);
+
// padding excluded
- var barWidth = Math.abs(x1 - x0) - 2 * TEXTPAD;
- var barHeight = Math.abs(y1 - y0) - 2 * TEXTPAD;
+ var barWidth = lx - 2 * TEXTPAD;
+ var barHeight = ly - 2 * TEXTPAD;
var textSelection;
+
var textBB;
var textWidth;
var textHeight;
@@ -356,22 +493,36 @@ function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, opts, makeOnCom
textSelection = appendTextNode(bar, text, font);
- textBB = Drawing.bBox(textSelection.node()),
- textWidth = textBB.width,
+ textBB = Drawing.bBox(textSelection.node());
+ textWidth = textBB.width;
textHeight = textBB.height;
var textHasSize = (textWidth > 0 && textHeight > 0);
- var fitsInside = (textWidth <= barWidth && textHeight <= barHeight);
- var fitsInsideIfRotated = (textWidth <= barHeight && textHeight <= barWidth);
- var fitsInsideIfShrunk = (isHorizontal) ?
- (barWidth >= textWidth * (barHeight / textHeight)) :
- (barHeight >= textHeight * (barWidth / textWidth));
-
- if(textHasSize && (
- fitsInside ||
- fitsInsideIfRotated ||
- fitsInsideIfShrunk)
- ) {
+
+ var fitsInside;
+ if(barIsRounded) {
+ // If bar is rounded, check if text fits between rounded corners
+ if(hasB) {
+ fitsInside = (
+ textfitsInsideBar(barWidth - 2 * r, barHeight, textWidth, textHeight, isHorizontal) ||
+ textfitsInsideBar(barWidth, barHeight - 2 * r, textWidth, textHeight, isHorizontal)
+ );
+ } else if(isHorizontal) {
+ fitsInside = (
+ textfitsInsideBar(barWidth - (r - overhead), barHeight, textWidth, textHeight, isHorizontal) ||
+ textfitsInsideBar(barWidth, barHeight - 2 * (r - overhead), textWidth, textHeight, isHorizontal)
+ );
+ } else {
+ fitsInside = (
+ textfitsInsideBar(barWidth, barHeight - (r - overhead), textWidth, textHeight, isHorizontal) ||
+ textfitsInsideBar(barWidth - 2 * (r - overhead), barHeight, textWidth, textHeight, isHorizontal)
+ );
+ }
+ } else {
+ fitsInside = textfitsInsideBar(barWidth, barHeight, textWidth, textHeight, isHorizontal);
+ }
+
+ if(textHasSize && fitsInside) {
textPosition = 'inside';
} else {
textPosition = 'outside';
@@ -424,7 +575,10 @@ function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, opts, makeOnCom
isHorizontal: isHorizontal,
constrained: constrained,
angle: angle,
- anchor: trace.insidetextanchor
+ anchor: insidetextanchor,
+ hasB: hasB,
+ r: r,
+ overhead: overhead,
});
}
@@ -436,6 +590,16 @@ function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, opts, makeOnCom
Lib.setTransormAndDisplay(s, transform);
}
+function textfitsInsideBar(barWidth, barHeight, textWidth, textHeight, isHorizontal) {
+ if(barWidth < 0 || barHeight < 0) return false;
+ var fitsInside = (textWidth <= barWidth && textHeight <= barHeight);
+ var fitsInsideIfRotated = (textWidth <= barHeight && textHeight <= barWidth);
+ var fitsInsideIfShrunk = (isHorizontal) ?
+ (barWidth >= textWidth * (barHeight / textHeight)) :
+ (barHeight >= textHeight * (barWidth / textWidth));
+ return fitsInside || fitsInsideIfRotated || fitsInsideIfShrunk;
+}
+
function getRotateFromAngle(angle) {
return (angle === 'auto') ? 0 : angle;
}
@@ -455,15 +619,19 @@ function toMoveInsideBar(x0, x1, y0, y1, textBB, opts) {
var isHorizontal = !!opts.isHorizontal;
var constrained = !!opts.constrained;
var angle = opts.angle || 0;
- var anchor = opts.anchor || 'end';
+ var anchor = opts.anchor;
var isEnd = anchor === 'end';
var isStart = anchor === 'start';
var leftToRight = opts.leftToRight || 0; // left: -1, center: 0, right: 1
var toRight = (leftToRight + 1) / 2;
var toLeft = 1 - toRight;
+ var hasB = opts.hasB;
+ var r = opts.r;
+ var overhead = opts.overhead;
var textWidth = textBB.width;
var textHeight = textBB.height;
+
var lx = Math.abs(x1 - x0);
var ly = Math.abs(y1 - y0);
@@ -480,21 +648,31 @@ function toMoveInsideBar(x0, x1, y0, y1, textBB, opts) {
if((angle === 'auto') &&
!(textWidth <= lx && textHeight <= ly) &&
(textWidth > lx || textHeight > ly) && (
- !(textWidth > ly || textHeight > lx) ||
- ((textWidth < textHeight) !== (lx < ly))
- )) {
+ !(textWidth > ly || textHeight > lx) ||
+ ((textWidth < textHeight) !== (lx < ly))
+ )) {
rotate += 90;
}
var t = getRotatedTextSize(textBB, rotate);
- var scale = 1;
- if(constrained) {
- scale = Math.min(
- 1,
- lx / t.x,
- ly / t.y
- );
+ var scale, padForRounding;
+ // Scale text for rounded bars
+ if(r && (r - overhead) > TEXTPAD) {
+ var scaleAndPad = scaleTextForRoundedBar(x0, x1, y0, y1, t, r, overhead, isHorizontal, hasB);
+ scale = scaleAndPad.scale;
+ padForRounding = scaleAndPad.pad;
+ // Scale text for non-rounded bars
+ } else {
+ scale = 1;
+ if(constrained) {
+ scale = Math.min(
+ 1,
+ lx / t.x,
+ ly / t.y
+ );
+ }
+ padForRounding = 0;
}
// compute text and target positions
@@ -512,6 +690,11 @@ function toMoveInsideBar(x0, x1, y0, y1, textBB, opts) {
var anchorY = 0;
if(isStart || isEnd) {
var extrapad = (isHorizontal ? t.x : t.y) / 2;
+
+ if(r && (isEnd || hasB)) {
+ textpad += padForRounding;
+ }
+
var dir = isHorizontal ? dirSign(x0, x1) : dirSign(y0, y1);
if(isHorizontal) {
@@ -545,6 +728,57 @@ function toMoveInsideBar(x0, x1, y0, y1, textBB, opts) {
};
}
+function scaleTextForRoundedBar(x0, x1, y0, y1, t, r, overhead, isHorizontal, hasB) {
+ var barWidth = Math.max(0, Math.abs(x1 - x0) - 2 * TEXTPAD);
+ var barHeight = Math.max(0, Math.abs(y1 - y0) - 2 * TEXTPAD);
+ var R = r - TEXTPAD;
+ var clippedR = overhead ? R - Math.sqrt(R * R - (R - overhead) * (R - overhead)) : R;
+ var rX = hasB ? R * 2 : (isHorizontal ? R - overhead : 2 * clippedR);
+ var rY = hasB ? R * 2 : (isHorizontal ? 2 * clippedR : R - overhead);
+ var a, b, c;
+ var scale, pad;
+
+
+ if(t.y / t.x >= barHeight / (barWidth - rX)) {
+ // Case 1 (Tall text)
+ scale = barHeight / t.y;
+ } else if(t.y / t.x <= (barHeight - rY) / barWidth) {
+ // Case 2 (Wide text)
+ scale = barWidth / t.x;
+ } else if(!hasB && isHorizontal) {
+ // Case 3a (Quadratic case, two side corners are rounded)
+ a = t.x * t.x + t.y * t.y / 4;
+ b = -2 * t.x * (barWidth - R) - t.y * (barHeight / 2 - R);
+ c = (barWidth - R) * (barWidth - R) + (barHeight / 2 - R) * (barHeight / 2 - R) - R * R;
+
+ scale = (-b + Math.sqrt(b * b - 4 * a * c)) / (2 * a);
+ } else if(!hasB) {
+ // Case 3b (Quadratic case, two top/bottom corners are rounded)
+ a = t.x * t.x / 4 + t.y * t.y;
+ b = -t.x * (barWidth / 2 - R) - 2 * t.y * (barHeight - R);
+ c = (barWidth / 2 - R) * (barWidth / 2 - R) + (barHeight - R) * (barHeight - R) - R * R;
+
+ scale = (-b + Math.sqrt(b * b - 4 * a * c)) / (2 * a);
+ } else {
+ // Case 4 (Quadratic case, all four corners are rounded)
+ a = (t.x * t.x + t.y * t.y) / 4;
+ b = -t.x * (barWidth / 2 - R) - t.y * (barHeight / 2 - R);
+ c = (barWidth / 2 - R) * (barWidth / 2 - R) + (barHeight / 2 - R) * (barHeight / 2 - R) - R * R;
+ scale = (-b + Math.sqrt(b * b - 4 * a * c)) / (2 * a);
+ }
+
+ // Scale should not be larger than 1
+ scale = Math.min(1, scale);
+
+ if(isHorizontal) {
+ pad = Math.max(0, R - Math.sqrt(Math.max(0, R * R - (R - (barHeight - t.y * scale) / 2) * (R - (barHeight - t.y * scale) / 2))) - overhead);
+ } else {
+ pad = Math.max(0, R - Math.sqrt(Math.max(0, R * R - (R - (barWidth - t.x * scale) / 2) * (R - (barWidth - t.x * scale) / 2))) - overhead);
+ }
+
+ return { scale: scale, pad: pad };
+}
+
function toMoveOutsideBar(x0, x1, y0, y1, textBB, opts) {
var isHorizontal = !!opts.isHorizontal;
var constrained = !!opts.constrained;
diff --git a/src/traces/barpolar/attributes.js b/src/traces/barpolar/attributes.js
index 3b4704ba985..0c0349ec4f2 100644
--- a/src/traces/barpolar/attributes.js
+++ b/src/traces/barpolar/attributes.js
@@ -5,6 +5,7 @@ var extendFlat = require('../../lib/extend').extendFlat;
var scatterPolarAttrs = require('../scatterpolar/attributes');
var barAttrs = require('../bar/attributes');
+
module.exports = {
r: scatterPolarAttrs.r,
theta: scatterPolarAttrs.theta,
@@ -60,7 +61,7 @@ module.exports = {
// constraintext: {},
// cliponaxis: extendFlat({}, barAttrs.cliponaxis, {dflt: false}),
- marker: barAttrs.marker,
+ marker: barPolarMarker(),
hoverinfo: scatterPolarAttrs.hoverinfo,
hovertemplate: hovertemplateAttrs(),
@@ -71,3 +72,9 @@ module.exports = {
// error_x (error_r, error_theta)
// error_y
};
+
+function barPolarMarker() {
+ var marker = extendFlat({}, barAttrs.marker);
+ delete marker.cornerradius;
+ return marker;
+}
diff --git a/src/traces/funnel/attributes.js b/src/traces/funnel/attributes.js
index 0ac65a5ad59..501738f9cca 100644
--- a/src/traces/funnel/attributes.js
+++ b/src/traces/funnel/attributes.js
@@ -115,5 +115,6 @@ module.exports = {
function funnelMarker() {
var marker = extendFlat({}, barAttrs.marker);
delete marker.pattern;
+ delete marker.cornerradius;
return marker;
}
diff --git a/src/traces/histogram/cross_trace_defaults.js b/src/traces/histogram/cross_trace_defaults.js
index 5e09a9e34c6..d389c095b68 100644
--- a/src/traces/histogram/cross_trace_defaults.js
+++ b/src/traces/histogram/cross_trace_defaults.js
@@ -5,6 +5,7 @@ var axisIds = require('../../plots/cartesian/axis_ids');
var traceIs = require('../../registry').traceIs;
var handleGroupingDefaults = require('../scatter/grouping_defaults');
+var validateCornerradius = require('../bar/defaults').validateCornerradius;
var nestedProperty = Lib.nestedProperty;
var getAxisGroup = require('../../plots/cartesian/constraints').getAxisGroup;
@@ -101,6 +102,13 @@ module.exports = function crossTraceDefaults(fullData, fullLayout) {
delete traceOut._xautoBinFinished;
delete traceOut._yautoBinFinished;
+ if(traceOut.type === 'histogram') {
+ var r = coerce('marker.cornerradius', fullLayout.barcornerradius);
+ if(traceOut.marker) {
+ traceOut.marker.cornerradius = validateCornerradius(r);
+ }
+ }
+
// N.B. need to coerce *alignmentgroup* before *bingroup*, as traces
// in same alignmentgroup "have to match"
if(!traceIs(traceOut, '2dMap')) {
diff --git a/test/image/baselines/bar_gantt-chart.png b/test/image/baselines/bar_gantt-chart.png
index b3be4f1cda3..fb7e3ab936f 100644
Binary files a/test/image/baselines/bar_gantt-chart.png and b/test/image/baselines/bar_gantt-chart.png differ
diff --git a/test/image/baselines/hist_cum_stacked.png b/test/image/baselines/hist_cum_stacked.png
index 950ffa6510f..54dce8a6544 100644
Binary files a/test/image/baselines/hist_cum_stacked.png and b/test/image/baselines/hist_cum_stacked.png differ
diff --git a/test/image/baselines/histogram-offsetgroups.png b/test/image/baselines/histogram-offsetgroups.png
index 74a1b96cdc6..15a8d865ecd 100644
Binary files a/test/image/baselines/histogram-offsetgroups.png and b/test/image/baselines/histogram-offsetgroups.png differ
diff --git a/test/image/baselines/histogram_colorscale.png b/test/image/baselines/histogram_colorscale.png
index f3e859c2f49..3adf7271a19 100644
Binary files a/test/image/baselines/histogram_colorscale.png and b/test/image/baselines/histogram_colorscale.png differ
diff --git a/test/image/baselines/period_positioning9.png b/test/image/baselines/period_positioning9.png
index 718cab30074..e4571743820 100644
Binary files a/test/image/baselines/period_positioning9.png and b/test/image/baselines/period_positioning9.png differ
diff --git a/test/image/baselines/zz-bar-rounded-corners.png b/test/image/baselines/zz-bar-rounded-corners.png
new file mode 100644
index 00000000000..22403ce8aa0
Binary files /dev/null and b/test/image/baselines/zz-bar-rounded-corners.png differ
diff --git a/test/image/baselines/zz-round-bar_attrs_relative.png b/test/image/baselines/zz-round-bar_attrs_relative.png
new file mode 100644
index 00000000000..f7c41079410
Binary files /dev/null and b/test/image/baselines/zz-round-bar_attrs_relative.png differ
diff --git a/test/image/baselines/zz-round-bar_stackto100_negative.png b/test/image/baselines/zz-round-bar_stackto100_negative.png
new file mode 100644
index 00000000000..f83fcf666f1
Binary files /dev/null and b/test/image/baselines/zz-round-bar_stackto100_negative.png differ
diff --git a/test/image/baselines/zz-round-worldcup.png b/test/image/baselines/zz-round-worldcup.png
new file mode 100644
index 00000000000..586622673b7
Binary files /dev/null and b/test/image/baselines/zz-round-worldcup.png differ
diff --git a/test/image/baselines/zz-rounded-bar_display_height_zero_no_line_width.png b/test/image/baselines/zz-rounded-bar_display_height_zero_no_line_width.png
new file mode 100644
index 00000000000..f8774543e42
Binary files /dev/null and b/test/image/baselines/zz-rounded-bar_display_height_zero_no_line_width.png differ
diff --git a/test/image/mocks/bar_gantt-chart.json b/test/image/mocks/bar_gantt-chart.json
index 4262c6e1bce..32ce571096a 100644
--- a/test/image/mocks/bar_gantt-chart.json
+++ b/test/image/mocks/bar_gantt-chart.json
@@ -68,6 +68,7 @@
}
],
"layout": {
+ "barcornerradius": "50%",
"width": 1000,
"height": 500,
"title": {
diff --git a/test/image/mocks/hist_cum_stacked.json b/test/image/mocks/hist_cum_stacked.json
index cf24af2ba36..00d8c4ffdf6 100644
--- a/test/image/mocks/hist_cum_stacked.json
+++ b/test/image/mocks/hist_cum_stacked.json
@@ -40,6 +40,7 @@
"yaxis2": {"domain": [0.8, 1], "showline": false, "showticklabels": false},
"height": 300,
"width": 400,
+ "barcornerradius": "50%",
"barmode": "stack"
}
}
diff --git a/test/image/mocks/histogram-offsetgroups.json b/test/image/mocks/histogram-offsetgroups.json
index d27a969f75e..494140d0925 100644
--- a/test/image/mocks/histogram-offsetgroups.json
+++ b/test/image/mocks/histogram-offsetgroups.json
@@ -58,6 +58,7 @@
}
],
"layout": {
+ "barcornerradius": "50%",
"uniformtext": {
"mode": "hide",
"minsize": 5
diff --git a/test/image/mocks/histogram_colorscale.json b/test/image/mocks/histogram_colorscale.json
index e07eab4bbc0..8bebe54597a 100644
--- a/test/image/mocks/histogram_colorscale.json
+++ b/test/image/mocks/histogram_colorscale.json
@@ -22,6 +22,7 @@
}
],
"layout": {
+ "barcornerradius": "50%",
"height": 500,
"width": 700
}
diff --git a/test/image/mocks/period_positioning9.json b/test/image/mocks/period_positioning9.json
index 9a25492229b..75f2e0fc8a9 100644
--- a/test/image/mocks/period_positioning9.json
+++ b/test/image/mocks/period_positioning9.json
@@ -22,7 +22,7 @@
"l": 25,
"r": 25
},
-
+ "barcornerradius": "50%",
"xaxis": {
"dtick": "M6",
"tickformat": "%Y H%h",
diff --git a/test/image/mocks/zz-bar-rounded-corners.json b/test/image/mocks/zz-bar-rounded-corners.json
new file mode 100644
index 00000000000..14e6da72248
--- /dev/null
+++ b/test/image/mocks/zz-bar-rounded-corners.json
@@ -0,0 +1,96 @@
+{
+ "data": [
+ {
+ "x": [
+ "giraffes",
+ "orangutans",
+ "monkeys",
+ "capybaras"
+ ],
+ "y": [20, -14, 2, 18],
+ "type": "bar",
+ "texttemplate": "%{y}",
+ "textfont": {"size": 20},
+ "marker": {"cornerradius": 15},
+ "name": "Reversed"
+ },
+ {
+ "y": [
+ "giraffes",
+ "orangutans",
+ "monkeys",
+ "capybaras"
+ ],
+ "x": [20, -4, 1, 18],
+ "base": [1, 2, 1, 2],
+ "type": "bar",
+ "orientation": "h",
+ "texttemplate": "%{y}: %{x}",
+ "textfont": {"size": 10},
+ "xaxis": "x2",
+ "yaxis": "y2",
+ "name": "Normal"
+ },
+ {
+ "x": [
+ "giraffes",
+ "orangutans",
+ "monkeys",
+ "capybaras"
+ ],
+ "y": [20, -14, 2, 18],
+ "type": "bar",
+ "texttemplate": "%{y}",
+ "textfont": {"size": 10},
+ "marker": {"cornerradius": "40%"},
+ "xaxis": "x3",
+ "yaxis": "y3",
+ "name": "Normal"
+ },
+ {
+ "x": [
+ "giraffes",
+ "orangutans",
+ "monkeys",
+ "capybaras"
+ ],
+ "y": [20, -14, 2, 10],
+ "text": ["aaaaa", "aaaaaaaaa", "aaaaaaaaa", "aaaaaaaaaaaa"],
+ "type": "bar",
+ "marker": {"cornerradius": 5000},
+ "xaxis": "x3",
+ "yaxis": "y3",
+ "name": "Normal"
+ },
+ {
+ "y": [
+ "giraffes",
+ "orangutans",
+ "monkeys",
+ "capybaras"
+ ],
+ "x": [20, -4, 1, 18],
+ "base": [1, 2, 1, 2],
+ "type": "bar",
+ "marker": {"cornerradius": "20.5%"},
+ "orientation": "h",
+ "texttemplate": "%{y}: %{x}",
+ "textfont": {"size": 10},
+ "insidetextanchor": "middle",
+ "xaxis": "x4",
+ "yaxis": "y4",
+ "name": "Reversed"
+ }
+ ],
+ "layout": {
+ "width": 900,
+ "height": 450,
+ "grid": {"rows": 2, "columns": 2, "pattern": "independent"},
+ "barmode": "stack",
+ "barcornerradius": "10",
+ "xaxis": {"autorange": "reversed"},
+ "yaxis": {"autorange": "reversed"},
+ "xaxis4": {"autorange": "reversed"},
+ "yaxis4": {"autorange": "reversed"}
+ }
+ }
diff --git a/test/image/mocks/zz-round-bar_attrs_relative.json b/test/image/mocks/zz-round-bar_attrs_relative.json
new file mode 100644
index 00000000000..f152ba0863a
--- /dev/null
+++ b/test/image/mocks/zz-round-bar_attrs_relative.json
@@ -0,0 +1,42 @@
+{
+ "data":[
+ {
+ "width":[1,0.8,0.6,0.4],
+ "text":[1,2,3333333333,4],
+ "textposition":"outside",
+ "y":[1,2,3,4],
+ "x":[1,2,3,4],
+ "type":"bar"
+ }, {
+ "width":[0.4,0.6,0.8,1],
+ "text":["Three",2,"inside text",0],
+ "textposition":"auto",
+ "textfont":{"size":[10]},
+ "y":[3,2,1,0],
+ "x":[1,2,3,4],
+ "type":"bar"
+ }, {
+ "width":1,
+ "text":[-1,-3,-2,-4],
+ "textposition":"inside",
+ "y":[-1,-3,-2,-4],
+ "x":[1,2,3,4],
+ "type":"bar"
+ }, {
+ "text":[0,"outside text",-3,-2],
+ "textposition":"auto",
+ "y":[0,-0.25,-3,-2],
+ "x":[1,2,3,4],
+ "type":"bar"
+ }
+ ],
+ "layout":{
+ "xaxis": {"showgrid":true},
+ "yaxis": {"range":[-6,6]},
+ "height":400,
+ "width":400,
+ "barcornerradius": "50%",
+ "barmode":"relative",
+ "barnorm":""
+ }
+}
diff --git a/test/image/mocks/zz-round-bar_stackto100_negative.json b/test/image/mocks/zz-round-bar_stackto100_negative.json
new file mode 100644
index 00000000000..124b88e6124
--- /dev/null
+++ b/test/image/mocks/zz-round-bar_stackto100_negative.json
@@ -0,0 +1,35 @@
+{
+ "data":[
+ {
+ "name":"Col1",
+ "y":["1","2","3","4","5"],
+ "x":["1","2","3","4","5"],
+ "type":"bar"
+ },
+ {
+ "name":"Col2",
+ "y":["2","3","4","3","2"],
+ "x":["1","2","3","4","5"],
+ "type":"bar"
+ },
+ {
+ "name":"Col3",
+ "y":["5","4","3","2","1"],
+ "x":["1","2","3","4","5"],
+ "type":"bar"
+ },
+ {
+ "name":"Col4",
+ "y":["-1","0","1","0","-1"],
+ "x":["1","2","3","4","5"],
+ "type":"bar"
+ }
+ ],
+ "layout":{
+ "height":400,
+ "width":400,
+ "barcornerradius": "50%",
+ "barmode":"stack",
+ "barnorm":"percent"
+ }
+}
diff --git a/test/image/mocks/zz-round-worldcup.json b/test/image/mocks/zz-round-worldcup.json
new file mode 100644
index 00000000000..5985a4ebf6b
--- /dev/null
+++ b/test/image/mocks/zz-round-worldcup.json
@@ -0,0 +1,66 @@
+{
+ "data": [
+ {
+ "orientation": "h",
+ "type": "bar",
+ "marker": { "color": "lightblue" },
+ "textangle": 0,
+ "textposition": "inside",
+ "textfont": {
+ "color": "white",
+ "size": 32
+ },
+ "text": [
+ "1958 | 1962 | 1970 | 1994 | 2002 ๐ง๐ท",
+ "1954 | 1974 | 1990 | 2014 ๐ฉ๐ช",
+ "1934 | 1938 | 1982 | 2006 ๐ฎ๐น",
+ "1930 | 1950 ๐บ๐พ",
+ "1998 | 2018 ๐ซ๐ท",
+ "1974 | 1986 ๐ฆ๐ท",
+ "1966 ๐ด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ",
+ "2010 ๐ช๐ธ"
+ ],
+ "y": [
+ "Brazil",
+ "Germany",
+ "Italy",
+ "Uruguay",
+ "France",
+ "Argentina",
+ "England",
+ "Spain"
+ ],
+ "x": [5, 4, 4, 2, 2, 2, 1, 1]
+ }
+ ],
+ "layout": {
+ "barcornerradius": "50%",
+ "title": {
+ "text": "FIFA ๐ World Cup ๐ winners",
+ "font": {
+ "size": 40,
+ "color": "gray"
+ }
+ },
+ "width": 600,
+ "height": 600,
+ "margin": {
+ "l": 20,
+ "r": 20
+ },
+ "yaxis": {
+ "ticklabelposition": "inside",
+ "autorange": "reversed"
+ },
+ "xaxis": {
+ "zeroline": false,
+ "range": [-1, 6],
+ "tickvals": [0, 1, 2, 3, 4, 5],
+ "ticktext": ["๐", "๐", "๐", "๐คจ", "๐", "๐"],
+ "tickfont": { "size": 32 },
+ "title": {
+ "text": "
An interactive Plotly graph"
+ }
+ }
+ }
+}
diff --git a/test/image/mocks/zz-rounded-bar_display_height_zero_no_line_width.json b/test/image/mocks/zz-rounded-bar_display_height_zero_no_line_width.json
new file mode 100644
index 00000000000..762a5316b78
--- /dev/null
+++ b/test/image/mocks/zz-rounded-bar_display_height_zero_no_line_width.json
@@ -0,0 +1,322 @@
+{
+ "data": [
+ {
+ "type": "bar",
+ "x": [
+ "A",
+ "b",
+ "c",
+ "d",
+ "E"
+ ],
+ "y": [
+ null,
+ 0.25,
+ 0.5,
+ 0.75,
+ 1
+ ],
+ "text": [
+ null,
+ 0.25,
+ 0.5,
+ 0.75,
+ 1
+ ],
+ "insidetextanchor": "middle",
+ "cliponaxis": false
+ },
+ {
+ "type": "bar",
+ "marker": {
+ "line": {
+ "width": "1e1"
+ }
+ },
+ "x": [
+ "A",
+ "b",
+ "c",
+ "d",
+ "E"
+ ],
+ "y": [
+ 0,
+ 0,
+ 0.5,
+ null,
+ 1
+ ],
+ "text": [
+ 0,
+ 0,
+ 0.5,
+ null,
+ 1
+ ],
+ "insidetextanchor": "middle",
+ "cliponaxis": false
+ },
+ {
+ "type": "bar",
+ "x": [
+ "A",
+ "b",
+ "c",
+ "d",
+ "E"
+ ],
+ "y": [
+ null,
+ 0.25,
+ 0.5,
+ 0.75,
+ 1
+ ],
+ "text": [
+ null,
+ 0.25,
+ 0.5,
+ 0.75,
+ 1
+ ],
+ "insidetextanchor": "middle",
+ "cliponaxis": false,
+ "xaxis": "x2",
+ "yaxis": "y2"
+ },
+ {
+ "type": "bar",
+ "marker": {
+ "line": {
+ "width": "1e1"
+ }
+ },
+ "x": [
+ "A",
+ "b",
+ "c",
+ "d",
+ "E"
+ ],
+ "y": [
+ 0,
+ 0,
+ 0.5,
+ null,
+ 1
+ ],
+ "text": [
+ 0,
+ 0,
+ 0.5,
+ null,
+ 1
+ ],
+ "insidetextanchor": "middle",
+ "cliponaxis": false,
+ "xaxis": "x2",
+ "yaxis": "y2"
+ },
+ {
+ "type": "bar",
+ "orientation": "h",
+ "y": [
+ "A",
+ "b",
+ "c",
+ "d",
+ "E"
+ ],
+ "x": [
+ null,
+ 0.25,
+ 0.5,
+ 0.75,
+ 1
+ ],
+ "text": [
+ null,
+ 0.25,
+ 0.5,
+ 0.75,
+ 1
+ ],
+ "insidetextanchor": "middle",
+ "cliponaxis": false,
+ "xaxis": "x3",
+ "yaxis": "y3"
+ },
+ {
+ "type": "bar",
+ "marker": {
+ "line": {
+ "width": "1e1"
+ }
+ },
+ "orientation": "h",
+ "y": [
+ "A",
+ "b",
+ "c",
+ "d",
+ "E"
+ ],
+ "x": [
+ 0,
+ 0,
+ 0.5,
+ null,
+ 1
+ ],
+ "text": [
+ 0,
+ 0,
+ 0.5,
+ null,
+ 1
+ ],
+ "insidetextanchor": "middle",
+ "cliponaxis": false,
+ "xaxis": "x3",
+ "yaxis": "y3"
+ },
+ {
+ "type": "bar",
+ "orientation": "h",
+ "y": [
+ "A",
+ "b",
+ "c",
+ "d",
+ "E"
+ ],
+ "x": [
+ null,
+ 0.25,
+ 0.5,
+ 0.75,
+ 1
+ ],
+ "text": [
+ null,
+ 0.25,
+ 0.5,
+ 0.75,
+ 1
+ ],
+ "insidetextanchor": "middle",
+ "cliponaxis": false,
+ "xaxis": "x4",
+ "yaxis": "y4"
+ },
+ {
+ "type": "bar",
+ "marker": {
+ "line": {
+ "width": "1e1"
+ }
+ },
+ "orientation": "h",
+ "y": [
+ "A",
+ "b",
+ "c",
+ "d",
+ "E"
+ ],
+ "x": [
+ 0,
+ 0,
+ 0.5,
+ null,
+ 1
+ ],
+ "text": [
+ 0,
+ 0,
+ 0.5,
+ null,
+ 1
+ ],
+ "insidetextanchor": "middle",
+ "cliponaxis": false,
+ "xaxis": "x4",
+ "yaxis": "y4"
+ }
+ ],
+ "layout": {
+ "showlegend": true,
+ "width": 800,
+ "height": 800,
+ "dragmode": "pan",
+ "barmode": "relative",
+ "barcornerradius": "50%",
+ "xaxis": {
+ "zeroline": false,
+ "domain": [
+ 0,
+ 0.48
+ ]
+ },
+ "xaxis2": {
+ "zeroline": false,
+ "autorange": "reversed",
+ "anchor": "y2",
+ "domain": [
+ 0.52,
+ 1
+ ]
+ },
+ "xaxis3": {
+ "zeroline": false,
+ "anchor": "y3",
+ "domain": [
+ 0,
+ 0.48
+ ]
+ },
+ "xaxis4": {
+ "zeroline": false,
+ "autorange": "reversed",
+ "anchor": "y4",
+ "domain": [
+ 0.52,
+ 1
+ ]
+ },
+ "yaxis": {
+ "zeroline": false,
+ "domain": [
+ 0,
+ 0.48
+ ]
+ },
+ "yaxis2": {
+ "zeroline": false,
+ "autorange": "reversed",
+ "anchor": "x2",
+ "domain": [
+ 0.52,
+ 1
+ ]
+ },
+ "yaxis3": {
+ "zeroline": false,
+ "anchor": "x3",
+ "domain": [
+ 0.52,
+ 1
+ ]
+ },
+ "yaxis4": {
+ "zeroline": false,
+ "autorange": "reversed",
+ "anchor": "x4",
+ "domain": [
+ 0,
+ 0.48
+ ]
+ }
+ }
+}
diff --git a/test/plot-schema.json b/test/plot-schema.json
index 9f5b82beb8a..833e3e89946 100644
--- a/test/plot-schema.json
+++ b/test/plot-schema.json
@@ -14113,6 +14113,11 @@
"editType": "none",
"valType": "string"
},
+ "cornerradius": {
+ "description": "Sets the rounding of corners. May be an integer number of pixels, or a percentage of bar width (as a string ending in %). Defaults to `layout.barcornerradius`. In stack or relative barmode, the first trace to set cornerradius is used for the whole stack.",
+ "editType": "calc",
+ "valType": "any"
+ },
"editType": "calc",
"line": {
"autocolorscale": {
@@ -14800,6 +14805,11 @@
"zoomScale"
],
"layoutAttributes": {
+ "barcornerradius": {
+ "description": "Sets the rounding of bar corners. May be an integer number of pixels, or a percentage of bar width (as a string ending in %).",
+ "editType": "calc",
+ "valType": "any"
+ },
"bargap": {
"description": "Sets the gap (in plot fraction) between bars of adjacent location coordinates.",
"editType": "calc",
@@ -31469,6 +31479,11 @@
"editType": "none",
"valType": "string"
},
+ "cornerradius": {
+ "description": "Sets the rounding of corners. May be an integer number of pixels, or a percentage of bar width (as a string ending in %). Defaults to `layout.barcornerradius`. In stack or relative barmode, the first trace to set cornerradius is used for the whole stack.",
+ "editType": "calc",
+ "valType": "any"
+ },
"editType": "calc",
"line": {
"autocolorscale": {
@@ -32074,6 +32089,11 @@
"showLegend"
],
"layoutAttributes": {
+ "barcornerradius": {
+ "description": "Sets the rounding of bar corners. May be an integer number of pixels, or a percentage of bar width (as a string ending in %).",
+ "editType": "calc",
+ "valType": "any"
+ },
"bargap": {
"description": "Sets the gap (in plot fraction) between bars of adjacent location coordinates.",
"editType": "calc",