mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
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:
parent
27b91b7458
commit
47d4b04836
@ -189,6 +189,7 @@ module.exports = {
|
||||
'scales/time-line',
|
||||
'scales/time-max-span',
|
||||
'scales/time-combo',
|
||||
'scales/stacked'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@ -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)
|
||||
|
||||
71
docs/samples/scales/stacked.md
Normal file
71
docs/samples/scales/stacked.md
Normal 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,
|
||||
};
|
||||
```
|
||||
@ -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,
|
||||
|
||||
106
test/fixtures/core.layouts/stacked-boxes.js
vendored
Normal file
106
test/fixtures/core.layouts/stacked-boxes.js
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
};
|
||||
BIN
test/fixtures/core.layouts/stacked-boxes.png
vendored
Normal file
BIN
test/fixtures/core.layouts/stacked-boxes.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
12
types/index.esm.d.ts
vendored
12
types/index.esm.d.ts
vendored
@ -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'.
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user