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",