@cdx-ui/components 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/coverage.svg +1 -1
- package/docs/components/bar-chart/content.md +703 -0
- package/docs/components/content.md +9 -1
- package/docs/components/line-chart/content.md +567 -0
- package/docs/components/pie-chart/content.md +493 -0
- package/docs/components/progress-segmented/content.md +1 -1
- package/docs/components/stepper/content.md +599 -0
- package/docs/components/system-alert/content.md +533 -0
- package/docs/foundations/styles/tokens/all-tokens/content.md +2 -1
- package/docs/llms.txt +9 -0
- package/docs/patterns/content.md +1 -0
- package/docs/patterns/dashboard/content.md +423 -19
- package/docs/patterns/insights/content.md +590 -0
- package/docs/resources/release-notes/0.3.5/content.md +7 -0
- package/docs/resources/release-notes/content.md +1 -0
- package/lib/commonjs/components/BarChart/geometry.js +1130 -0
- package/lib/commonjs/components/BarChart/geometry.js.map +1 -0
- package/lib/commonjs/components/BarChart/index.js +942 -0
- package/lib/commonjs/components/BarChart/index.js.map +1 -0
- package/lib/commonjs/components/BarChart/styles.js +26 -0
- package/lib/commonjs/components/BarChart/styles.js.map +1 -0
- package/lib/commonjs/components/Button/index.js +2 -2
- package/lib/commonjs/components/Button/index.js.map +1 -1
- package/lib/commonjs/components/LineChart/geometry.js +698 -0
- package/lib/commonjs/components/LineChart/geometry.js.map +1 -0
- package/lib/commonjs/components/LineChart/index.js +747 -0
- package/lib/commonjs/components/LineChart/index.js.map +1 -0
- package/lib/commonjs/components/LineChart/styles.js +25 -0
- package/lib/commonjs/components/LineChart/styles.js.map +1 -0
- package/lib/commonjs/components/PieChart/geometry.js +216 -0
- package/lib/commonjs/components/PieChart/geometry.js.map +1 -0
- package/lib/commonjs/components/PieChart/index.js +476 -0
- package/lib/commonjs/components/PieChart/index.js.map +1 -0
- package/lib/commonjs/components/PieChart/styles.js +88 -0
- package/lib/commonjs/components/PieChart/styles.js.map +1 -0
- package/lib/commonjs/components/ProgressCircle/ProgressCircleRing.js +9 -31
- package/lib/commonjs/components/ProgressCircle/ProgressCircleRing.js.map +1 -1
- package/lib/commonjs/components/ProgressCircle/ProgressRing.js +52 -0
- package/lib/commonjs/components/ProgressCircle/ProgressRing.js.map +1 -0
- package/lib/commonjs/components/ScrollShadow/useScrollShadow.js +8 -7
- package/lib/commonjs/components/ScrollShadow/useScrollShadow.js.map +1 -1
- package/lib/commonjs/components/Stepper/index.js +369 -0
- package/lib/commonjs/components/Stepper/index.js.map +1 -0
- package/lib/commonjs/components/Stepper/styles.js +211 -0
- package/lib/commonjs/components/Stepper/styles.js.map +1 -0
- package/lib/commonjs/components/Stepper/useStepperLabelFit.js +22 -0
- package/lib/commonjs/components/Stepper/useStepperLabelFit.js.map +1 -0
- package/lib/commonjs/components/Stepper/useStepperLabelFit.web.js +52 -0
- package/lib/commonjs/components/Stepper/useStepperLabelFit.web.js.map +1 -0
- package/lib/commonjs/components/Stepper/useStepperRingColors.js +31 -0
- package/lib/commonjs/components/Stepper/useStepperRingColors.js.map +1 -0
- package/lib/commonjs/components/SystemAlert/animation.js +218 -0
- package/lib/commonjs/components/SystemAlert/animation.js.map +1 -0
- package/lib/commonjs/components/SystemAlert/index.js +525 -0
- package/lib/commonjs/components/SystemAlert/index.js.map +1 -0
- package/lib/commonjs/components/SystemAlert/styles.js +178 -0
- package/lib/commonjs/components/SystemAlert/styles.js.map +1 -0
- package/lib/commonjs/components/SystemAlert/useSystemAlertStatusBarStyle.js +53 -0
- package/lib/commonjs/components/SystemAlert/useSystemAlertStatusBarStyle.js.map +1 -0
- package/lib/commonjs/components/Tabs/index.js +4 -0
- package/lib/commonjs/components/Tabs/index.js.map +1 -1
- package/lib/commonjs/components/Tabs/styles.js +1 -1
- package/lib/commonjs/components/Tabs/styles.js.map +1 -1
- package/lib/commonjs/components/chart/ChartInspector.js +610 -0
- package/lib/commonjs/components/chart/ChartInspector.js.map +1 -0
- package/lib/commonjs/components/chart/format.js +93 -0
- package/lib/commonjs/components/chart/format.js.map +1 -0
- package/lib/commonjs/components/chart/index.js +2 -0
- package/lib/commonjs/components/chart/index.js.map +1 -0
- package/lib/commonjs/components/chart/motion.js +97 -0
- package/lib/commonjs/components/chart/motion.js.map +1 -0
- package/lib/commonjs/components/chart/palette.js +178 -0
- package/lib/commonjs/components/chart/palette.js.map +1 -0
- package/lib/commonjs/components/chart/size.js +34 -0
- package/lib/commonjs/components/chart/size.js.map +1 -0
- package/lib/commonjs/components/chart/ticks.js +59 -0
- package/lib/commonjs/components/chart/ticks.js.map +1 -0
- package/lib/commonjs/components/chart/types.js +6 -0
- package/lib/commonjs/components/chart/types.js.map +1 -0
- package/lib/commonjs/components/chart/useChartHostWidth.js +27 -0
- package/lib/commonjs/components/chart/useChartHostWidth.js.map +1 -0
- package/lib/commonjs/components/index.js +60 -0
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/commonjs/figma/SystemAlert.Button.figma.js +28 -0
- package/lib/commonjs/figma/SystemAlert.Button.figma.js.map +1 -0
- package/lib/commonjs/figma/SystemAlert.Close.figma.js +22 -0
- package/lib/commonjs/figma/SystemAlert.Close.figma.js.map +1 -0
- package/lib/commonjs/figma/SystemAlert.Description.figma.js +20 -0
- package/lib/commonjs/figma/SystemAlert.Description.figma.js.map +1 -0
- package/lib/commonjs/figma/SystemAlert.IconButton.figma.js +31 -0
- package/lib/commonjs/figma/SystemAlert.IconButton.figma.js.map +1 -0
- package/lib/commonjs/figma/SystemAlert.figma.js +48 -0
- package/lib/commonjs/figma/SystemAlert.figma.js.map +1 -0
- package/lib/module/components/BarChart/geometry.js +1110 -0
- package/lib/module/components/BarChart/geometry.js.map +1 -0
- package/lib/module/components/BarChart/index.js +936 -0
- package/lib/module/components/BarChart/index.js.map +1 -0
- package/lib/module/components/BarChart/styles.js +22 -0
- package/lib/module/components/BarChart/styles.js.map +1 -0
- package/lib/module/components/Button/index.js +2 -2
- package/lib/module/components/Button/index.js.map +1 -1
- package/lib/module/components/LineChart/geometry.js +685 -0
- package/lib/module/components/LineChart/geometry.js.map +1 -0
- package/lib/module/components/LineChart/index.js +741 -0
- package/lib/module/components/LineChart/index.js.map +1 -0
- package/lib/module/components/LineChart/styles.js +21 -0
- package/lib/module/components/LineChart/styles.js.map +1 -0
- package/lib/module/components/PieChart/geometry.js +209 -0
- package/lib/module/components/PieChart/geometry.js.map +1 -0
- package/lib/module/components/PieChart/index.js +471 -0
- package/lib/module/components/PieChart/index.js.map +1 -0
- package/lib/module/components/PieChart/styles.js +84 -0
- package/lib/module/components/PieChart/styles.js.map +1 -0
- package/lib/module/components/ProgressCircle/ProgressCircleRing.js +10 -32
- package/lib/module/components/ProgressCircle/ProgressCircleRing.js.map +1 -1
- package/lib/module/components/ProgressCircle/ProgressRing.js +48 -0
- package/lib/module/components/ProgressCircle/ProgressRing.js.map +1 -0
- package/lib/module/components/ScrollShadow/useScrollShadow.js +8 -7
- package/lib/module/components/ScrollShadow/useScrollShadow.js.map +1 -1
- package/lib/module/components/Stepper/index.js +360 -0
- package/lib/module/components/Stepper/index.js.map +1 -0
- package/lib/module/components/Stepper/styles.js +206 -0
- package/lib/module/components/Stepper/styles.js.map +1 -0
- package/lib/module/components/Stepper/useStepperLabelFit.js +18 -0
- package/lib/module/components/Stepper/useStepperLabelFit.js.map +1 -0
- package/lib/module/components/Stepper/useStepperLabelFit.web.js +48 -0
- package/lib/module/components/Stepper/useStepperLabelFit.web.js.map +1 -0
- package/lib/module/components/Stepper/useStepperRingColors.js +28 -0
- package/lib/module/components/Stepper/useStepperRingColors.js.map +1 -0
- package/lib/module/components/SystemAlert/animation.js +213 -0
- package/lib/module/components/SystemAlert/animation.js.map +1 -0
- package/lib/module/components/SystemAlert/index.js +515 -0
- package/lib/module/components/SystemAlert/index.js.map +1 -0
- package/lib/module/components/SystemAlert/styles.js +174 -0
- package/lib/module/components/SystemAlert/styles.js.map +1 -0
- package/lib/module/components/SystemAlert/useSystemAlertStatusBarStyle.js +49 -0
- package/lib/module/components/SystemAlert/useSystemAlertStatusBarStyle.js.map +1 -0
- package/lib/module/components/Tabs/index.js +4 -0
- package/lib/module/components/Tabs/index.js.map +1 -1
- package/lib/module/components/Tabs/styles.js +1 -1
- package/lib/module/components/Tabs/styles.js.map +1 -1
- package/lib/module/components/chart/ChartInspector.js +600 -0
- package/lib/module/components/chart/ChartInspector.js.map +1 -0
- package/lib/module/components/chart/format.js +84 -0
- package/lib/module/components/chart/format.js.map +1 -0
- package/lib/module/components/chart/index.js +2 -0
- package/lib/module/components/chart/index.js.map +1 -0
- package/lib/module/components/chart/motion.js +89 -0
- package/lib/module/components/chart/motion.js.map +1 -0
- package/lib/module/components/chart/palette.js +168 -0
- package/lib/module/components/chart/palette.js.map +1 -0
- package/lib/module/components/chart/size.js +29 -0
- package/lib/module/components/chart/size.js.map +1 -0
- package/lib/module/components/chart/ticks.js +55 -0
- package/lib/module/components/chart/ticks.js.map +1 -0
- package/lib/module/components/chart/types.js +4 -0
- package/lib/module/components/chart/types.js.map +1 -0
- package/lib/module/components/chart/useChartHostWidth.js +23 -0
- package/lib/module/components/chart/useChartHostWidth.js.map +1 -0
- package/lib/module/components/index.js +5 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/figma/SystemAlert.Button.figma.js +22 -0
- package/lib/module/figma/SystemAlert.Button.figma.js.map +1 -0
- package/lib/module/figma/SystemAlert.Close.figma.js +18 -0
- package/lib/module/figma/SystemAlert.Close.figma.js.map +1 -0
- package/lib/module/figma/SystemAlert.Description.figma.js +14 -0
- package/lib/module/figma/SystemAlert.Description.figma.js.map +1 -0
- package/lib/module/figma/SystemAlert.IconButton.figma.js +25 -0
- package/lib/module/figma/SystemAlert.IconButton.figma.js.map +1 -0
- package/lib/module/figma/SystemAlert.figma.js +42 -0
- package/lib/module/figma/SystemAlert.figma.js.map +1 -0
- package/lib/typescript/components/BarChart/geometry.d.ts +236 -0
- package/lib/typescript/components/BarChart/geometry.d.ts.map +1 -0
- package/lib/typescript/components/BarChart/index.d.ts +193 -0
- package/lib/typescript/components/BarChart/index.d.ts.map +1 -0
- package/lib/typescript/components/BarChart/styles.d.ts +32 -0
- package/lib/typescript/components/BarChart/styles.d.ts.map +1 -0
- package/lib/typescript/components/LineChart/geometry.d.ts +248 -0
- package/lib/typescript/components/LineChart/geometry.d.ts.map +1 -0
- package/lib/typescript/components/LineChart/index.d.ts +105 -0
- package/lib/typescript/components/LineChart/index.d.ts.map +1 -0
- package/lib/typescript/components/LineChart/styles.d.ts +12 -0
- package/lib/typescript/components/LineChart/styles.d.ts.map +1 -0
- package/lib/typescript/components/PieChart/geometry.d.ts +43 -0
- package/lib/typescript/components/PieChart/geometry.d.ts.map +1 -0
- package/lib/typescript/components/PieChart/index.d.ts +147 -0
- package/lib/typescript/components/PieChart/index.d.ts.map +1 -0
- package/lib/typescript/components/PieChart/styles.d.ts +27 -0
- package/lib/typescript/components/PieChart/styles.d.ts.map +1 -0
- package/lib/typescript/components/ProgressCircle/ProgressCircleRing.d.ts.map +1 -1
- package/lib/typescript/components/ProgressCircle/ProgressRing.d.ts +17 -0
- package/lib/typescript/components/ProgressCircle/ProgressRing.d.ts.map +1 -0
- package/lib/typescript/components/ScrollShadow/useScrollShadow.d.ts.map +1 -1
- package/lib/typescript/components/Stepper/index.d.ts +68 -0
- package/lib/typescript/components/Stepper/index.d.ts.map +1 -0
- package/lib/typescript/components/Stepper/styles.d.ts +46 -0
- package/lib/typescript/components/Stepper/styles.d.ts.map +1 -0
- package/lib/typescript/components/Stepper/useStepperLabelFit.d.ts +15 -0
- package/lib/typescript/components/Stepper/useStepperLabelFit.d.ts.map +1 -0
- package/lib/typescript/components/Stepper/useStepperLabelFit.web.d.ts +10 -0
- package/lib/typescript/components/Stepper/useStepperLabelFit.web.d.ts.map +1 -0
- package/lib/typescript/components/Stepper/useStepperRingColors.d.ts +6 -0
- package/lib/typescript/components/Stepper/useStepperRingColors.d.ts.map +1 -0
- package/lib/typescript/components/SystemAlert/animation.d.ts +56 -0
- package/lib/typescript/components/SystemAlert/animation.d.ts.map +1 -0
- package/lib/typescript/components/SystemAlert/index.d.ts +110 -0
- package/lib/typescript/components/SystemAlert/index.d.ts.map +1 -0
- package/lib/typescript/components/SystemAlert/styles.d.ts +38 -0
- package/lib/typescript/components/SystemAlert/styles.d.ts.map +1 -0
- package/lib/typescript/components/SystemAlert/useSystemAlertStatusBarStyle.d.ts +14 -0
- package/lib/typescript/components/SystemAlert/useSystemAlertStatusBarStyle.d.ts.map +1 -0
- package/lib/typescript/components/Tabs/index.d.ts.map +1 -1
- package/lib/typescript/components/chart/ChartInspector.d.ts +101 -0
- package/lib/typescript/components/chart/ChartInspector.d.ts.map +1 -0
- package/lib/typescript/components/chart/format.d.ts +16 -0
- package/lib/typescript/components/chart/format.d.ts.map +1 -0
- package/lib/typescript/components/chart/index.d.ts +3 -0
- package/lib/typescript/components/chart/index.d.ts.map +1 -0
- package/lib/typescript/components/chart/motion.d.ts +27 -0
- package/lib/typescript/components/chart/motion.d.ts.map +1 -0
- package/lib/typescript/components/chart/palette.d.ts +44 -0
- package/lib/typescript/components/chart/palette.d.ts.map +1 -0
- package/lib/typescript/components/chart/size.d.ts +25 -0
- package/lib/typescript/components/chart/size.d.ts.map +1 -0
- package/lib/typescript/components/chart/ticks.d.ts +3 -0
- package/lib/typescript/components/chart/ticks.d.ts.map +1 -0
- package/lib/typescript/components/chart/types.d.ts +163 -0
- package/lib/typescript/components/chart/types.d.ts.map +1 -0
- package/lib/typescript/components/chart/useChartHostWidth.d.ts +9 -0
- package/lib/typescript/components/chart/useChartHostWidth.d.ts.map +1 -0
- package/lib/typescript/components/index.d.ts +5 -0
- package/lib/typescript/components/index.d.ts.map +1 -1
- package/lib/typescript/figma/SystemAlert.Button.figma.d.ts +11 -0
- package/lib/typescript/figma/SystemAlert.Button.figma.d.ts.map +1 -0
- package/lib/typescript/figma/SystemAlert.Close.figma.d.ts +11 -0
- package/lib/typescript/figma/SystemAlert.Close.figma.d.ts.map +1 -0
- package/lib/typescript/figma/SystemAlert.Description.figma.d.ts +8 -0
- package/lib/typescript/figma/SystemAlert.Description.figma.d.ts.map +1 -0
- package/lib/typescript/figma/SystemAlert.IconButton.figma.d.ts +11 -0
- package/lib/typescript/figma/SystemAlert.IconButton.figma.d.ts.map +1 -0
- package/lib/typescript/figma/SystemAlert.figma.d.ts +8 -0
- package/lib/typescript/figma/SystemAlert.figma.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/components/BarChart/geometry.ts +1469 -0
- package/src/components/BarChart/index.tsx +1424 -0
- package/src/components/BarChart/styles.ts +31 -0
- package/src/components/Button/index.tsx +1 -1
- package/src/components/LineChart/geometry.ts +957 -0
- package/src/components/LineChart/index.tsx +1109 -0
- package/src/components/LineChart/styles.ts +26 -0
- package/src/components/PieChart/geometry.ts +295 -0
- package/src/components/PieChart/index.tsx +715 -0
- package/src/components/PieChart/styles.ts +101 -0
- package/src/components/ProgressCircle/ProgressCircleRing.tsx +10 -39
- package/src/components/ProgressCircle/ProgressRing.tsx +60 -0
- package/src/components/ScrollShadow/useScrollShadow.ts +12 -11
- package/src/components/Stepper/index.tsx +452 -0
- package/src/components/Stepper/styles.ts +234 -0
- package/src/components/Stepper/useStepperLabelFit.ts +21 -0
- package/src/components/Stepper/useStepperLabelFit.web.ts +53 -0
- package/src/components/Stepper/useStepperRingColors.ts +29 -0
- package/src/components/SystemAlert/animation.ts +252 -0
- package/src/components/SystemAlert/index.tsx +684 -0
- package/src/components/SystemAlert/styles.ts +212 -0
- package/src/components/SystemAlert/useSystemAlertStatusBarStyle.ts +57 -0
- package/src/components/Tabs/index.tsx +4 -0
- package/src/components/Tabs/styles.ts +1 -1
- package/src/components/chart/ChartInspector.tsx +726 -0
- package/src/components/chart/format.ts +95 -0
- package/src/components/chart/index.ts +24 -0
- package/src/components/chart/motion.ts +98 -0
- package/src/components/chart/palette.tsx +247 -0
- package/src/components/chart/size.ts +21 -0
- package/src/components/chart/ticks.ts +53 -0
- package/src/components/chart/types.ts +183 -0
- package/src/components/chart/useChartHostWidth.ts +26 -0
- package/src/components/index.ts +5 -0
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { niceTicks } from '../chart/ticks';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where category names sit around the plot. `top` / `bottom` apply to vertical
|
|
7
|
+
* bars; `left` / `right` apply to horizontal bars. Other combinations fall back
|
|
8
|
+
* to `bottom` or `left`. `none` draws no category names.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* How a stack group piles its series. `diverging` stacks positive values up and negative values
|
|
13
|
+
* down from zero. `none` adds every value to one running total. `expand` scales each category to
|
|
14
|
+
* the same length, so segments show their share.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** How the chart draws its series: side by side or stacked, a comparison overlay, or a waterfall. */
|
|
18
|
+
|
|
19
|
+
/** A series resolved for layout. `id` is the public series id. */
|
|
20
|
+
|
|
21
|
+
/** Share of each category slot left empty around the bars. MUI X `categoryGapRatio`. */
|
|
22
|
+
export const CATEGORY_GAP_RATIO = 0.36;
|
|
23
|
+
/** Gap between bars in one category, as a share of one bar's thickness. MUI X `barGapRatio`. */
|
|
24
|
+
export const BAR_GAP_RATIO = 0.5;
|
|
25
|
+
/** Primary bar share of the overlay slot. Comparison series are thinner and peek out. */
|
|
26
|
+
export const OVERLAY_PRIMARY_RATIO = 0.7;
|
|
27
|
+
export const OVERLAY_COMPARE_RATIO = 0.16;
|
|
28
|
+
export const OVERLAY_SPACING_RATIO = 0.08;
|
|
29
|
+
/** Pixel gap between stacked segments. `0` joins them with only the outer cap rounded. */
|
|
30
|
+
export const DEFAULT_STACK_GAP = 3;
|
|
31
|
+
export const DEFAULT_TICK_COUNT = 4;
|
|
32
|
+
/** Minimum pixels per category when scrolling. Keeps labels and bars readable. */
|
|
33
|
+
export const MIN_CATEGORY_BAND = 48;
|
|
34
|
+
/** `expand` ticks when the value axis sets no `min`, `max`, or `tickNumber`. */
|
|
35
|
+
const EXPAND_TICKS = [0, 25, 50, 75, 100];
|
|
36
|
+
function clamp(value, min, max) {
|
|
37
|
+
return Math.min(max, Math.max(min, value));
|
|
38
|
+
}
|
|
39
|
+
function round2(value) {
|
|
40
|
+
const rounded = Math.round(value * 100) / 100;
|
|
41
|
+
return Object.is(rounded, -0) ? 0 : rounded;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Reads a finite number from `row[key]`. Anything else is a missing value. */
|
|
45
|
+
export function readBarValue(row, key) {
|
|
46
|
+
const raw = row[key];
|
|
47
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return raw;
|
|
51
|
+
}
|
|
52
|
+
function readBarFlag(row, key) {
|
|
53
|
+
return key !== undefined && row[key] === true;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* One slot per stack group, in first-appearance order. A series without `stack` gets its own
|
|
57
|
+
* slot. The first `stackOffset` set in a group applies to the whole group.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveBarGroups(series) {
|
|
60
|
+
const groups = [];
|
|
61
|
+
const byStack = new Map();
|
|
62
|
+
series.forEach((item, seriesIndex) => {
|
|
63
|
+
if (item.stack === undefined) {
|
|
64
|
+
groups.push({
|
|
65
|
+
stackId: undefined,
|
|
66
|
+
offset: null,
|
|
67
|
+
members: [{
|
|
68
|
+
item,
|
|
69
|
+
seriesIndex
|
|
70
|
+
}]
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
let group = byStack.get(item.stack);
|
|
75
|
+
if (group === undefined) {
|
|
76
|
+
group = {
|
|
77
|
+
stackId: item.stack,
|
|
78
|
+
offset: null,
|
|
79
|
+
members: []
|
|
80
|
+
};
|
|
81
|
+
byStack.set(item.stack, group);
|
|
82
|
+
groups.push(group);
|
|
83
|
+
}
|
|
84
|
+
group.members.push({
|
|
85
|
+
item,
|
|
86
|
+
seriesIndex
|
|
87
|
+
});
|
|
88
|
+
if (group.offset === null && item.stackOffset !== undefined) {
|
|
89
|
+
group.offset = item.stackOffset;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return groups.map(group => group.stackId === undefined ? group : {
|
|
93
|
+
...group,
|
|
94
|
+
offset: group.offset ?? 'diverging'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function groupDomainValues(dataset, group) {
|
|
98
|
+
if (group.offset === 'expand') {
|
|
99
|
+
return [0, 100];
|
|
100
|
+
}
|
|
101
|
+
const values = [];
|
|
102
|
+
for (const row of dataset) {
|
|
103
|
+
if (group.offset === null) {
|
|
104
|
+
for (const {
|
|
105
|
+
item
|
|
106
|
+
} of group.members) {
|
|
107
|
+
const value = readBarValue(row, item.dataKey);
|
|
108
|
+
if (value !== null) {
|
|
109
|
+
values.push(value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
let positive = 0;
|
|
115
|
+
let negative = 0;
|
|
116
|
+
let running = 0;
|
|
117
|
+
for (const {
|
|
118
|
+
item
|
|
119
|
+
} of group.members) {
|
|
120
|
+
const value = readBarValue(row, item.dataKey);
|
|
121
|
+
if (value === null) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (group.offset === 'none') {
|
|
125
|
+
running += value;
|
|
126
|
+
values.push(running);
|
|
127
|
+
} else if (value >= 0) {
|
|
128
|
+
positive += value;
|
|
129
|
+
} else {
|
|
130
|
+
negative += value;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (group.offset === 'diverging') {
|
|
134
|
+
values.push(positive, negative);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return values;
|
|
138
|
+
}
|
|
139
|
+
function collectWaterfallTotals(dataset, dataKey, totalKey) {
|
|
140
|
+
let cursor = 0;
|
|
141
|
+
const totals = [0];
|
|
142
|
+
dataset.forEach((row, index) => {
|
|
143
|
+
const raw = readBarValue(row, dataKey);
|
|
144
|
+
if (readBarFlag(row, totalKey)) {
|
|
145
|
+
if (raw !== null && index === 0) {
|
|
146
|
+
cursor = raw;
|
|
147
|
+
}
|
|
148
|
+
totals.push(cursor);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (raw === null) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
cursor += raw;
|
|
155
|
+
totals.push(cursor);
|
|
156
|
+
});
|
|
157
|
+
return totals;
|
|
158
|
+
}
|
|
159
|
+
function stackSegments(row, members, offset) {
|
|
160
|
+
const raw = [];
|
|
161
|
+
let positiveTotal = 0;
|
|
162
|
+
for (const {
|
|
163
|
+
item,
|
|
164
|
+
seriesIndex
|
|
165
|
+
} of members) {
|
|
166
|
+
const value = readBarValue(row, item.dataKey);
|
|
167
|
+
if (value === null) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (offset === 'expand' && value <= 0) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (value > 0) {
|
|
174
|
+
positiveTotal += value;
|
|
175
|
+
}
|
|
176
|
+
raw.push({
|
|
177
|
+
item,
|
|
178
|
+
seriesIndex,
|
|
179
|
+
value,
|
|
180
|
+
display: value
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (offset !== 'expand' || positiveTotal <= 0) {
|
|
184
|
+
return raw;
|
|
185
|
+
}
|
|
186
|
+
return raw.map(segment => ({
|
|
187
|
+
...segment,
|
|
188
|
+
display: segment.value / positiveTotal * 100
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
function computeDomain(values) {
|
|
192
|
+
if (values.length === 0) {
|
|
193
|
+
return {
|
|
194
|
+
min: 0,
|
|
195
|
+
max: 1
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const min = Math.min(0, ...values);
|
|
199
|
+
let max = Math.max(0, ...values);
|
|
200
|
+
if (min === max) {
|
|
201
|
+
max = 1;
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
min,
|
|
205
|
+
max
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function finite(value) {
|
|
209
|
+
return value !== undefined && Number.isFinite(value) ? value : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Value-axis domain and ticks. `min` / `max` pin the domain exactly; otherwise it rounds out to
|
|
214
|
+
* readable ticks. On an `expand` axis, `min` / `max` are 0–1 shares.
|
|
215
|
+
*/
|
|
216
|
+
export function resolveBarValueScale(values, axis, expand) {
|
|
217
|
+
const scale = expand ? 100 : 1;
|
|
218
|
+
const axisMin = finite(axis?.min);
|
|
219
|
+
const axisMax = finite(axis?.max);
|
|
220
|
+
const tickNumber = finite(axis?.tickNumber);
|
|
221
|
+
const raw = expand ? {
|
|
222
|
+
min: 0,
|
|
223
|
+
max: 100
|
|
224
|
+
} : computeDomain(values);
|
|
225
|
+
const lo = axisMin !== undefined ? axisMin * scale : raw.min;
|
|
226
|
+
let hi = axisMax !== undefined ? axisMax * scale : raw.max;
|
|
227
|
+
if (hi <= lo) {
|
|
228
|
+
hi = lo + 1;
|
|
229
|
+
}
|
|
230
|
+
const candidates = expand && axisMin === undefined && axisMax === undefined && tickNumber === undefined ? EXPAND_TICKS : niceTicks(lo, hi, Math.round(tickNumber ?? (expand ? EXPAND_TICKS.length : DEFAULT_TICK_COUNT)));
|
|
231
|
+
const min = axisMin !== undefined ? lo : Math.min(lo, candidates[0] ?? lo);
|
|
232
|
+
const max = axisMax !== undefined ? hi : Math.max(hi, candidates.at(-1) ?? hi);
|
|
233
|
+
const epsilon = (max - min) * 1e-9;
|
|
234
|
+
const ticks = candidates.filter(tick => tick >= min - epsilon && tick <= max + epsilon);
|
|
235
|
+
return {
|
|
236
|
+
min,
|
|
237
|
+
max,
|
|
238
|
+
ticks
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Default tick formatter when the chart passes no `formatTick`. */
|
|
243
|
+
export function formatBarValue(value) {
|
|
244
|
+
if (Number.isInteger(value)) {
|
|
245
|
+
return String(value);
|
|
246
|
+
}
|
|
247
|
+
return String(round2(value));
|
|
248
|
+
}
|
|
249
|
+
function categoryLabelPadding(categories, orientation) {
|
|
250
|
+
if (orientation !== 'horizontal') {
|
|
251
|
+
return 40;
|
|
252
|
+
}
|
|
253
|
+
const longest = categories.reduce((max, label) => Math.max(max, label.length), 0);
|
|
254
|
+
return clamp(longest * 8 + 16, 64, 168);
|
|
255
|
+
}
|
|
256
|
+
const CATEGORY_EDGE_GUTTER = 28;
|
|
257
|
+
/** Average `text-xs` glyph width; SVG text cannot be measured before layout. */
|
|
258
|
+
const CATEGORY_LABEL_CHAR_WIDTH = 6.5;
|
|
259
|
+
const VALUE_TICK_LEFT = 40;
|
|
260
|
+
const VALUE_TICK_END = 28;
|
|
261
|
+
const TIGHT_GUTTER = 8;
|
|
262
|
+
const VALUES_END_GUTTER = 72;
|
|
263
|
+
const OVERLAY_VALUES_END_GUTTER = 156;
|
|
264
|
+
/** Room for `$1,234` above the tallest bar (`fontSize` 10 + iOS baseline). */
|
|
265
|
+
const VALUES_TOP_GUTTER = 36;
|
|
266
|
+
const PLOT_END_GUTTER = 20;
|
|
267
|
+
const NEGATIVE_VALUES_GUTTER = 16;
|
|
268
|
+
export function resolveCategoryLabelPosition(orientation, position) {
|
|
269
|
+
if (position === 'none') {
|
|
270
|
+
return 'none';
|
|
271
|
+
}
|
|
272
|
+
if (orientation === 'vertical') {
|
|
273
|
+
return position === 'top' ? 'top' : 'bottom';
|
|
274
|
+
}
|
|
275
|
+
return position === 'right' ? 'right' : 'left';
|
|
276
|
+
}
|
|
277
|
+
export function resolveBarChartGutters({
|
|
278
|
+
categories,
|
|
279
|
+
hasNegative,
|
|
280
|
+
orientation,
|
|
281
|
+
barLabel,
|
|
282
|
+
categoryLabelPosition,
|
|
283
|
+
showTicks = true,
|
|
284
|
+
overlay = false
|
|
285
|
+
}) {
|
|
286
|
+
const labels = resolveCategoryLabelPosition(orientation, categoryLabelPosition);
|
|
287
|
+
if (orientation === 'vertical') {
|
|
288
|
+
const labelsTop = labels === 'top';
|
|
289
|
+
return {
|
|
290
|
+
paddingLeft: showTicks ? VALUE_TICK_LEFT : TIGHT_GUTTER,
|
|
291
|
+
paddingRight: showTicks ? VALUE_TICK_END : PLOT_END_GUTTER,
|
|
292
|
+
paddingTop: labelsTop ? CATEGORY_EDGE_GUTTER : barLabel ? VALUES_TOP_GUTTER : TIGHT_GUTTER,
|
|
293
|
+
paddingBottom: (labelsTop || labels === 'none' ? TIGHT_GUTTER : CATEGORY_EDGE_GUTTER) + (hasNegative && barLabel ? NEGATIVE_VALUES_GUTTER : 0)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const labelsRight = labels === 'right';
|
|
297
|
+
const categoryPad = labels === 'none' ? TIGHT_GUTTER : categoryLabelPadding(categories, orientation);
|
|
298
|
+
const valuesEnd = barLabel && overlay ? OVERLAY_VALUES_END_GUTTER : barLabel ? VALUES_END_GUTTER : TIGHT_GUTTER;
|
|
299
|
+
return {
|
|
300
|
+
paddingLeft: labelsRight ? TIGHT_GUTTER : categoryPad,
|
|
301
|
+
paddingRight: labelsRight ? categoryPad : Math.max(valuesEnd, showTicks ? VALUE_TICK_END : 0),
|
|
302
|
+
paddingTop: TIGHT_GUTTER,
|
|
303
|
+
paddingBottom: showTicks ? CATEGORY_EDGE_GUTTER : TIGHT_GUTTER
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Gutters from rows with a string `label` field. `LineChart` lays out with the same gutters.
|
|
309
|
+
*/
|
|
310
|
+
export function barChartGutters(data, orientation, barLabel, categoryLabelPosition, showTicks = true, overlay = false) {
|
|
311
|
+
return resolveBarChartGutters({
|
|
312
|
+
categories: data.map(row => {
|
|
313
|
+
const label = row.label;
|
|
314
|
+
return typeof label === 'string' ? label : '';
|
|
315
|
+
}),
|
|
316
|
+
hasNegative: data.some(row => Object.values(row).some(value => typeof value === 'number' && value < 0)),
|
|
317
|
+
orientation,
|
|
318
|
+
barLabel,
|
|
319
|
+
categoryLabelPosition,
|
|
320
|
+
showTicks,
|
|
321
|
+
overlay
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Viewport is the visible box (screen or parent). Content grows when there are
|
|
327
|
+
* too many categories to keep a readable band (`scrollable` / `visiblePoints`).
|
|
328
|
+
*/
|
|
329
|
+
export function resolveBarChartContentSize({
|
|
330
|
+
viewportWidth,
|
|
331
|
+
viewportHeight,
|
|
332
|
+
categoryCount,
|
|
333
|
+
orientation,
|
|
334
|
+
gutters,
|
|
335
|
+
scrollable,
|
|
336
|
+
visiblePoints
|
|
337
|
+
}) {
|
|
338
|
+
if (!scrollable || categoryCount <= 0) {
|
|
339
|
+
return {
|
|
340
|
+
width: viewportWidth,
|
|
341
|
+
height: viewportHeight
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (orientation === 'vertical') {
|
|
345
|
+
const plotWidth = Math.max(viewportWidth - gutters.paddingLeft - gutters.paddingRight, 0);
|
|
346
|
+
const band = visiblePoints !== undefined && visiblePoints > 0 ? plotWidth / visiblePoints : MIN_CATEGORY_BAND;
|
|
347
|
+
const contentPlot = Math.max(plotWidth, categoryCount * band);
|
|
348
|
+
return {
|
|
349
|
+
width: contentPlot + gutters.paddingLeft + gutters.paddingRight,
|
|
350
|
+
height: viewportHeight
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const plotHeight = Math.max(viewportHeight - gutters.paddingTop - gutters.paddingBottom, 0);
|
|
354
|
+
const band = visiblePoints !== undefined && visiblePoints > 0 ? plotHeight / visiblePoints : MIN_CATEGORY_BAND;
|
|
355
|
+
const contentPlot = Math.max(plotHeight, categoryCount * band);
|
|
356
|
+
return {
|
|
357
|
+
width: viewportWidth,
|
|
358
|
+
height: contentPlot + gutters.paddingTop + gutters.paddingBottom
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function groupedRect({
|
|
362
|
+
index,
|
|
363
|
+
seriesIndex,
|
|
364
|
+
slot,
|
|
365
|
+
item,
|
|
366
|
+
label,
|
|
367
|
+
value,
|
|
368
|
+
orientation,
|
|
369
|
+
band,
|
|
370
|
+
groupOffset,
|
|
371
|
+
thickness,
|
|
372
|
+
gap,
|
|
373
|
+
plotX,
|
|
374
|
+
plotY,
|
|
375
|
+
valueToX,
|
|
376
|
+
valueToY
|
|
377
|
+
}) {
|
|
378
|
+
const zeroY = valueToY(0);
|
|
379
|
+
const zeroX = valueToX(0);
|
|
380
|
+
const base = {
|
|
381
|
+
index,
|
|
382
|
+
seriesId: item.id,
|
|
383
|
+
seriesIndex,
|
|
384
|
+
label,
|
|
385
|
+
seriesLabel: item.label,
|
|
386
|
+
value,
|
|
387
|
+
plotValue: value,
|
|
388
|
+
rounded: true
|
|
389
|
+
};
|
|
390
|
+
if (orientation === 'vertical') {
|
|
391
|
+
const x = plotX + index * band + groupOffset + slot * (thickness + gap);
|
|
392
|
+
const y = value >= 0 ? valueToY(value) : zeroY;
|
|
393
|
+
const barHeight = Math.abs(valueToY(value) - zeroY);
|
|
394
|
+
return {
|
|
395
|
+
...base,
|
|
396
|
+
x: round2(x),
|
|
397
|
+
y: round2(y),
|
|
398
|
+
width: round2(Math.max(thickness, 0)),
|
|
399
|
+
height: round2(Math.max(barHeight, 0))
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const y = plotY + index * band + groupOffset + slot * (thickness + gap);
|
|
403
|
+
const x = value >= 0 ? zeroX : valueToX(value);
|
|
404
|
+
const barWidth = Math.abs(valueToX(value) - zeroX);
|
|
405
|
+
return {
|
|
406
|
+
...base,
|
|
407
|
+
x: round2(x),
|
|
408
|
+
y: round2(y),
|
|
409
|
+
width: round2(Math.max(barWidth, 0)),
|
|
410
|
+
height: round2(Math.max(thickness, 0))
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function overlayRect({
|
|
414
|
+
index,
|
|
415
|
+
seriesIndex,
|
|
416
|
+
item,
|
|
417
|
+
label,
|
|
418
|
+
value,
|
|
419
|
+
orientation,
|
|
420
|
+
band,
|
|
421
|
+
groupOffset,
|
|
422
|
+
inner,
|
|
423
|
+
plotX,
|
|
424
|
+
plotY,
|
|
425
|
+
valueToX,
|
|
426
|
+
valueToY
|
|
427
|
+
}) {
|
|
428
|
+
const isPrimary = seriesIndex === 0;
|
|
429
|
+
const primaryThickness = inner * OVERLAY_PRIMARY_RATIO;
|
|
430
|
+
const thickness = isPrimary ? primaryThickness : inner * OVERLAY_COMPARE_RATIO;
|
|
431
|
+
const slot = plotX + index * band + groupOffset;
|
|
432
|
+
const slotY = plotY + index * band + groupOffset;
|
|
433
|
+
const offset = isPrimary ? 0 : primaryThickness + inner * OVERLAY_SPACING_RATIO;
|
|
434
|
+
const zeroY = valueToY(0);
|
|
435
|
+
const zeroX = valueToX(0);
|
|
436
|
+
const base = {
|
|
437
|
+
index,
|
|
438
|
+
seriesId: item.id,
|
|
439
|
+
seriesIndex,
|
|
440
|
+
label,
|
|
441
|
+
seriesLabel: item.label,
|
|
442
|
+
value,
|
|
443
|
+
plotValue: value,
|
|
444
|
+
rounded: true
|
|
445
|
+
};
|
|
446
|
+
if (orientation === 'vertical') {
|
|
447
|
+
const x = slot + offset;
|
|
448
|
+
const y = value >= 0 ? valueToY(value) : zeroY;
|
|
449
|
+
const barHeight = Math.abs(valueToY(value) - zeroY);
|
|
450
|
+
return {
|
|
451
|
+
...base,
|
|
452
|
+
x: round2(x),
|
|
453
|
+
y: round2(y),
|
|
454
|
+
width: round2(Math.max(thickness, 0)),
|
|
455
|
+
height: round2(Math.max(barHeight, 0))
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const y = slotY + offset;
|
|
459
|
+
const x = value >= 0 ? zeroX : valueToX(value);
|
|
460
|
+
const barWidth = Math.abs(valueToX(value) - zeroX);
|
|
461
|
+
return {
|
|
462
|
+
...base,
|
|
463
|
+
x: round2(x),
|
|
464
|
+
y: round2(y),
|
|
465
|
+
width: round2(Math.max(barWidth, 0)),
|
|
466
|
+
height: round2(Math.max(thickness, 0))
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function layoutStackedRow({
|
|
470
|
+
row,
|
|
471
|
+
label,
|
|
472
|
+
index,
|
|
473
|
+
group,
|
|
474
|
+
orientation,
|
|
475
|
+
band,
|
|
476
|
+
groupOffset,
|
|
477
|
+
thickness,
|
|
478
|
+
plotX,
|
|
479
|
+
plotY,
|
|
480
|
+
valueToX,
|
|
481
|
+
valueToY,
|
|
482
|
+
bars,
|
|
483
|
+
stackGap
|
|
484
|
+
}) {
|
|
485
|
+
const offset = group.offset ?? 'diverging';
|
|
486
|
+
const shared = offset === 'none';
|
|
487
|
+
const segments = stackSegments(row, group.members, offset);
|
|
488
|
+
const lastPositiveId = shared ? segments.at(-1)?.item.id : [...segments].reverse().find(segment => segment.display >= 0)?.item.id;
|
|
489
|
+
const lastNegativeId = shared ? segments.at(-1)?.item.id : [...segments].reverse().find(segment => segment.display < 0)?.item.id;
|
|
490
|
+
const separated = stackGap > 0;
|
|
491
|
+
let positiveCursor = 0;
|
|
492
|
+
let negativeCursor = 0;
|
|
493
|
+
let positiveCount = 0;
|
|
494
|
+
let negativeCount = 0;
|
|
495
|
+
segments.forEach(segment => {
|
|
496
|
+
const positive = segment.display >= 0;
|
|
497
|
+
const countBefore = shared ? positiveCount + negativeCount : positive ? positiveCount : negativeCount;
|
|
498
|
+
const gap = countBefore > 0 ? stackGap : 0;
|
|
499
|
+
if (positive) {
|
|
500
|
+
positiveCount += 1;
|
|
501
|
+
} else {
|
|
502
|
+
negativeCount += 1;
|
|
503
|
+
}
|
|
504
|
+
const rounded = separated || positive && segment.item.id === lastPositiveId || !positive && segment.item.id === lastNegativeId;
|
|
505
|
+
const from = shared || positive ? positiveCursor : negativeCursor;
|
|
506
|
+
const to = from + segment.display;
|
|
507
|
+
if (shared) {
|
|
508
|
+
positiveCursor = to;
|
|
509
|
+
negativeCursor = to;
|
|
510
|
+
} else if (positive) {
|
|
511
|
+
positiveCursor = to;
|
|
512
|
+
} else {
|
|
513
|
+
negativeCursor = to;
|
|
514
|
+
}
|
|
515
|
+
const base = {
|
|
516
|
+
index,
|
|
517
|
+
seriesId: segment.item.id,
|
|
518
|
+
seriesIndex: segment.seriesIndex,
|
|
519
|
+
label,
|
|
520
|
+
seriesLabel: segment.item.label,
|
|
521
|
+
value: segment.value,
|
|
522
|
+
plotValue: segment.display,
|
|
523
|
+
rounded,
|
|
524
|
+
roundAll: separated && countBefore > 0,
|
|
525
|
+
stackId: group.stackId,
|
|
526
|
+
...(offset === 'expand' ? {
|
|
527
|
+
expand: true
|
|
528
|
+
} : {})
|
|
529
|
+
};
|
|
530
|
+
if (orientation === 'vertical') {
|
|
531
|
+
const top = valueToY(Math.max(from, to));
|
|
532
|
+
const bottom = valueToY(Math.min(from, to));
|
|
533
|
+
bars.push({
|
|
534
|
+
...base,
|
|
535
|
+
x: round2(plotX + index * band + groupOffset),
|
|
536
|
+
y: round2(positive ? top : top + gap),
|
|
537
|
+
width: round2(Math.max(thickness, 0)),
|
|
538
|
+
height: round2(Math.max(bottom - top - gap, 0))
|
|
539
|
+
});
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const left = valueToX(Math.min(from, to));
|
|
543
|
+
const right = valueToX(Math.max(from, to));
|
|
544
|
+
bars.push({
|
|
545
|
+
...base,
|
|
546
|
+
x: round2(positive ? left + gap : left),
|
|
547
|
+
y: round2(plotY + index * band + groupOffset),
|
|
548
|
+
width: round2(Math.max(right - left - gap, 0)),
|
|
549
|
+
height: round2(Math.max(thickness, 0))
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
function waterfallRole(isTotal, delta) {
|
|
554
|
+
if (isTotal) {
|
|
555
|
+
return 'total';
|
|
556
|
+
}
|
|
557
|
+
return delta >= 0 ? 'increase' : 'decrease';
|
|
558
|
+
}
|
|
559
|
+
function layoutWaterfallBar({
|
|
560
|
+
item,
|
|
561
|
+
label,
|
|
562
|
+
index,
|
|
563
|
+
orientation,
|
|
564
|
+
band,
|
|
565
|
+
groupOffset,
|
|
566
|
+
thickness,
|
|
567
|
+
plotX,
|
|
568
|
+
plotY,
|
|
569
|
+
from,
|
|
570
|
+
to,
|
|
571
|
+
delta,
|
|
572
|
+
role,
|
|
573
|
+
valueToX,
|
|
574
|
+
valueToY
|
|
575
|
+
}) {
|
|
576
|
+
const low = Math.min(from, to);
|
|
577
|
+
const high = Math.max(from, to);
|
|
578
|
+
const base = {
|
|
579
|
+
index,
|
|
580
|
+
seriesId: item.id,
|
|
581
|
+
seriesIndex: index,
|
|
582
|
+
label,
|
|
583
|
+
seriesLabel: item.label,
|
|
584
|
+
value: role === 'total' ? to : delta,
|
|
585
|
+
plotValue: to - from,
|
|
586
|
+
rounded: true,
|
|
587
|
+
// Totals sit on the axis, so only the free end is rounded.
|
|
588
|
+
roundAll: role !== 'total',
|
|
589
|
+
waterfallRole: role
|
|
590
|
+
};
|
|
591
|
+
if (orientation === 'vertical') {
|
|
592
|
+
const top = valueToY(high);
|
|
593
|
+
const bottom = valueToY(low);
|
|
594
|
+
return {
|
|
595
|
+
...base,
|
|
596
|
+
x: round2(plotX + index * band + groupOffset),
|
|
597
|
+
y: round2(top),
|
|
598
|
+
width: round2(Math.max(thickness, 0)),
|
|
599
|
+
height: round2(Math.max(bottom - top, 0))
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const left = valueToX(low);
|
|
603
|
+
const right = valueToX(high);
|
|
604
|
+
return {
|
|
605
|
+
...base,
|
|
606
|
+
x: round2(left),
|
|
607
|
+
y: round2(plotY + index * band + groupOffset),
|
|
608
|
+
width: round2(Math.max(right - left, 0)),
|
|
609
|
+
height: round2(Math.max(thickness, 0))
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function waterfallConnector(fromBar, toBar, value, orientation, valueToX, valueToY) {
|
|
613
|
+
if (orientation === 'vertical') {
|
|
614
|
+
const y = round2(valueToY(value));
|
|
615
|
+
return {
|
|
616
|
+
x1: round2(fromBar.x + fromBar.width),
|
|
617
|
+
y1: y,
|
|
618
|
+
x2: round2(toBar.x),
|
|
619
|
+
y2: y
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const x = round2(valueToX(value));
|
|
623
|
+
return {
|
|
624
|
+
x1: x,
|
|
625
|
+
y1: round2(fromBar.y + fromBar.height),
|
|
626
|
+
x2: x,
|
|
627
|
+
y2: round2(toBar.y)
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
export function barPath(bar, orientation, radius) {
|
|
631
|
+
'worklet';
|
|
632
|
+
|
|
633
|
+
const {
|
|
634
|
+
x,
|
|
635
|
+
y,
|
|
636
|
+
width,
|
|
637
|
+
height
|
|
638
|
+
} = bar;
|
|
639
|
+
if (width <= 0 || height <= 0) {
|
|
640
|
+
return '';
|
|
641
|
+
}
|
|
642
|
+
if (!bar.rounded) {
|
|
643
|
+
return `M ${String(x)} ${String(y)} L ${String(x + width)} ${String(y)} L ${String(x + width)} ${String(y + height)} L ${String(x)} ${String(y + height)} Z`;
|
|
644
|
+
}
|
|
645
|
+
if (bar.roundAll) {
|
|
646
|
+
const r = Math.min(radius, width / 2, height / 2);
|
|
647
|
+
const x2 = x + width;
|
|
648
|
+
const y2 = y + height;
|
|
649
|
+
return `M ${String(x + r)} ${String(y)} L ${String(x2 - r)} ${String(y)} Q ${String(x2)} ${String(y)} ${String(x2)} ${String(y + r)} L ${String(x2)} ${String(y2 - r)} Q ${String(x2)} ${String(y2)} ${String(x2 - r)} ${String(y2)} L ${String(x + r)} ${String(y2)} Q ${String(x)} ${String(y2)} ${String(x)} ${String(y2 - r)} L ${String(x)} ${String(y + r)} Q ${String(x)} ${String(y)} ${String(x + r)} ${String(y)} Z`;
|
|
650
|
+
}
|
|
651
|
+
const r = orientation === 'vertical' ? Math.min(radius, width / 2, height) : Math.min(radius, height / 2, width);
|
|
652
|
+
const x2 = x + width;
|
|
653
|
+
const y2 = y + height;
|
|
654
|
+
const capPositive = bar.value >= 0;
|
|
655
|
+
if (orientation === 'vertical') {
|
|
656
|
+
if (capPositive) {
|
|
657
|
+
return `M ${String(x)} ${String(y2)} L ${String(x)} ${String(y + r)} Q ${String(x)} ${String(y)} ${String(x + r)} ${String(y)} L ${String(x2 - r)} ${String(y)} Q ${String(x2)} ${String(y)} ${String(x2)} ${String(y + r)} L ${String(x2)} ${String(y2)} Z`;
|
|
658
|
+
}
|
|
659
|
+
return `M ${String(x)} ${String(y)} L ${String(x2)} ${String(y)} L ${String(x2)} ${String(y2 - r)} Q ${String(x2)} ${String(y2)} ${String(x2 - r)} ${String(y2)} L ${String(x + r)} ${String(y2)} Q ${String(x)} ${String(y2)} ${String(x)} ${String(y2 - r)} Z`;
|
|
660
|
+
}
|
|
661
|
+
if (capPositive) {
|
|
662
|
+
return `M ${String(x)} ${String(y)} L ${String(x2 - r)} ${String(y)} Q ${String(x2)} ${String(y)} ${String(x2)} ${String(y + r)} L ${String(x2)} ${String(y2 - r)} Q ${String(x2)} ${String(y2)} ${String(x2 - r)} ${String(y2)} L ${String(x)} ${String(y2)} Z`;
|
|
663
|
+
}
|
|
664
|
+
return `M ${String(x2)} ${String(y)} L ${String(x2)} ${String(y2)} L ${String(x + r)} ${String(y2)} Q ${String(x)} ${String(y2)} ${String(x)} ${String(y2 - r)} L ${String(x)} ${String(y + r)} Q ${String(x)} ${String(y)} ${String(x + r)} ${String(y)} Z`;
|
|
665
|
+
}
|
|
666
|
+
const EMPTY_LAYOUT = {
|
|
667
|
+
bars: [],
|
|
668
|
+
categoryLabels: [],
|
|
669
|
+
valueTicks: [],
|
|
670
|
+
gridLines: [],
|
|
671
|
+
referenceLines: [],
|
|
672
|
+
connectors: [],
|
|
673
|
+
baseline: 0,
|
|
674
|
+
plot: {
|
|
675
|
+
x: 0,
|
|
676
|
+
y: 0,
|
|
677
|
+
width: 0,
|
|
678
|
+
height: 0
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
/** True when every stack group uses `expand`, so the value axis is a 0–1 share axis. */
|
|
683
|
+
export function isExpandAxis(kind, groups) {
|
|
684
|
+
return kind === 'bars' && groups.length > 0 && groups.every(group => group.offset === 'expand');
|
|
685
|
+
}
|
|
686
|
+
export function layoutBarChart({
|
|
687
|
+
dataset,
|
|
688
|
+
categories,
|
|
689
|
+
series,
|
|
690
|
+
kind = 'bars',
|
|
691
|
+
waterfallTotalKey,
|
|
692
|
+
width,
|
|
693
|
+
height,
|
|
694
|
+
orientation,
|
|
695
|
+
barLabel,
|
|
696
|
+
categoryLabelPosition,
|
|
697
|
+
showTicks = true,
|
|
698
|
+
referenceLines = [],
|
|
699
|
+
valueAxis,
|
|
700
|
+
formatTick = formatBarValue,
|
|
701
|
+
stackGap = DEFAULT_STACK_GAP,
|
|
702
|
+
categoryGapRatio = CATEGORY_GAP_RATIO,
|
|
703
|
+
barGapRatio = BAR_GAP_RATIO,
|
|
704
|
+
showCategoryLabel
|
|
705
|
+
}) {
|
|
706
|
+
if (width <= 0 || height <= 0 || dataset.length === 0 || series.length === 0) {
|
|
707
|
+
return EMPTY_LAYOUT;
|
|
708
|
+
}
|
|
709
|
+
const categoryOf = index => categories?.[index] ?? String(index);
|
|
710
|
+
const resolvedCategories = dataset.map((_, index) => categoryOf(index));
|
|
711
|
+
const overlay = kind === 'overlay';
|
|
712
|
+
const waterfall = kind === 'waterfall';
|
|
713
|
+
const groups = resolveBarGroups(series);
|
|
714
|
+
const expand = isExpandAxis(kind, groups);
|
|
715
|
+
const labels = resolveCategoryLabelPosition(orientation, categoryLabelPosition);
|
|
716
|
+
const {
|
|
717
|
+
paddingLeft,
|
|
718
|
+
paddingRight,
|
|
719
|
+
paddingTop,
|
|
720
|
+
paddingBottom
|
|
721
|
+
} = resolveBarChartGutters({
|
|
722
|
+
categories: resolvedCategories,
|
|
723
|
+
hasNegative: dataset.some(row => series.some(item => (readBarValue(row, item.dataKey) ?? 0) < 0)),
|
|
724
|
+
orientation,
|
|
725
|
+
barLabel,
|
|
726
|
+
categoryLabelPosition: labels,
|
|
727
|
+
showTicks,
|
|
728
|
+
overlay
|
|
729
|
+
});
|
|
730
|
+
const plot = {
|
|
731
|
+
x: paddingLeft,
|
|
732
|
+
y: paddingTop,
|
|
733
|
+
width: Math.max(width - paddingLeft - paddingRight, 0),
|
|
734
|
+
height: Math.max(height - paddingTop - paddingBottom, 0)
|
|
735
|
+
};
|
|
736
|
+
const waterfallSeries = series.at(0);
|
|
737
|
+
const dataValues = waterfall ? collectWaterfallTotals(dataset, waterfallSeries?.dataKey ?? '', waterfallTotalKey) : overlay ? groupDomainValues(dataset, {
|
|
738
|
+
stackId: undefined,
|
|
739
|
+
offset: null,
|
|
740
|
+
members: series.map((item, seriesIndex) => ({
|
|
741
|
+
item,
|
|
742
|
+
seriesIndex
|
|
743
|
+
}))
|
|
744
|
+
}) : groups.flatMap(group => groupDomainValues(dataset, group));
|
|
745
|
+
const values = [...dataValues, ...referenceLines.flatMap(line => Number.isFinite(line.value) ? [line.value] : [])];
|
|
746
|
+
const scale = resolveBarValueScale(values, valueAxis, expand);
|
|
747
|
+
const domainMin = scale.min;
|
|
748
|
+
const domainMax = scale.max;
|
|
749
|
+
const span = domainMax - domainMin || 1;
|
|
750
|
+
const categoryCount = dataset.length;
|
|
751
|
+
const slotCount = overlay || waterfall ? 1 : groups.length;
|
|
752
|
+
const band = orientation === 'vertical' ? plot.width / categoryCount : plot.height / categoryCount;
|
|
753
|
+
const gapShare = clamp(Number.isFinite(categoryGapRatio) ? categoryGapRatio : CATEGORY_GAP_RATIO, 0, 0.95);
|
|
754
|
+
const inner = band * (1 - gapShare);
|
|
755
|
+
const barGap = Math.max(Number.isFinite(barGapRatio) ? barGapRatio : BAR_GAP_RATIO, 0);
|
|
756
|
+
const gapSlots = Math.max(slotCount - 1, 0);
|
|
757
|
+
const thickness = inner / (slotCount + gapSlots * barGap);
|
|
758
|
+
const gap = gapSlots > 0 ? thickness * barGap : 0;
|
|
759
|
+
const groupOffset = (band - inner) / 2;
|
|
760
|
+
const toPlot = value => (clamp(value, domainMin, domainMax) - domainMin) / span;
|
|
761
|
+
const valueToY = value => plot.y + plot.height * (1 - toPlot(value));
|
|
762
|
+
const valueToX = value => plot.x + plot.width * toPlot(value);
|
|
763
|
+
const bars = [];
|
|
764
|
+
const categoryLabels = [];
|
|
765
|
+
const connectors = [];
|
|
766
|
+
let waterfallCursor = 0;
|
|
767
|
+
let waterfallPrev = null;
|
|
768
|
+
dataset.forEach((row, index) => {
|
|
769
|
+
const label = categoryOf(index);
|
|
770
|
+
const shownLabel = showCategoryLabel === undefined || showCategoryLabel(index) ? label : '';
|
|
771
|
+
// With `none`, category names still reach the tooltip and screen readers through the bars.
|
|
772
|
+
if (labels !== 'none' && orientation === 'vertical') {
|
|
773
|
+
const center = plot.x + index * band + band / 2;
|
|
774
|
+
const halfWidth = shownLabel.length * CATEGORY_LABEL_CHAR_WIDTH / 2;
|
|
775
|
+
const anchor = center - halfWidth < plot.x ? {
|
|
776
|
+
x: plot.x,
|
|
777
|
+
textAnchor: 'start'
|
|
778
|
+
} : center + halfWidth > plot.x + plot.width ? {
|
|
779
|
+
x: plot.x + plot.width,
|
|
780
|
+
textAnchor: 'end'
|
|
781
|
+
} : {
|
|
782
|
+
x: center,
|
|
783
|
+
textAnchor: 'middle'
|
|
784
|
+
};
|
|
785
|
+
categoryLabels.push({
|
|
786
|
+
label: shownLabel,
|
|
787
|
+
x: round2(anchor.x),
|
|
788
|
+
y: round2(labels === 'top' ? paddingTop / 2 : height - 8),
|
|
789
|
+
textAnchor: anchor.textAnchor
|
|
790
|
+
});
|
|
791
|
+
} else if (labels !== 'none') {
|
|
792
|
+
categoryLabels.push({
|
|
793
|
+
label: shownLabel,
|
|
794
|
+
x: round2(labels === 'right' ? plot.x + plot.width + 8 : plot.x - 8),
|
|
795
|
+
y: round2(plot.y + index * band + band / 2),
|
|
796
|
+
textAnchor: labels === 'right' ? 'start' : 'end'
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
if (waterfall) {
|
|
800
|
+
if (waterfallSeries === undefined) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const isTotal = readBarFlag(row, waterfallTotalKey);
|
|
804
|
+
const raw = readBarValue(row, waterfallSeries.dataKey);
|
|
805
|
+
if (!isTotal && raw === null) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const from = isTotal ? 0 : waterfallCursor;
|
|
809
|
+
if (isTotal) {
|
|
810
|
+
if (raw !== null && index === 0) {
|
|
811
|
+
waterfallCursor = raw;
|
|
812
|
+
}
|
|
813
|
+
} else if (raw !== null) {
|
|
814
|
+
waterfallCursor += raw;
|
|
815
|
+
}
|
|
816
|
+
const to = waterfallCursor;
|
|
817
|
+
const delta = isTotal ? to : raw ?? 0;
|
|
818
|
+
const bar = layoutWaterfallBar({
|
|
819
|
+
item: waterfallSeries,
|
|
820
|
+
label,
|
|
821
|
+
index,
|
|
822
|
+
orientation,
|
|
823
|
+
band,
|
|
824
|
+
groupOffset,
|
|
825
|
+
thickness,
|
|
826
|
+
plotX: plot.x,
|
|
827
|
+
plotY: plot.y,
|
|
828
|
+
from,
|
|
829
|
+
to,
|
|
830
|
+
delta,
|
|
831
|
+
role: waterfallRole(isTotal, delta),
|
|
832
|
+
valueToX,
|
|
833
|
+
valueToY
|
|
834
|
+
});
|
|
835
|
+
if (waterfallPrev !== null) {
|
|
836
|
+
connectors.push(waterfallConnector(waterfallPrev.bar, bar, waterfallPrev.total, orientation, valueToX, valueToY));
|
|
837
|
+
}
|
|
838
|
+
bars.push(bar);
|
|
839
|
+
waterfallPrev = {
|
|
840
|
+
bar,
|
|
841
|
+
total: to
|
|
842
|
+
};
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (overlay) {
|
|
846
|
+
series.forEach((item, seriesIndex) => {
|
|
847
|
+
const value = readBarValue(row, item.dataKey);
|
|
848
|
+
if (value === null) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
bars.push(overlayRect({
|
|
852
|
+
index,
|
|
853
|
+
seriesIndex,
|
|
854
|
+
item,
|
|
855
|
+
label,
|
|
856
|
+
value,
|
|
857
|
+
orientation,
|
|
858
|
+
band,
|
|
859
|
+
groupOffset,
|
|
860
|
+
inner,
|
|
861
|
+
plotX: plot.x,
|
|
862
|
+
plotY: plot.y,
|
|
863
|
+
valueToX,
|
|
864
|
+
valueToY
|
|
865
|
+
}));
|
|
866
|
+
});
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
groups.forEach((group, slot) => {
|
|
870
|
+
const slotOffset = groupOffset + slot * (thickness + gap);
|
|
871
|
+
if (group.stackId !== undefined) {
|
|
872
|
+
layoutStackedRow({
|
|
873
|
+
row,
|
|
874
|
+
label,
|
|
875
|
+
index,
|
|
876
|
+
group,
|
|
877
|
+
orientation,
|
|
878
|
+
band,
|
|
879
|
+
groupOffset: slotOffset,
|
|
880
|
+
thickness,
|
|
881
|
+
plotX: plot.x,
|
|
882
|
+
plotY: plot.y,
|
|
883
|
+
valueToX,
|
|
884
|
+
valueToY,
|
|
885
|
+
bars,
|
|
886
|
+
stackGap: Math.max(stackGap, 0)
|
|
887
|
+
});
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const member = group.members.at(0);
|
|
891
|
+
if (member === undefined) {
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
const value = readBarValue(row, member.item.dataKey);
|
|
895
|
+
if (value === null) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
bars.push(groupedRect({
|
|
899
|
+
index,
|
|
900
|
+
seriesIndex: member.seriesIndex,
|
|
901
|
+
slot,
|
|
902
|
+
item: member.item,
|
|
903
|
+
label,
|
|
904
|
+
value,
|
|
905
|
+
orientation,
|
|
906
|
+
band,
|
|
907
|
+
groupOffset,
|
|
908
|
+
thickness,
|
|
909
|
+
gap,
|
|
910
|
+
plotX: plot.x,
|
|
911
|
+
plotY: plot.y,
|
|
912
|
+
valueToX,
|
|
913
|
+
valueToY
|
|
914
|
+
}));
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
const tickText = value => formatTick(expand ? value / 100 : value);
|
|
918
|
+
const valueTicks = scale.ticks.map(value => {
|
|
919
|
+
if (orientation === 'vertical') {
|
|
920
|
+
return {
|
|
921
|
+
value,
|
|
922
|
+
x: round2(plot.x - 8),
|
|
923
|
+
y: round2(valueToY(value)),
|
|
924
|
+
text: tickText(value)
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
return {
|
|
928
|
+
value,
|
|
929
|
+
x: round2(valueToX(value)),
|
|
930
|
+
y: round2(height - 8),
|
|
931
|
+
text: tickText(value)
|
|
932
|
+
};
|
|
933
|
+
});
|
|
934
|
+
const gridLines = scale.ticks.map(value => {
|
|
935
|
+
if (orientation === 'vertical') {
|
|
936
|
+
const y = round2(valueToY(value));
|
|
937
|
+
return {
|
|
938
|
+
x1: round2(plot.x),
|
|
939
|
+
y1: y,
|
|
940
|
+
x2: round2(plot.x + plot.width),
|
|
941
|
+
y2: y
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
const x = round2(valueToX(value));
|
|
945
|
+
return {
|
|
946
|
+
x1: x,
|
|
947
|
+
y1: round2(plot.y),
|
|
948
|
+
x2: x,
|
|
949
|
+
y2: round2(plot.y + plot.height)
|
|
950
|
+
};
|
|
951
|
+
});
|
|
952
|
+
const zero = clamp(0, domainMin, domainMax);
|
|
953
|
+
const baseline = round2(orientation === 'vertical' ? valueToY(zero) : valueToX(zero));
|
|
954
|
+
const laidReferenceLines = referenceLines.flatMap(line => {
|
|
955
|
+
if (!Number.isFinite(line.value) || line.value < domainMin || line.value > domainMax) {
|
|
956
|
+
return [];
|
|
957
|
+
}
|
|
958
|
+
if (orientation === 'vertical') {
|
|
959
|
+
const y = round2(valueToY(line.value));
|
|
960
|
+
return [{
|
|
961
|
+
x1: round2(plot.x),
|
|
962
|
+
y1: y,
|
|
963
|
+
x2: round2(plot.x + plot.width),
|
|
964
|
+
y2: y,
|
|
965
|
+
label: line.label
|
|
966
|
+
}];
|
|
967
|
+
}
|
|
968
|
+
const x = round2(valueToX(line.value));
|
|
969
|
+
return [{
|
|
970
|
+
x1: x,
|
|
971
|
+
y1: round2(plot.y),
|
|
972
|
+
x2: x,
|
|
973
|
+
y2: round2(plot.y + plot.height),
|
|
974
|
+
label: line.label
|
|
975
|
+
}];
|
|
976
|
+
});
|
|
977
|
+
return {
|
|
978
|
+
bars,
|
|
979
|
+
categoryLabels,
|
|
980
|
+
valueTicks,
|
|
981
|
+
gridLines,
|
|
982
|
+
referenceLines: laidReferenceLines,
|
|
983
|
+
connectors,
|
|
984
|
+
baseline,
|
|
985
|
+
plot
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
export function overlayDelta(bars, index) {
|
|
989
|
+
const group = bars.filter(bar => bar.index === index);
|
|
990
|
+
const primary = group.find(bar => bar.seriesIndex === 0);
|
|
991
|
+
const comparison = group.find(bar => bar.seriesIndex === 1);
|
|
992
|
+
if (primary === undefined || comparison === undefined) {
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
return round2(primary.value - comparison.value);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/** Waterfall steps read as signed changes; totals read as the running total. */
|
|
999
|
+
export function formatWaterfallBarValue(bar, formatValue) {
|
|
1000
|
+
if (bar.waterfallRole === 'increase') {
|
|
1001
|
+
return `+${formatValue(bar.value)}`;
|
|
1002
|
+
}
|
|
1003
|
+
if (bar.waterfallRole === 'decrease') {
|
|
1004
|
+
return `-${formatValue(Math.abs(bar.value))}`;
|
|
1005
|
+
}
|
|
1006
|
+
return formatValue(bar.value);
|
|
1007
|
+
}
|
|
1008
|
+
export function waterfallValueAnchor(bar, orientation) {
|
|
1009
|
+
if (orientation === 'vertical') {
|
|
1010
|
+
return {
|
|
1011
|
+
x: round2(bar.x + bar.width / 2),
|
|
1012
|
+
y: round2(bar.y - 4),
|
|
1013
|
+
textAnchor: 'middle'
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
x: round2(bar.x + bar.width + 4),
|
|
1018
|
+
y: round2(bar.y + bar.height / 2),
|
|
1019
|
+
textAnchor: 'start'
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
export function barValueAnchor(bar, orientation) {
|
|
1023
|
+
if (orientation === 'vertical') {
|
|
1024
|
+
return {
|
|
1025
|
+
x: round2(bar.x + bar.width / 2),
|
|
1026
|
+
y: round2(bar.plotValue >= 0 ? Math.max(bar.y - 12, 12) : bar.y + bar.height + 12),
|
|
1027
|
+
textAnchor: 'middle'
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
x: round2(bar.plotValue >= 0 ? bar.x + bar.width + 4 : bar.x - 4),
|
|
1032
|
+
y: round2(bar.y + bar.height / 2),
|
|
1033
|
+
textAnchor: bar.plotValue >= 0 ? 'start' : 'end'
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
/** One total label per stack in each category. */
|
|
1037
|
+
export function stackedTotalAnchors(bars, orientation) {
|
|
1038
|
+
const byStack = new Map();
|
|
1039
|
+
bars.forEach(bar => {
|
|
1040
|
+
if (bar.stackId === undefined) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const key = `${String(bar.index)}:${bar.stackId}`;
|
|
1044
|
+
const group = byStack.get(key) ?? [];
|
|
1045
|
+
group.push(bar);
|
|
1046
|
+
byStack.set(key, group);
|
|
1047
|
+
});
|
|
1048
|
+
return [...byStack.entries()].flatMap(([key, group]) => {
|
|
1049
|
+
const outer = group.filter(bar => bar.plotValue >= 0).at(-1) ?? group.at(-1);
|
|
1050
|
+
if (outer === undefined) {
|
|
1051
|
+
return [];
|
|
1052
|
+
}
|
|
1053
|
+
const total = group.reduce((sum, bar) => sum + bar.plotValue, 0);
|
|
1054
|
+
const anchor = barValueAnchor({
|
|
1055
|
+
...outer,
|
|
1056
|
+
plotValue: total,
|
|
1057
|
+
value: total
|
|
1058
|
+
}, orientation);
|
|
1059
|
+
return [{
|
|
1060
|
+
key,
|
|
1061
|
+
index: outer.index,
|
|
1062
|
+
...anchor,
|
|
1063
|
+
total,
|
|
1064
|
+
expand: outer.expand === true,
|
|
1065
|
+
seriesId: outer.seriesId
|
|
1066
|
+
}];
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Next bar for keyboard movement. Along the category axis keeps the same series; along the
|
|
1072
|
+
* value axis steps series inside the category. Home / End jump the category axis.
|
|
1073
|
+
*/
|
|
1074
|
+
export function barKeyboardNeighbor(bars, from, key, orientation) {
|
|
1075
|
+
const current = bars[from];
|
|
1076
|
+
if (current === undefined) {
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
const categories = [...new Set(bars.map(bar => bar.index))];
|
|
1080
|
+
const categoryPos = categories.indexOf(current.index);
|
|
1081
|
+
const seriesInCategory = bars.map((bar, index) => ({
|
|
1082
|
+
bar,
|
|
1083
|
+
index
|
|
1084
|
+
})).filter(item => item.bar.index === current.index);
|
|
1085
|
+
const seriesPos = seriesInCategory.findIndex(item => item.index === from);
|
|
1086
|
+
const alongCategory = key === 'Tab' ? 1 : key === 'Shift+Tab' ? -1 : orientation === 'vertical' ? key === 'ArrowRight' || key === 'End' ? 1 : key === 'ArrowLeft' || key === 'Home' ? -1 : 0 : key === 'ArrowDown' || key === 'End' ? 1 : key === 'ArrowUp' || key === 'Home' ? -1 : 0;
|
|
1087
|
+
const alongSeries = orientation === 'vertical' ? key === 'ArrowDown' ? 1 : key === 'ArrowUp' ? -1 : 0 : key === 'ArrowRight' ? 1 : key === 'ArrowLeft' ? -1 : 0;
|
|
1088
|
+
if (key === 'Home' || key === 'End') {
|
|
1089
|
+
const nextCategory = alongCategory > 0 ? categories.at(-1) : categories[0];
|
|
1090
|
+
if (nextCategory === undefined) {
|
|
1091
|
+
return from;
|
|
1092
|
+
}
|
|
1093
|
+
const match = bars.findIndex(bar => bar.index === nextCategory && bar.seriesIndex === current.seriesIndex);
|
|
1094
|
+
return match >= 0 ? match : bars.findIndex(bar => bar.index === nextCategory);
|
|
1095
|
+
}
|
|
1096
|
+
if (alongCategory !== 0) {
|
|
1097
|
+
const nextCategory = categories[categoryPos + alongCategory];
|
|
1098
|
+
if (nextCategory === undefined) {
|
|
1099
|
+
return from;
|
|
1100
|
+
}
|
|
1101
|
+
const match = bars.findIndex(bar => bar.index === nextCategory && bar.seriesIndex === current.seriesIndex);
|
|
1102
|
+
return match >= 0 ? match : bars.findIndex(bar => bar.index === nextCategory);
|
|
1103
|
+
}
|
|
1104
|
+
if (alongSeries !== 0) {
|
|
1105
|
+
const next = seriesInCategory[seriesPos + alongSeries];
|
|
1106
|
+
return next === undefined ? from : next.index;
|
|
1107
|
+
}
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
//# sourceMappingURL=geometry.js.map
|