Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Layout: support box stacking #9364

Merged
merged 5 commits into from Jul 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Expand Up @@ -189,6 +189,7 @@ module.exports = {
'scales/time-line',
'scales/time-max-span',
'scales/time-combo',
'scales/stacked'
]
},
{
Expand Down
2 changes: 2 additions & 0 deletions docs/axes/cartesian/_common.md
Expand Up @@ -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 changes: 71 additions & 0 deletions docs/samples/scales/stacked.md
@@ -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,
};
```
133 changes: 91 additions & 42 deletions 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';

/**
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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,
Expand Down