diff --git a/src/components/shapes/calc_autorange.js b/src/components/shapes/calc_autorange.js index 7892bceb12f..3311e3de05f 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,39 @@ 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(isVerticalAxis) { + var isYSizeModePixel = shape.ysizemode === 'pixel'; + v0 = isYSizeModePixel ? shape.yanchor : shape.y0; + v1 = isYSizeModePixel ? shape.yanchor : shape.y1; + } else { + var isXSizeModePixel = shape.xsizemode === 'pixel'; + v0 = isXSizeModePixel ? shape.xanchor : shape.x0; + v1 = isXSizeModePixel ? shape.xanchor : 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/baselines/zzz_shape_shift_horizontal.png b/test/image/baselines/zzz_shape_shift_horizontal.png new file mode 100644 index 00000000000..9ba14f4b578 Binary files /dev/null and b/test/image/baselines/zzz_shape_shift_horizontal.png differ 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 00000000000..3b3b2ef38f1 Binary files /dev/null and b/test/image/baselines/zzz_shape_shift_vertical.png differ 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",