From 508f7257a4218909773e5bc5a31792212e0ebe7a Mon Sep 17 00:00:00 2001 From: My-Tien Nguyen Date: Mon, 3 Jun 2024 15:59:28 +0200 Subject: [PATCH 1/3] categoryshapeshiftstart and categoryshapeshiftend properties for category axes --- src/components/shapes/calc_autorange.js | 40 ++++-- src/components/shapes/display_labels.js | 22 ++- src/components/shapes/draw.js | 22 ++- src/components/shapes/helpers.js | 42 ++++-- src/plots/cartesian/axis_defaults.js | 5 + src/plots/cartesian/layout_attributes.js | 22 +++ .../mocks/zzz_shape_shift_horizontal.json | 130 ++++++++++++++++++ .../image/mocks/zzz_shape_shift_vertical.json | 126 +++++++++++++++++ test/jasmine/tests/shapes_test.js | 36 +++-- test/plot-schema.json | 44 ++++++ 10 files changed, 443 insertions(+), 46 deletions(-) create mode 100644 test/image/mocks/zzz_shape_shift_horizontal.json create mode 100644 test/image/mocks/zzz_shape_shift_vertical.json diff --git a/src/components/shapes/calc_autorange.js b/src/components/shapes/calc_autorange.js index 7892bceb12f..cfe351ee245 100644 --- a/src/components/shapes/calc_autorange.js +++ b/src/components/shapes/calc_autorange.js @@ -23,22 +23,18 @@ module.exports = function calcAutorange(gd) { // paper and axis domain referenced shapes don't affect autorange if(shape.xref !== 'paper' && xRefType !== 'domain') { - var vx0 = shape.xsizemode === 'pixel' ? shape.xanchor : shape.x0; - var vx1 = shape.xsizemode === 'pixel' ? shape.xanchor : shape.x1; ax = Axes.getFromId(gd, shape.xref); - bounds = shapeBounds(ax, vx0, vx1, shape.path, constants.paramIsX); + bounds = shapeBounds(ax, shape, constants.paramIsX, false); if(bounds) { shape._extremes[ax._id] = Axes.findExtremes(ax, bounds, calcXPaddingOptions(shape)); } } if(shape.yref !== 'paper' && yRefType !== 'domain') { - var vy0 = shape.ysizemode === 'pixel' ? shape.yanchor : shape.y0; - var vy1 = shape.ysizemode === 'pixel' ? shape.yanchor : shape.y1; ax = Axes.getFromId(gd, shape.yref); - bounds = shapeBounds(ax, vy0, vy1, shape.path, constants.paramIsY); + bounds = shapeBounds(ax, shape, constants.paramIsY, true); if(bounds) { shape._extremes[ax._id] = Axes.findExtremes(ax, bounds, calcYPaddingOptions(shape)); } @@ -77,15 +73,37 @@ function calcPaddingOptions(lineWidth, sizeMode, v0, v1, path, isYAxis) { } } -function shapeBounds(ax, v0, v1, path, paramsToUse) { - var convertVal = (ax.type === 'category' || ax.type === 'multicategory') ? ax.r2c : ax.d2c; +function shapeBounds(ax, shape, paramsToUse, isVerticalAxis) { + var v0; + var v1; + var shiftStart = 0; + var shiftEnd = 0; + if(shape.xsizemode === 'pixel') { + v0 = isVerticalAxis ? shape.yanchor : shape.xanchor; + v1 = isVerticalAxis ? shape.yanchor : shape.xanchor; + } else { + v0 = isVerticalAxis ? shape.y0 : shape.x0; + v1 = isVerticalAxis ? shape.y1 : shape.x1; + } + + var convertVal; + + if(ax.type === 'category' || ax.type === 'multicategory') { + convertVal = ax.r2c; + if(shape.xsizemode === 'scale') { + shiftStart = ax.categoryshapeshiftstart; + shiftEnd = ax.categoryshapeshiftend; + } + } else { + convertVal = ax.d2c; + } - if(v0 !== undefined) return [convertVal(v0), convertVal(v1)]; - if(!path) return; + if(v0 !== undefined) return [convertVal(v0) + shiftStart, convertVal(v1) + shiftEnd]; + if(!shape.path) return; var min = Infinity; var max = -Infinity; - var segments = path.match(constants.segmentRE); + var segments = shape.path.match(constants.segmentRE); var i; var segment; var drawnParam; diff --git a/src/components/shapes/display_labels.js b/src/components/shapes/display_labels.js index 82c0856d943..bc0182eb208 100644 --- a/src/components/shapes/display_labels.js +++ b/src/components/shapes/display_labels.js @@ -88,15 +88,25 @@ module.exports = function drawLabel(gd, index, options, shapeGroup) { // and convert them to pixel coordinates // Setup conversion functions var xa = Axes.getFromId(gd, options.xref); + var xShiftStart = xa ? xa.categoryshapeshiftstart : 0; + var xShiftEnd = xa ? xa.categoryshapeshiftend : 0; var xRefType = Axes.getRefType(options.xref); var ya = Axes.getFromId(gd, options.yref); + var yShiftStart = ya ? ya.categoryshapeshiftstart : 0; + var yShiftEnd = ya ? ya.categoryshapeshiftend : 0; var yRefType = Axes.getRefType(options.yref); - var x2p = helpers.getDataToPixel(gd, xa, false, xRefType); - var y2p = helpers.getDataToPixel(gd, ya, true, yRefType); - shapex0 = x2p(options.x0); - shapex1 = x2p(options.x1); - shapey0 = y2p(options.y0); - shapey1 = y2p(options.y1); + var x2p = function(v, shift) { + var dataToPixel = helpers.getDataToPixel(gd, xa, shift, false, xRefType); + return dataToPixel(v); + }; + var y2p = function(v, shift) { + var dataToPixel = helpers.getDataToPixel(gd, ya, shift, true, yRefType); + return dataToPixel(v); + }; + shapex0 = x2p(options.x0, xShiftStart); + shapex1 = x2p(options.x1, xShiftEnd); + shapey0 = y2p(options.y0, yShiftStart); + shapey1 = y2p(options.y1, yShiftEnd); } // Handle `auto` angle diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 417c884c3f2..30eec725b1d 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -227,8 +227,18 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer, editHe var xRefType = Axes.getRefType(shapeOptions.xref); var ya = Axes.getFromId(gd, shapeOptions.yref); var yRefType = Axes.getRefType(shapeOptions.yref); - var x2p = helpers.getDataToPixel(gd, xa, false, xRefType); - var y2p = helpers.getDataToPixel(gd, ya, true, yRefType); + var shiftXStart = xa ? xa.categoryshapeshiftstart : 0; + var shiftXEnd = xa ? xa.categoryshapeshiftend : 0; + var shiftYStart = ya ? ya.categoryshapeshiftstart : 0; + var shiftYEnd = ya ? ya.categoryshapeshiftend : 0; + var x2p = function(v, shift) { + var dataToPixel = helpers.getDataToPixel(gd, xa, shift, false, xRefType); + return dataToPixel(v); + }; + var y2p = function(v, shift) { + var dataToPixel = helpers.getDataToPixel(gd, ya, shift, true, yRefType); + return dataToPixel(v); + }; var p2x = helpers.getPixelToData(gd, xa, false, xRefType); var p2y = helpers.getPixelToData(gd, ya, true, yRefType); @@ -279,8 +289,8 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer, editHe g.append('circle') .attr({ 'data-line-point': 'start-point', - cx: xPixelSized ? x2p(shapeOptions.xanchor) + shapeOptions.x0 : x2p(shapeOptions.x0), - cy: yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y0 : y2p(shapeOptions.y0), + cx: xPixelSized ? x2p(shapeOptions.xanchor) + shapeOptions.x0 : x2p(shapeOptions.x0, shiftXStart), + cy: yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y0 : y2p(shapeOptions.y0, shiftYStart), r: circleRadius }) .style(circleStyle) @@ -289,8 +299,8 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer, editHe g.append('circle') .attr({ 'data-line-point': 'end-point', - cx: xPixelSized ? x2p(shapeOptions.xanchor) + shapeOptions.x1 : x2p(shapeOptions.x1), - cy: yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y1 : y2p(shapeOptions.y1), + cx: xPixelSized ? x2p(shapeOptions.xanchor) + shapeOptions.x1 : x2p(shapeOptions.x1, shiftXEnd), + cy: yPixelSized ? y2p(shapeOptions.yanchor) - shapeOptions.y1 : y2p(shapeOptions.y1, shiftYEnd), r: circleRadius }) .style(circleStyle) diff --git a/src/components/shapes/helpers.js b/src/components/shapes/helpers.js index d07fe6948eb..fe1a54776e5 100644 --- a/src/components/shapes/helpers.js +++ b/src/components/shapes/helpers.js @@ -53,7 +53,7 @@ exports.extractPathCoords = function(path, paramsToUse, isRaw) { return extractedCoordinates; }; -exports.getDataToPixel = function(gd, axis, isVertical, refType) { +exports.getDataToPixel = function(gd, axis, shift, isVertical, refType) { var gs = gd._fullLayout._size; var dataToPixel; @@ -66,7 +66,15 @@ exports.getDataToPixel = function(gd, axis, isVertical, refType) { var d2r = exports.shapePositionToRange(axis); dataToPixel = function(v) { - return axis._offset + axis.r2p(d2r(v, true)); + var shiftPixels = 0; + if(axis.type === 'category' || axis.type === 'multicategory') { + if(isVertical) { + shiftPixels = ((axis.r2p(1) - axis.r2p(0)) * shift); + } else { + shiftPixels = axis.r2p(0.5) * shift; + } + } + return axis._offset + axis.r2p(d2r(v, true)) + shiftPixels; }; if(axis.type === 'date') dataToPixel = exports.decodeDate(dataToPixel); @@ -179,6 +187,10 @@ exports.getPathString = function(gd, options) { var ya = Axes.getFromId(gd, options.yref); var gs = gd._fullLayout._size; var x2r, x2p, y2r, y2p; + var xShiftStart = 0; + var xShiftEnd = 0; + var yShiftStart = 0; + var yShiftEnd = 0; var x0, x1, y0, y1; if(xa) { @@ -187,6 +199,11 @@ exports.getPathString = function(gd, options) { } else { x2r = exports.shapePositionToRange(xa); x2p = function(v) { return xa._offset + xa.r2p(x2r(v, true)); }; + if(xa.type === 'category' || xa.type === 'multicategory') { + var shiftUnitX = xa.r2p(0.5); + xShiftStart = shiftUnitX * xa.categoryshapeshiftstart; + xShiftEnd = shiftUnitX * xa.categoryshapeshiftend; + } } } else { x2p = function(v) { return gs.l + gs.w * v; }; @@ -198,6 +215,11 @@ exports.getPathString = function(gd, options) { } else { y2r = exports.shapePositionToRange(ya); y2p = function(v) { return ya._offset + ya.r2p(y2r(v, true)); }; + if(ya.type === 'category' || ya.type === 'multicategory') { + var shiftUnitY = ya.r2p(0) - ya.r2p(1); + yShiftStart = shiftUnitY * ya.categoryshapeshiftstart; + yShiftEnd = shiftUnitY * ya.categoryshapeshiftend; + } } } else { y2p = function(v) { return gs.t + gs.h * (1 - v); }; @@ -211,20 +233,20 @@ exports.getPathString = function(gd, options) { if(options.xsizemode === 'pixel') { var xAnchorPos = x2p(options.xanchor); - x0 = xAnchorPos + options.x0; - x1 = xAnchorPos + options.x1; + x0 = xAnchorPos + options.x0 + xShiftStart; + x1 = xAnchorPos + options.x1 + xShiftEnd; } else { - x0 = x2p(options.x0); - x1 = x2p(options.x1); + x0 = x2p(options.x0) + xShiftStart; + x1 = x2p(options.x1) + xShiftEnd; } if(options.ysizemode === 'pixel') { var yAnchorPos = y2p(options.yanchor); - y0 = yAnchorPos - options.y0; - y1 = yAnchorPos - options.y1; + y0 = yAnchorPos - options.y0 - yShiftStart; + y1 = yAnchorPos - options.y1 - yShiftEnd; } else { - y0 = y2p(options.y0); - y1 = y2p(options.y1); + y0 = y2p(options.y0) - yShiftStart; + y1 = y2p(options.y1) - yShiftEnd; } if(type === 'line') return 'M' + x0 + ',' + y0 + 'L' + x1 + ',' + y1; diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 0b03fecf376..58d3fe32e50 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -96,6 +96,11 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, handleCategoryOrderDefaults(containerIn, containerOut, coerce, options); + if(axType === 'category' || axType === 'multicategory') { + containerOut.categoryshapeshiftstart = containerIn.categoryshapeshiftstart || 0; + containerOut.categoryshapeshiftend = containerIn.categoryshapeshiftend || 0; + } + if(axType !== 'category' && !options.noHover) coerce('hoverformat'); var dfltColor = coerce('color'); diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 2532e0c75cd..aad813dc31f 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -1190,6 +1190,28 @@ module.exports = { 'Used with `categoryorder`.' ].join(' ') }, + categoryshapeshiftstart: { + valType: 'number', + dflt: 0, + min: -0.5, + max: 0.5, + editType: 'calc', + description: [ + 'Only relevant if axis is a (multi-)category axes. Shifts x0/y0 by a fraction of the', + 'reference unit.' + ] + }, + categoryshapeshiftend: { + valType: 'number', + dflt: 0, + min: -0.5, + max: 0.5, + editType: 'calc', + description: [ + 'Only relevant if axis is a (multi-)category axes. Shifts x1/y1 by a fraction of the', + 'reference unit.' + ] + }, uirevision: { valType: 'any', editType: 'none', diff --git a/test/image/mocks/zzz_shape_shift_horizontal.json b/test/image/mocks/zzz_shape_shift_horizontal.json new file mode 100644 index 00000000000..99dd8e0b92d --- /dev/null +++ b/test/image/mocks/zzz_shape_shift_horizontal.json @@ -0,0 +1,130 @@ +{ + "data": [ + { + "y": [ + ["A", "A", "B", "B"], ["C", "D", "C", "D"] + ], + "x": [ + 1, 2, 3, 4 + ], + "type": "bar", + "marker": { + "color": "rgba(153, 217, 234, 1)" + }, + "orientation": "h" + }, + { + "y": [ + ["A", "A", "B", "B"], ["C", "D", "C", "D"] + ], + "x": [ + 4, 3, 2, 1 + ], + "type": "bar", + "orientation": "h" + }, + { + "y": ["A", "B", "C", "D"], + "x": [ + 1, 2, 3, 4 + ], + "type": "bar", + "marker": { + "color": "rgba(153, 217, 234, 1)" + }, + "yaxis": "y2", + "xaxis": "x2", + "orientation": "h" + }, + { + "y": [ + ["A", "A", "B", "B"], ["C", "D", "C", "D"] + ], + "x": [ + 4, 3, 2, 1 + ], + "type": "bar", + "marker": { + "color": "rgba(214, 120, 120, 1)" + }, + "yaxis": "y3", + "xaxis": "x3", + "orientation": "h" + } + ], + "layout": { + "grid": { + "rows": 2, + "columns": 2, + "xgap": 0.1, + "ygap": 0.1, + "pattern": "independent" + }, + "shapes": [ + { + "layer": "above", + "type": "rect", + "label": { + "text": "around A,D", + "textposition": "bottom center", + "textangle": 0 + }, + "line": { + "color": "black", + "width": 3.0 + }, + "y0": ["A", "D"], + "y1": ["A", "D"], + "x0": 0, + "x1": 1, + "xref": "x domain" + }, + { + "layer": "above", + "type": "line", + "label": { + "text": "on B", + "textposition": "middle", + "textangle": 0 + }, + "line": { + "color": "blue", + "width": 3.0 + }, + "y0": "B", + "y1": "B", + "yref": "y2", + "x0": 0, + "x1": 0.25, + "xref": "x2 domain" + }, + { + "layer": "above", + "type": "line", + "label": { + "text": "Before B,D", + "textposition": "middle", + "textangle": 0 + }, + "line": { + "color": "green", + "width": 3.0 + }, + "y0": ["B", "D"], + "y1": ["B", "D"], + "yref": "y3", + "x0": 0, + "x1": 0.25, + "xref": "x3 domain" + } + ], + "yaxis": { + "categoryshapeshiftstart": -0.5, + "categoryshapeshiftend": 0.5 + }, + "yaxis3": { + "categoryshapeshiftstart": -0.5, + "categoryshapeshiftend": -0.5 + } + } +} diff --git a/test/image/mocks/zzz_shape_shift_vertical.json b/test/image/mocks/zzz_shape_shift_vertical.json new file mode 100644 index 00000000000..2b6f6e0c8eb --- /dev/null +++ b/test/image/mocks/zzz_shape_shift_vertical.json @@ -0,0 +1,126 @@ +{ + "data": [ + { + "x": [ + ["A", "A", "B", "B"], ["C", "D", "C", "D"] + ], + "y": [ + 1, 2, 3, 4 + ], + "type": "bar", + "marker": { + "color": "rgba(153, 217, 234, 1)" + } + }, + { + "x": [ + ["A", "A", "B", "B"], ["C", "D", "C", "D"] + ], + "y": [ + 4, 3, 2, 1 + ], + "type": "bar" + }, + { + "x": ["A", "B", "C", "D"], + "y": [ + 1, 2, 3, 4 + ], + "type": "bar", + "marker": { + "color": "rgba(153, 217, 234, 1)" + }, + "xaxis": "x2", + "yaxis": "y2" + }, + { + "x": [ + ["A", "A", "B", "B"], ["C", "D", "C", "D"] + ], + "y": [ + 4, 3, 2, 1 + ], + "type": "bar", + "marker": { + "color": "rgba(214, 120, 120, 1)" + }, + "xaxis": "x3", + "yaxis": "y3" + } + ], + "layout": { + "grid": { + "rows": 2, + "columns": 2, + "xgap": 0.1, + "ygap": 0.3, + "pattern": "independent" + }, + "shapes": [ + { + "layer": "above", + "type": "rect", + "label": { + "text": "around A,D", + "textposition": "top center", + "textangle": 0 + }, + "line": { + "color": "black", + "width": 3.0 + }, + "x0": ["A", "D"], + "x1": ["A", "D"], + "y0": 0, + "y1": 1, + "yref": "y domain" + }, + { + "layer": "above", + "type": "line", + "label": { + "text": "on B", + "textposition": "end", + "textangle": 0 + }, + "line": { + "color": "blue", + "width": 3.0 + }, + "x0": "B", + "x1": "B", + "xref": "x2", + "y0": 0, + "y1": 0.25, + "yref": "y2 domain" + }, + { + "layer": "above", + "type": "line", + "label": { + "text": "Before B,D", + "textposition": "end", + "textangle": 0 + }, + "line": { + "color": "green", + "width": 3.0 + }, + "x0": ["B", "D"], + "x1": ["B", "D"], + "xref": "x3", + "y0": 0, + "y1": 0.5, + "yref": "y3 domain" + } + ], + "xaxis": { + "categoryshapeshiftstart": -0.5, + "categoryshapeshiftend": 0.5 + }, + "xaxis3": { + "categoryshapeshiftstart": -0.5, + "categoryshapeshiftend": -0.5 + } + } +} diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index 112add2bee7..30c8f147759 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -1487,13 +1487,19 @@ describe('Test shapes', function() { function testShapeDrag(dx, dy, layoutShape, node) { var xa = Axes.getFromId(gd, layoutShape.xref); var ya = Axes.getFromId(gd, layoutShape.yref); - var x2p = helpers.getDataToPixel(gd, xa); - var y2p = helpers.getDataToPixel(gd, ya, true); + var x2p = function(v, shift) { + var dataToPixel = helpers.getDataToPixel(gd, xa, shift); + return dataToPixel(v); + }; + var y2p = function(v, shift) { + var dataToPixel = helpers.getDataToPixel(gd, ya, shift, true); + return dataToPixel(v); + }; - var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); + var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p, xa, ya); return drag({node: node, dpos: [dx, dy]}).then(function() { - var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); + var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p, xa, ya); expect(finalCoordinates.x0 - initialCoordinates.x0).toBeCloseTo(dx); expect(finalCoordinates.x1 - initialCoordinates.x1).toBeCloseTo(dx); @@ -1502,12 +1508,16 @@ describe('Test shapes', function() { }); } - function getShapeCoordinates(layoutShape, x2p, y2p) { + function getShapeCoordinates(layoutShape, x2p, y2p, xa, ya) { + var shiftXStart = xa ? xa.categoryshapeshiftstart : 0; + var shiftXEnd = xa ? xa.categoryshapeshiftend : 0; + var shiftYStart = ya ? ya.categoryshapeshiftstart : 0; + var shiftYEnd = ya ? ya.categoryshapeshiftend : 0; return { - x0: x2p(layoutShape.x0), - x1: x2p(layoutShape.x1), - y0: y2p(layoutShape.y0), - y1: y2p(layoutShape.y1) + x0: x2p(layoutShape.x0, shiftXStart), + x1: x2p(layoutShape.x1, shiftXEnd), + y0: y2p(layoutShape.y0, shiftYStart), + y1: y2p(layoutShape.y1, shiftYEnd) }; } @@ -1549,10 +1559,10 @@ describe('Test shapes', function() { var x2p = helpers.getDataToPixel(gd, xa); var y2p = helpers.getDataToPixel(gd, ya, true); - var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); + var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p, xa, ya); return drag({node: node, dpos: [dx, dy], edge: direction}).then(function() { - var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); + var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p, xa, ya); var keyN, keyS, keyW, keyE; if(initialCoordinates.y0 < initialCoordinates.y1) { @@ -1599,9 +1609,9 @@ describe('Test shapes', function() { getResizeLineOverStartPointElement() : getResizeLineOverEndPointElement(); - var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); + var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p, xa, ya); return drag({node: dragHandle, dpos: [10, 10]}).then(function() { - var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); + var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p, xa, ya); if(pointToMove === 'start') { expect(finalCoordinates.x0 - initialCoordinates.x0).toBeCloseTo(10); diff --git a/test/plot-schema.json b/test/plot-schema.json index fd2b1659e37..280bdcaeb5d 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -14034,6 +14034,28 @@ "median descending" ] }, + "categoryshapeshiftend": { + "description": [ + "Only relevant if axis is a (multi-)category axes. Shifts x1/y1 by a fraction of the", + "reference unit." + ], + "dflt": 0, + "editType": "calc", + "max": 0.5, + "min": -0.5, + "valType": "number" + }, + "categoryshapeshiftstart": { + "description": [ + "Only relevant if axis is a (multi-)category axes. Shifts x0/y0 by a fraction of the", + "reference unit." + ], + "dflt": 0, + "editType": "calc", + "max": 0.5, + "min": -0.5, + "valType": "number" + }, "color": { "description": "Sets default for all colors associated with this axis all at once: line, font, tick, and grid colors. Grid color is lightened by blending this with the plot background Individual pieces can override this.", "dflt": "#444", @@ -15660,6 +15682,28 @@ "median descending" ] }, + "categoryshapeshiftend": { + "description": [ + "Only relevant if axis is a (multi-)category axes. Shifts x1/y1 by a fraction of the", + "reference unit." + ], + "dflt": 0, + "editType": "calc", + "max": 0.5, + "min": -0.5, + "valType": "number" + }, + "categoryshapeshiftstart": { + "description": [ + "Only relevant if axis is a (multi-)category axes. Shifts x0/y0 by a fraction of the", + "reference unit." + ], + "dflt": 0, + "editType": "calc", + "max": 0.5, + "min": -0.5, + "valType": "number" + }, "color": { "description": "Sets default for all colors associated with this axis all at once: line, font, tick, and grid colors. Grid color is lightened by blending this with the plot background Individual pieces can override this.", "dflt": "#444", From 241f31c6055938867547e07f4e1b39ebf8e6be60 Mon Sep 17 00:00:00 2001 From: My-Tien Nguyen Date: Mon, 3 Jun 2024 17:43:05 +0200 Subject: [PATCH 2/3] Fix shapeBounds calculation read xsizemode for horizontal axes and ysizemode for vertical axes. --- src/components/shapes/calc_autorange.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/shapes/calc_autorange.js b/src/components/shapes/calc_autorange.js index cfe351ee245..3311e3de05f 100644 --- a/src/components/shapes/calc_autorange.js +++ b/src/components/shapes/calc_autorange.js @@ -78,12 +78,14 @@ function shapeBounds(ax, shape, paramsToUse, isVerticalAxis) { var v1; var shiftStart = 0; var shiftEnd = 0; - if(shape.xsizemode === 'pixel') { - v0 = isVerticalAxis ? shape.yanchor : shape.xanchor; - v1 = isVerticalAxis ? shape.yanchor : shape.xanchor; + if(isVerticalAxis) { + var isYSizeModePixel = shape.ysizemode === 'pixel'; + v0 = isYSizeModePixel ? shape.yanchor : shape.y0; + v1 = isYSizeModePixel ? shape.yanchor : shape.y1; } else { - v0 = isVerticalAxis ? shape.y0 : shape.x0; - v1 = isVerticalAxis ? shape.y1 : shape.x1; + var isXSizeModePixel = shape.xsizemode === 'pixel'; + v0 = isXSizeModePixel ? shape.xanchor : shape.x0; + v1 = isXSizeModePixel ? shape.xanchor : shape.x1; } var convertVal; From b087493e96050e05867084958e32c0ffe320baba Mon Sep 17 00:00:00 2001 From: My-Tien Nguyen Date: Mon, 3 Jun 2024 17:43:40 +0200 Subject: [PATCH 3/3] Baseline images for shape_shift_horizontal and shape_shift_vertical --- .../baselines/zzz_shape_shift_horizontal.png | Bin 0 -> 25931 bytes .../baselines/zzz_shape_shift_vertical.png | Bin 0 -> 26179 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/image/baselines/zzz_shape_shift_horizontal.png create mode 100644 test/image/baselines/zzz_shape_shift_vertical.png diff --git a/test/image/baselines/zzz_shape_shift_horizontal.png b/test/image/baselines/zzz_shape_shift_horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..9ba14f4b578495bf6fb8ab854d87e321eb9376e2 GIT binary patch literal 25931 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 HKR);;mzd<= literal 0 HcmV?d00001 diff --git a/test/image/baselines/zzz_shape_shift_vertical.png b/test/image/baselines/zzz_shape_shift_vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..3b3b2ef38f14a0fce155e36f2f6578066fcabbcd GIT binary patch literal 26179 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 literal 0 HcmV?d00001