Layout: support box stacking (#9364)

* Layout: support box stacking

* Add stackWeight and sample

* Cleanup, update docs and types

* Avoid div0

* missing semi
This commit is contained in:
Jukka Kurkela 2021-07-11 13:23:42 +03:00 committed by GitHub
parent 27b91b7458
commit 47d4b04836
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 284 additions and 43 deletions

View File

@ -189,6 +189,7 @@ module.exports = {
'scales/time-line',
'scales/time-max-span',
'scales/time-combo',
'scales/stacked'
]
},
{

View File

@ -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)

View File

@ -0,0 +1,71 @@
# Stacked Linear / Category
```js chart-editor
// <block:setup:1>
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',
}
]
};
// </block:setup>
// <block:config:0>
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
}
}
}
},
};
// </block:config>
module.exports = {
config: config,
};
```

View File

@ -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 placeBoxes(boxes, chartArea, params) {
const userPadding = params.padding;
let x = chartArea.x;
let y = chartArea.y;
let i, ilen, layout, box;
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;
}
for (i = 0, ilen = boxes.length; i < ilen; ++i) {
layout = boxes[i];
box = layout.box;
function placeBoxes(boxes, chartArea, params, stacks) {
const userPadding = params.padding;
let {x, y} = chartArea;
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,

View File

@ -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
}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

12
types/index.esm.d.ts vendored
View File

@ -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'.
*/