From: Evert Timberg Date: Mon, 16 Dec 2019 23:17:42 +0000 (-0500) Subject: Allow axes to be centered on the chart area (#6818) X-Git-Tag: v3.0.0-alpha~194 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=374b7491a32c4c5d7220ef906e65abcdd47bba42;p=thirdparty%2FChart.js.git Allow axes to be centered on the chart area (#6818) Allow axes to be centered on the chart area or at a dynamic position based on another axis --- diff --git a/docs/axes/cartesian/README.md b/docs/axes/cartesian/README.md index 6b9b98253..91bde5954 100644 --- a/docs/axes/cartesian/README.md +++ b/docs/axes/cartesian/README.md @@ -14,13 +14,30 @@ All of the included cartesian axes support a number of common options. | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `type` | `string` | | Type of scale being employed. Custom scales can be created and registered with a string key. This allows changing the type of an axis for a chart. -| `position` | `string` | | Position of the axis in the chart. Possible values are: `'top'`, `'left'`, `'bottom'`, `'right'` +| `position` | `string` | | Position of the axis. [more...](#axis-position) +| `axis` | `string` | | Which type of axis this is. Possible values are: `'x'`, `'y'`. If not set, this is inferred from the first character of the ID which should be `'x'` or `'y'`. | `offset` | `boolean` | `false` | If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. This is set to `true` for a bar chart by default. | `id` | `string` | | The ID is used to link datasets and scale axes together. [more...](#axis-id) | `gridLines` | `object` | | Grid line configuration. [more...](../styling.md#grid-line-configuration) | `scaleLabel` | `object` | | Scale title configuration. [more...](../labelling.md#scale-title-configuration) | `ticks` | `object` | | Tick configuration. [more...](#tick-configuration) +### Axis Position + +An axis can either be positioned at the edge of the chart, at the center of the chart area, or dynamically with respect to a data value. + +To position the axis at the edge of the chart, set the `position` option to one of: `'top'`, `'left'`, `'bottom'`, `'right'`. +To position the axis at the center of the chart area, set the `position` option to `'center'`. In this mode, either the `axis` option is specified or the axis ID starts with the letter 'x' or 'y'. +To position the axis with respect to a data value, set the `position` option to an object such as: + +```javascript +{ + x: -20 +} +``` + +This will position the axis at a value of -20 on the axis with ID "x". For cartesian axes, only 1 axis may be specified. + ### Tick Configuration The following options are common to all cartesian axes but do not apply to other axes. diff --git a/samples/samples.js b/samples/samples.js index 15ece3ff5..d1cae5672 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -148,6 +148,9 @@ }, { title: 'Axes Labels', path: 'scales/axes-labels.html' + }, { + title: 'Center Positioning', + path: 'scales/axis-center-position.html' }] }, { title: 'Legend', diff --git a/samples/scales/axis-center-position.html b/samples/scales/axis-center-position.html new file mode 100644 index 000000000..e3f942ca4 --- /dev/null +++ b/samples/scales/axis-center-position.html @@ -0,0 +1,117 @@ + + + + + Scatter Chart + + + + + + +
+ + + + diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 3ea05a925..320049212 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -147,8 +147,9 @@ function updateConfig(chart) { chart.tooltip.initialize(); } -function positionIsHorizontal(position) { - return position === 'top' || position === 'bottom'; +const KNOWN_POSITIONS = ['top', 'bottom', 'left', 'right', 'chartArea']; +function positionIsHorizontal(position, axis) { + return position === 'top' || position === 'bottom' || (!KNOWN_POSITIONS.includes(position) && axis === 'x'); } function compare2Level(l1, l2) { @@ -341,7 +342,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { var id = scaleOptions.id; var scaleType = valueOrDefault(scaleOptions.type, item.dtype); - if (positionIsHorizontal(scaleOptions.position) !== positionIsHorizontal(item.dposition)) { + if (scaleOptions.position === undefined || positionIsHorizontal(scaleOptions.position, scaleOptions.axis || id[0]) !== positionIsHorizontal(item.dposition)) { scaleOptions.position = item.dposition; } diff --git a/src/core/core.layouts.js b/src/core/core.layouts.js index a166649bd..ac1b3eec9 100644 --- a/src/core/core.layouts.js +++ b/src/core/core.layouts.js @@ -5,10 +5,14 @@ var helpers = require('../helpers/index'); var extend = helpers.extend; +const STATIC_POSITIONS = ['left', 'top', 'right', 'bottom']; + function filterByPosition(array, position) { - return helpers.where(array, function(v) { - return v.pos === position; - }); + return helpers.where(array, v => v.pos === position); +} + +function filterDynamicPositionByAxis(array, axis) { + return helpers.where(array, v => STATIC_POSITIONS.indexOf(v.pos) === -1 && v.box.axis === axis); } function sortByWeight(array, reverse) { @@ -52,18 +56,20 @@ function setLayoutDims(layouts, params) { } function buildLayoutBoxes(boxes) { - var layoutBoxes = wrapBoxes(boxes); - var left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true); - var right = sortByWeight(filterByPosition(layoutBoxes, 'right')); - var top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true); - var bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom')); + const layoutBoxes = wrapBoxes(boxes); + const left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true); + const right = sortByWeight(filterByPosition(layoutBoxes, 'right')); + const top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true); + const bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom')); + const centerHorizontal = filterDynamicPositionByAxis(layoutBoxes, 'x'); + const centerVertical = filterDynamicPositionByAxis(layoutBoxes, 'y'); return { leftAndTop: left.concat(top), - rightAndBottom: right.concat(bottom), + rightAndBottom: right.concat(centerVertical).concat(bottom).concat(centerHorizontal), chartArea: filterByPosition(layoutBoxes, 'chartArea'), - vertical: left.concat(right), - horizontal: top.concat(bottom) + vertical: left.concat(right).concat(centerVertical), + horizontal: top.concat(bottom).concat(centerHorizontal) }; } @@ -375,7 +381,9 @@ module.exports = { left: chartArea.left, top: chartArea.top, right: chartArea.left + chartArea.w, - bottom: chartArea.top + chartArea.h + bottom: chartArea.top + chartArea.h, + height: chartArea.h, + width: chartArea.w, }; // Finally update boxes in chartArea (radial scale for example) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index dfef9f448..6adce4a89 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -14,7 +14,6 @@ const resolve = helpers.options.resolve; defaults._set('scale', { display: true, - position: 'left', offset: false, reverse: false, beginAtZero: false, @@ -669,7 +668,7 @@ class Scale extends Element { var scaleLabelOpts = opts.scaleLabel; var gridLineOpts = opts.gridLines; var display = me._isVisible(); - var isBottom = opts.position === 'bottom'; + var labelsBelowTicks = opts.position !== 'top' && me.axis === 'x'; var isHorizontal = me.isHorizontal(); // Width @@ -717,10 +716,10 @@ class Scale extends Element { // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned // which means that the right padding is dominated by the font height if (isRotated) { - paddingLeft = isBottom ? + paddingLeft = labelsBelowTicks ? cosRotation * firstLabelSize.width + sinRotation * firstLabelSize.offset : sinRotation * (firstLabelSize.height - firstLabelSize.offset); - paddingRight = isBottom ? + paddingRight = labelsBelowTicks ? sinRotation * (lastLabelSize.height - lastLabelSize.offset) : cosRotation * lastLabelSize.width + sinRotation * lastLabelSize.offset; } else { @@ -778,8 +777,8 @@ class Scale extends Element { // Shared Methods isHorizontal() { - var pos = this.options.position; - return pos === 'top' || pos === 'bottom'; + const {axis, position} = this.options; + return position === 'top' || position === 'bottom' || axis === 'x'; } isFullWidth() { return this.options.fullWidth; @@ -965,10 +964,10 @@ class Scale extends Element { */ _computeGridLineItems(chartArea) { var me = this; + const axis = me.axis; var chart = me.chart; var options = me.options; - var gridLines = options.gridLines; - var position = options.position; + const {gridLines, position} = options; var offsetGridLines = gridLines.offsetGridLines; var isHorizontal = me.isHorizontal(); var ticks = me.ticks; @@ -1006,12 +1005,38 @@ class Scale extends Element { tx2 = borderValue - axisHalfWidth; x1 = alignBorderValue(chartArea.left) + axisHalfWidth; x2 = chartArea.right; - } else { + } else if (position === 'right') { borderValue = alignBorderValue(me.left); x1 = chartArea.left; x2 = alignBorderValue(chartArea.right) - axisHalfWidth; tx1 = borderValue + axisHalfWidth; tx2 = me.left + tl; + } else if (axis === 'x') { + if (position === 'center') { + borderValue = alignBorderValue((chartArea.top + chartArea.bottom) / 2); + } else if (helpers.isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + borderValue = alignBorderValue(me.chart.scales[positionAxisID].getPixelForValue(value)); + } + + y1 = chartArea.top; + y2 = chartArea.bottom; + ty1 = borderValue + axisHalfWidth; + ty2 = ty1 + tl; + } else if (axis === 'y') { + if (position === 'center') { + borderValue = alignBorderValue((chartArea.left + chartArea.right) / 2); + } else if (helpers.isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + borderValue = alignBorderValue(me.chart.scales[positionAxisID].getPixelForValue(value)); + } + + tx1 = borderValue - axisHalfWidth; + tx2 = tx1 - tl; + x1 = chartArea.left; + x2 = chartArea.right; } for (i = 0; i < ticksLength; ++i) { @@ -1067,20 +1092,20 @@ class Scale extends Element { /** * @private */ - _computeLabelItems() { - var me = this; - var options = me.options; - var optionTicks = options.ticks; - var position = options.position; - var isMirrored = optionTicks.mirror; - var isHorizontal = me.isHorizontal(); - var ticks = me.ticks; - var fonts = parseTickFontOptions(optionTicks); - var tickPadding = optionTicks.padding; - var tl = getTickMarkLength(options.gridLines); - var rotation = -helpers.toRadians(me.labelRotation); - var items = []; - var i, ilen, tick, label, x, y, textAlign, pixel, font, lineHeight, lineCount, textOffset; + _computeLabelItems(chartArea) { + const me = this; + const axis = me.axis; + const options = me.options; + const {position, ticks: optionTicks} = options; + const isMirrored = optionTicks.mirror; + const isHorizontal = me.isHorizontal(); + const ticks = me.ticks; + const fonts = parseTickFontOptions(optionTicks); + const tickPadding = optionTicks.padding; + const tl = getTickMarkLength(options.gridLines); + const rotation = -helpers.toRadians(me.labelRotation); + const items = []; + let i, ilen, tick, label, x, y, textAlign, pixel, font, lineHeight, lineCount, textOffset; if (position === 'top') { y = me.bottom - tl - tickPadding; @@ -1091,9 +1116,27 @@ class Scale extends Element { } else if (position === 'left') { x = me.right - (isMirrored ? 0 : tl) - tickPadding; textAlign = isMirrored ? 'left' : 'right'; - } else { + } else if (position === 'right') { x = me.left + (isMirrored ? 0 : tl) + tickPadding; textAlign = isMirrored ? 'right' : 'left'; + } else if (axis === 'x') { + if (position === 'center') { + y = ((chartArea.top + chartArea.bottom) / 2) + tl + tickPadding; + } else if (helpers.isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + y = me.chart.scales[positionAxisID].getPixelForValue(value) + tl + tickPadding; + } + textAlign = !rotation ? 'center' : 'right'; + } else if (axis === 'y') { + if (position === 'center') { + x = ((chartArea.left + chartArea.right) / 2) - tl - tickPadding; + } else if (helpers.isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + x = me.chart.scales[positionAxisID].getPixelForValue(value); + } + textAlign = 'right'; } for (i = 0, ilen = ticks.length; i < ilen; ++i) { @@ -1214,7 +1257,7 @@ class Scale extends Element { /** * @private */ - _drawLabels() { + _drawLabels(chartArea) { var me = this; var optionTicks = me.options.ticks; @@ -1223,7 +1266,7 @@ class Scale extends Element { } var ctx = me.ctx; - var items = me._labelItems || (me._labelItems = me._computeLabelItems()); + var items = me._labelItems || (me._labelItems = me._computeLabelItems(chartArea)); var i, j, ilen, jlen, item, tickFont, label, y; for (i = 0, ilen = items.length; i < ilen; ++i) { @@ -1335,7 +1378,7 @@ class Scale extends Element { me._drawGrid(chartArea); me._drawTitle(); - me._drawLabels(); + me._drawLabels(chartArea); } /** diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index 147a42d3e..15d1afc55 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -3,7 +3,6 @@ import Scale from '../core/core.scale'; const defaultConfig = { - position: 'bottom' }; class CategoryScale extends Scale { diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index 2670eb513..a26db1332 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -5,7 +5,6 @@ import LinearScaleBase from './scale.linearbase'; import Ticks from '../core/core.ticks'; const defaultConfig = { - position: 'left', ticks: { callback: Ticks.formatters.linear } diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index a5ae779b8..d48ce5b64 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -56,8 +56,6 @@ function generateTicks(generationOptions, dataRange) { } const defaultConfig = { - position: 'left', - // label settings ticks: { callback: Ticks.formatters.logarithmic diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 4741bcfcb..9e39e6a08 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -485,8 +485,6 @@ function filterBetween(timestamps, min, max) { } const defaultConfig = { - position: 'bottom', - /** * Data distribution along the scale: * - 'linear': data are spread according to their time (distances can vary), diff --git a/test/fixtures/core.scale/x-axis-position-center.json b/test/fixtures/core.scale/x-axis-position-center.json new file mode 100644 index 000000000..94583f9e3 --- /dev/null +++ b/test/fixtures/core.scale/x-axis-position-center.json @@ -0,0 +1,57 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "data": [{ + "x": -20, + "y": -30 + }, { + "x": 0, + "y": 0 + }, { + "x": 20, + "y": 15 + }] + }] + }, + "options": { + "legend": false, + "title": false, + "scales": { + "x": { + "position": "center", + "axis": "x", + "min": -100, + "max": 100, + "gridLines": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": false + } + }, + "y": { + "position": "left", + "axis": "y", + "min": -100, + "max": 100, + "gridLines": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": false + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/core.scale/x-axis-position-center.png b/test/fixtures/core.scale/x-axis-position-center.png new file mode 100644 index 000000000..c9ef8c878 Binary files /dev/null and b/test/fixtures/core.scale/x-axis-position-center.png differ diff --git a/test/fixtures/core.scale/x-axis-position-dynamic.json b/test/fixtures/core.scale/x-axis-position-dynamic.json new file mode 100644 index 000000000..84e80670c --- /dev/null +++ b/test/fixtures/core.scale/x-axis-position-dynamic.json @@ -0,0 +1,59 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "data": [{ + "x": -20, + "y": -30 + }, { + "x": 0, + "y": 0 + }, { + "x": 20, + "y": 15 + }] + }] + }, + "options": { + "legend": false, + "title": false, + "scales": { + "x": { + "position": { + "y": 30 + }, + "axis": "x", + "min": -100, + "max": 100, + "gridLines": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": false + } + }, + "y": { + "position": "left", + "axis": "y", + "min": -100, + "max": 100, + "gridLines": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": false + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/core.scale/x-axis-position-dynamic.png b/test/fixtures/core.scale/x-axis-position-dynamic.png new file mode 100644 index 000000000..0ac0a903c Binary files /dev/null and b/test/fixtures/core.scale/x-axis-position-dynamic.png differ diff --git a/test/fixtures/core.scale/y-axis-position-center.json b/test/fixtures/core.scale/y-axis-position-center.json new file mode 100644 index 000000000..76cc0db3e --- /dev/null +++ b/test/fixtures/core.scale/y-axis-position-center.json @@ -0,0 +1,57 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "data": [{ + "x": -20, + "y": -30 + }, { + "x": 0, + "y": 0 + }, { + "x": 20, + "y": 15 + }] + }] + }, + "options": { + "legend": false, + "title": false, + "scales": { + "x": { + "position": "bottom", + "axis": "x", + "min": -100, + "max": 100, + "gridLines": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": false + } + }, + "y": { + "position": "center", + "axis": "y", + "min": -100, + "max": 100, + "gridLines": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": false + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/core.scale/y-axis-position-center.png b/test/fixtures/core.scale/y-axis-position-center.png new file mode 100644 index 000000000..ce00eada3 Binary files /dev/null and b/test/fixtures/core.scale/y-axis-position-center.png differ diff --git a/test/fixtures/core.scale/y-axis-position-dynamic.json b/test/fixtures/core.scale/y-axis-position-dynamic.json new file mode 100644 index 000000000..c0f9bc304 --- /dev/null +++ b/test/fixtures/core.scale/y-axis-position-dynamic.json @@ -0,0 +1,59 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "data": [{ + "x": -20, + "y": -30 + }, { + "x": 0, + "y": 0 + }, { + "x": 20, + "y": 15 + }] + }] + }, + "options": { + "legend": false, + "title": false, + "scales": { + "x": { + "position": "bottom", + "axis": "x", + "min": -100, + "max": 100, + "gridLines": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": false + } + }, + "y": { + "position": { + "x": -50 + }, + "axis": "y", + "min": -100, + "max": 100, + "gridLines": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": false + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/core.scale/y-axis-position-dynamic.png b/test/fixtures/core.scale/y-axis-position-dynamic.png new file mode 100644 index 000000000..70f4efbf3 Binary files /dev/null and b/test/fixtures/core.scale/y-axis-position-dynamic.png differ diff --git a/test/specs/scale.category.tests.js b/test/specs/scale.category.tests.js index 5485f137e..daf69e7ff 100644 --- a/test/specs/scale.category.tests.js +++ b/test/specs/scale.category.tests.js @@ -34,7 +34,6 @@ describe('Category scale tests', function() { borderDash: [], borderDashOffset: 0.0 }, - position: 'bottom', offset: false, scaleLabel: Chart.defaults.scale.scaleLabel, ticks: { @@ -68,6 +67,7 @@ describe('Category scale tests', function() { }; var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); + config.position = 'bottom'; var Constructor = Chart.scaleService.getScaleConstructor('category'); var scale = new Constructor({ ctx: {}, @@ -95,6 +95,7 @@ describe('Category scale tests', function() { }; var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); + config.position = 'bottom'; var Constructor = Chart.scaleService.getScaleConstructor('category'); var scale = new Constructor({ ctx: {}, diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index 459db671c..90a4f05fd 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -25,7 +25,6 @@ describe('Linear Scale', function() { borderDash: [], borderDashOffset: 0.0 }, - position: 'left', offset: false, reverse: false, beginAtZero: false, diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index 599dfd38e..5c0fa9cdc 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -25,7 +25,6 @@ describe('Logarithmic Scale tests', function() { borderDash: [], borderDashOffset: 0.0 }, - position: 'left', offset: false, reverse: false, beginAtZero: false, diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 09fc2ff02..465a7e0f4 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -72,7 +72,6 @@ describe('Time scale tests', function() { borderDash: [], borderDashOffset: 0.0 }, - position: 'bottom', offset: false, reverse: false, beginAtZero: false,