From: Jukka Kurkela Date: Sun, 11 Jul 2021 10:23:42 +0000 (+0300) Subject: Layout: support box stacking (#9364) X-Git-Tag: v3.5.0~25 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=47d4b04836d4defc2878650cee03ea9a66829b4c;p=thirdparty%2FChart.js.git Layout: support box stacking (#9364) * Layout: support box stacking * Add stackWeight and sample * Cleanup, update docs and types * Avoid div0 * missing semi --- diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 3a635348f..f3867aa56 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -189,6 +189,7 @@ module.exports = { 'scales/time-line', 'scales/time-max-span', 'scales/time-combo', + 'scales/stacked' ] }, { diff --git a/docs/axes/cartesian/_common.md b/docs/axes/cartesian/_common.md index 96e93a1c9..a9082b55f 100644 --- a/docs/axes/cartesian/_common.md +++ b/docs/axes/cartesian/_common.md @@ -6,6 +6,8 @@ Namespace: `options.scales[scaleId]` | ---- | ---- | ------- | ----------- | `bounds` | `string` | `'ticks'` | Determines the scale bounds. [more...](./index.md#scale-bounds) | `position` | `string` | | Position of the axis. [more...](./index.md#axis-position) +| `stack` | `string` | | Stack group. Axes at the same `position` with same `stack` are stacked. +| `stackWeight` | `number` | 1 | Weight of the scale in stack group. Used to determine the amount of allocated space for the scale within the group. | `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. | `title` | `object` | | Scale title configuration. [more...](../labelling.md#scale-title-configuration) diff --git a/docs/samples/scales/stacked.md b/docs/samples/scales/stacked.md new file mode 100644 index 000000000..f6081ab6c --- /dev/null +++ b/docs/samples/scales/stacked.md @@ -0,0 +1,71 @@ +# Stacked Linear / Category + +```js chart-editor +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: [10, 30, 50, 20, 25, 44, -10], + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.CHART_COLORS.red, + }, + { + label: 'Dataset 2', + data: ['ON', 'ON', 'OFF', 'ON', 'OFF', 'OFF', 'ON'], + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.CHART_COLORS.blue, + stepped: true, + yAxisID: 'y2', + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Stacked scales', + }, + }, + scales: { + y: { + type: 'linear', + position: 'left', + stack: 'demo', + stackWeight: 2, + grid: { + borderColor: Utils.CHART_COLORS.red + } + }, + y2: { + type: 'category', + labels: ['ON', 'OFF'], + offset: true, + position: 'left', + stack: 'demo', + stackWeight: 1, + grid: { + borderColor: Utils.CHART_COLORS.blue + } + } + } + }, +}; +// + +module.exports = { + config: config, +}; +``` diff --git a/src/core/core.layouts.js b/src/core/core.layouts.js index 7206db0d9..d5c1e20b3 100644 --- a/src/core/core.layouts.js +++ b/src/core/core.layouts.js @@ -1,5 +1,5 @@ import defaults from './core.defaults'; -import {each, isObject} from '../helpers/helpers.core'; +import {defined, each, isObject} from '../helpers/helpers.core'; import {toPadding} from '../helpers/helpers.options'; /** @@ -28,34 +28,59 @@ function sortByWeight(array, reverse) { function wrapBoxes(boxes) { const layoutBoxes = []; - let i, ilen, box; + let i, ilen, box, pos, stack, stackWeight; for (i = 0, ilen = (boxes || []).length; i < ilen; ++i) { box = boxes[i]; + ({position: pos, options: {stack, stackWeight = 1}} = box); layoutBoxes.push({ index: i, box, - pos: box.position, + pos, horizontal: box.isHorizontal(), - weight: box.weight + weight: box.weight, + stack: stack && (pos + stack), + stackWeight }); } return layoutBoxes; } +function buildStacks(layouts) { + const stacks = {}; + for (const wrap of layouts) { + const {stack, pos, stackWeight} = wrap; + if (!stack || !STATIC_POSITIONS.includes(pos)) { + continue; + } + const _stack = stacks[stack] || (stacks[stack] = {count: 0, placed: 0, weight: 0, size: 0}); + _stack.count++; + _stack.weight += stackWeight; + } + return stacks; +} + +/** + * store dimensions used instead of available chartArea in fitBoxes + **/ function setLayoutDims(layouts, params) { + const stacks = buildStacks(layouts); + const {vBoxMaxWidth, hBoxMaxHeight} = params; let i, ilen, layout; for (i = 0, ilen = layouts.length; i < ilen; ++i) { layout = layouts[i]; - // store dimensions used instead of available chartArea in fitBoxes + const {fullSize} = layout.box; + const stack = stacks[layout.stack]; + const factor = stack && layout.stackWeight / stack.weight; if (layout.horizontal) { - layout.width = layout.box.fullSize && params.availableWidth; - layout.height = params.hBoxMaxHeight; + layout.width = factor ? factor * vBoxMaxWidth : fullSize && params.availableWidth; + layout.height = hBoxMaxHeight; } else { - layout.width = params.vBoxMaxWidth; - layout.height = layout.box.fullSize && params.availableHeight; + layout.width = vBoxMaxWidth; + layout.height = factor ? factor * hBoxMaxHeight : fullSize && params.availableHeight; } } + return stacks; } function buildLayoutBoxes(boxes) { @@ -89,18 +114,20 @@ function updateMaxPadding(maxPadding, boxPadding) { maxPadding.right = Math.max(maxPadding.right, boxPadding.right); } -function updateDims(chartArea, params, layout) { - const box = layout.box; +function updateDims(chartArea, params, layout, stacks) { + const {pos, box} = layout; const maxPadding = chartArea.maxPadding; // dynamically placed boxes size is not considered - if (!isObject(layout.pos)) { + if (!isObject(pos)) { if (layout.size) { // this layout was already counted for, lets first reduce old size - chartArea[layout.pos] -= layout.size; + chartArea[pos] -= layout.size; } - layout.size = layout.horizontal ? box.height : box.width; - chartArea[layout.pos] += layout.size; + const stack = stacks[layout.stack] || {size: 0, count: 1}; + stack.size = Math.max(stack.size, layout.horizontal ? box.height : box.width); + layout.size = stack.size / stack.count; + chartArea[pos] += layout.size; } if (box.getPadding) { @@ -150,7 +177,7 @@ function getMargins(horizontal, chartArea) { : marginForPositions(['top', 'bottom']); } -function fitBoxes(boxes, chartArea, params) { +function fitBoxes(boxes, chartArea, params, stacks) { const refitBoxes = []; let i, ilen, layout, box, refit, changed; @@ -163,7 +190,7 @@ function fitBoxes(boxes, chartArea, params) { layout.height || chartArea.h, getMargins(layout.horizontal, chartArea) ); - const {same, other} = updateDims(chartArea, params, layout); + const {same, other} = updateDims(chartArea, params, layout, stacks); // Dimensions changed and there were non full width boxes before this // -> we have to refit those @@ -177,31 +204,53 @@ function fitBoxes(boxes, chartArea, params) { } } - return refit && fitBoxes(refitBoxes, chartArea, params) || changed; + return refit && fitBoxes(refitBoxes, chartArea, params, stacks) || changed; +} + +function setBoxDims(box, left, top, width, height) { + box.top = top; + box.left = left; + box.right = left + width; + box.bottom = top + height; + box.width = width; + box.height = height; } -function placeBoxes(boxes, chartArea, params) { +function placeBoxes(boxes, chartArea, params, stacks) { const userPadding = params.padding; - let x = chartArea.x; - let y = chartArea.y; - let i, ilen, layout, box; + let {x, y} = chartArea; - for (i = 0, ilen = boxes.length; i < ilen; ++i) { - layout = boxes[i]; - box = layout.box; + for (const layout of boxes) { + const box = layout.box; + const stack = stacks[layout.stack] || {count: 1, placed: 0, weight: 1}; + const weight = (stack.weight * layout.stackWeight) || 1; if (layout.horizontal) { - box.left = box.fullSize ? userPadding.left : chartArea.left; - box.right = box.fullSize ? params.outerWidth - userPadding.right : chartArea.left + chartArea.w; - box.top = y; - box.bottom = y + box.height; - box.width = box.right - box.left; + const width = chartArea.w / weight; + const height = stack.size || box.height; + if (defined(stack.start)) { + y = stack.start; + } + if (box.fullSize) { + setBoxDims(box, userPadding.left, y, params.outerWidth - userPadding.right - userPadding.left, height); + } else { + setBoxDims(box, chartArea.left + stack.placed, y, width, height); + } + stack.start = y; + stack.placed += width; y = box.bottom; } else { - box.left = x; - box.right = x + box.width; - box.top = box.fullSize ? userPadding.top : chartArea.top; - box.bottom = box.fullSize ? params.outerHeight - userPadding.bottom : chartArea.top + chartArea.h; - box.height = box.bottom - box.top; + const height = chartArea.h / weight; + const width = stack.size || box.width; + if (defined(stack.start)) { + x = stack.start; + } + if (box.fullSize) { + setBoxDims(box, x, userPadding.top, width, params.outerHeight - userPadding.bottom - userPadding.top); + } else { + setBoxDims(box, x, chartArea.top + stack.placed, width, height); + } + stack.start = x; + stack.placed += height; x = box.right; } } @@ -372,30 +421,30 @@ export default { y: padding.top }, padding); - setLayoutDims(verticalBoxes.concat(horizontalBoxes), params); + const stacks = setLayoutDims(verticalBoxes.concat(horizontalBoxes), params); // First fit the fullSize boxes, to reduce probability of re-fitting. - fitBoxes(boxes.fullSize, chartArea, params); + fitBoxes(boxes.fullSize, chartArea, params, stacks); // Then fit vertical boxes - fitBoxes(verticalBoxes, chartArea, params); + fitBoxes(verticalBoxes, chartArea, params, stacks); // Then fit horizontal boxes - if (fitBoxes(horizontalBoxes, chartArea, params)) { + if (fitBoxes(horizontalBoxes, chartArea, params, stacks)) { // if the area changed, re-fit vertical boxes - fitBoxes(verticalBoxes, chartArea, params); + fitBoxes(verticalBoxes, chartArea, params, stacks); } handleMaxPadding(chartArea); // Finally place the boxes to correct coordinates - placeBoxes(boxes.leftAndTop, chartArea, params); + placeBoxes(boxes.leftAndTop, chartArea, params, stacks); // Move to opposite side of chart chartArea.x += chartArea.w; chartArea.y += chartArea.h; - placeBoxes(boxes.rightAndBottom, chartArea, params); + placeBoxes(boxes.rightAndBottom, chartArea, params, stacks); chart.chartArea = { left: chartArea.left, diff --git a/test/fixtures/core.layouts/stacked-boxes.js b/test/fixtures/core.layouts/stacked-boxes.js new file mode 100644 index 000000000..e4fb345c5 --- /dev/null +++ b/test/fixtures/core.layouts/stacked-boxes.js @@ -0,0 +1,106 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], borderColor: 'red'}, + {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], yAxisID: 'y1', xAxisID: 'x1', borderColor: 'green'}, + {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], yAxisID: 'y2', xAxisID: 'x2', borderColor: 'blue'}, + ], + labels: ['tick1', 'tick2', 'tick3'] + }, + options: { + plugins: false, + scales: { + x: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + bounds: 'data', + grid: { + borderColor: 'red' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + x1: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + bounds: 'data', + grid: { + borderColor: 'green' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + x2: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + bounds: 'data', + grid: { + borderColor: 'blue' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + y: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + grid: { + borderColor: 'red' + }, + ticks: { + precision: 0 + } + }, + y1: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + grid: { + borderColor: 'green' + }, + ticks: { + precision: 0 + } + }, + y2: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + grid: { + borderColor: 'blue' + }, + ticks: { + precision: 0 + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 384, + width: 384 + } + } +}; diff --git a/test/fixtures/core.layouts/stacked-boxes.png b/test/fixtures/core.layouts/stacked-boxes.png new file mode 100644 index 000000000..e92cea312 Binary files /dev/null and b/test/fixtures/core.layouts/stacked-boxes.png differ diff --git a/types/index.esm.d.ts b/types/index.esm.d.ts index 17c9c0282..8f95af252 100644 --- a/types/index.esm.d.ts +++ b/types/index.esm.d.ts @@ -2829,6 +2829,18 @@ export interface CartesianScaleOptions extends CoreScaleOptions { * Position of the axis. */ position: 'left' | 'top' | 'right' | 'bottom' | 'center' | { [scale: string]: number }; + + /** + * Stack group. Axes at the same `position` with same `stack` are stacked. + */ + stack?: string; + + /** + * Weight of the scale in stack group. Used to determine the amount of allocated space for the scale within the group. + * @default 1 + */ + stackWeight?: number; + /** * 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'. */