Skip to content

Commit

Permalink
Layout: support box stacking (#9364)
Browse files Browse the repository at this point in the history
* Layout: support box stacking

* Add stackWeight and sample

* Cleanup, update docs and types

* Avoid div0

* missing semi
  • Loading branch information
kurkle committed Jul 11, 2021
1 parent 27b91b7 commit 47d4b04
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 42 deletions.
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

0 comments on commit 47d4b04

Please sign in to comment.