]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Layout: support box stacking (#9364)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Sun, 11 Jul 2021 10:23:42 +0000 (13:23 +0300)
committerGitHub <noreply@github.com>
Sun, 11 Jul 2021 10:23:42 +0000 (06:23 -0400)
* Layout: support box stacking

* Add stackWeight and sample

* Cleanup, update docs and types

* Avoid div0

* missing semi

docs/.vuepress/config.js
docs/axes/cartesian/_common.md
docs/samples/scales/stacked.md [new file with mode: 0644]
src/core/core.layouts.js
test/fixtures/core.layouts/stacked-boxes.js [new file with mode: 0644]
test/fixtures/core.layouts/stacked-boxes.png [new file with mode: 0644]
types/index.esm.d.ts

index 3a635348fe7a2bd7a0b5a5628a4772e62d95ba60..f3867aa5653ccc369c2a6b769dfd507607ddb5c8 100644 (file)
@@ -189,6 +189,7 @@ module.exports = {
             'scales/time-line',
             'scales/time-max-span',
             'scales/time-combo',
+            'scales/stacked'
           ]
         },
         {
index 96e93a1c9e2191322219bf74f266d80263be042b..a9082b55fd5f905aa817578bc5e1e14ddf3709b6 100644 (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)
diff --git a/docs/samples/scales/stacked.md b/docs/samples/scales/stacked.md
new file mode 100644 (file)
index 0000000..f6081ab
--- /dev/null
@@ -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,
+};
+```
index 7206db0d99057767567a0d75a76e1ac79f9c32e6..d5c1e20b3aea03b99b236fcb217658714afb7aa8 100644 (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 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 (file)
index 0000000..e4fb345
--- /dev/null
@@ -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 (file)
index 0000000..e92cea3
Binary files /dev/null and b/test/fixtures/core.layouts/stacked-boxes.png differ
index 17c9c02821a6a0fe6314cda8390fe3b436ebbfa6..8f95af2529dab31b7bc48fd71cbcf5ee4efc2bc4 100644 (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'.
    */