@cdx-ui/components 0.3.4 → 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/select/content.md +144 -2
- package/docs/components/stepper/content.md +599 -0
- package/docs/components/system-alert/content.md +533 -0
- package/docs/foundations/expressions/icons/content.md +1 -1
- package/docs/foundations/styles/tokens/all-tokens/content.md +2 -1
- package/docs/llms.txt +10 -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.4/content.md +27 -0
- package/docs/resources/release-notes/0.3.5/content.md +7 -0
- package/docs/resources/release-notes/content.md +2 -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/Chip/styles.js +3 -1
- package/lib/commonjs/components/Chip/styles.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/Chip/styles.js +3 -1
- package/lib/module/components/Chip/styles.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/Chip/styles.d.ts.map +1 -1
- 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/Chip/styles.ts +3 -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,1469 @@
|
|
|
1
|
+
import { niceTicks } from '../chart/ticks';
|
|
2
|
+
|
|
3
|
+
export type BarChartOrientation = 'vertical' | 'horizontal';
|
|
4
|
+
/**
|
|
5
|
+
* Where category names sit around the plot. `top` / `bottom` apply to vertical
|
|
6
|
+
* bars; `left` / `right` apply to horizontal bars. Other combinations fall back
|
|
7
|
+
* to `bottom` or `left`. `none` draws no category names.
|
|
8
|
+
*/
|
|
9
|
+
export type BarChartCategoryLabelPosition = 'top' | 'bottom' | 'left' | 'right' | 'none';
|
|
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
|
+
export type BarStackOffset = 'diverging' | 'none' | 'expand';
|
|
17
|
+
|
|
18
|
+
/** How the chart draws its series: side by side or stacked, a comparison overlay, or a waterfall. */
|
|
19
|
+
export type BarChartKind = 'bars' | 'overlay' | 'waterfall';
|
|
20
|
+
|
|
21
|
+
export type BarWaterfallRole = 'total' | 'increase' | 'decrease';
|
|
22
|
+
|
|
23
|
+
/** A series resolved for layout. `id` is the public series id. */
|
|
24
|
+
export interface BarGeometrySeries {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly dataKey: string;
|
|
27
|
+
readonly label: string;
|
|
28
|
+
/** Stack group id. Series with the same id share one bar per category. */
|
|
29
|
+
readonly stack?: string;
|
|
30
|
+
readonly stackOffset?: BarStackOffset;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BarChartReferenceLine {
|
|
34
|
+
readonly value: number;
|
|
35
|
+
readonly label?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BarValueAxisConfig {
|
|
39
|
+
readonly min?: number;
|
|
40
|
+
readonly max?: number;
|
|
41
|
+
readonly tickNumber?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface BarRect {
|
|
45
|
+
/** Row in the dataset. */
|
|
46
|
+
readonly index: number;
|
|
47
|
+
readonly seriesId: string;
|
|
48
|
+
/** Position in the resolved series list (or the category, for waterfall). Picks the fill. */
|
|
49
|
+
readonly seriesIndex: number;
|
|
50
|
+
readonly label: string;
|
|
51
|
+
readonly seriesLabel: string;
|
|
52
|
+
readonly value: number;
|
|
53
|
+
readonly plotValue: number;
|
|
54
|
+
readonly x: number;
|
|
55
|
+
readonly y: number;
|
|
56
|
+
readonly width: number;
|
|
57
|
+
readonly height: number;
|
|
58
|
+
/** Round the end away from the baseline (the outer cap). */
|
|
59
|
+
readonly rounded: boolean;
|
|
60
|
+
/** Round every corner (floating waterfall steps and inner stacked segments with a gap). */
|
|
61
|
+
readonly roundAll?: boolean;
|
|
62
|
+
/** Set on stacked segments. */
|
|
63
|
+
readonly stackId?: string;
|
|
64
|
+
/** Set on `expand` segments: `plotValue` is a 0–100 share. */
|
|
65
|
+
readonly expand?: boolean;
|
|
66
|
+
/** Set on waterfall bars. */
|
|
67
|
+
readonly waterfallRole?: BarWaterfallRole;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface BarLabelAnchor {
|
|
71
|
+
readonly label: string;
|
|
72
|
+
readonly x: number;
|
|
73
|
+
readonly y: number;
|
|
74
|
+
readonly textAnchor: 'start' | 'middle' | 'end';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface BarValueTick {
|
|
78
|
+
readonly value: number;
|
|
79
|
+
readonly x: number;
|
|
80
|
+
readonly y: number;
|
|
81
|
+
readonly text: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface BarGridLine {
|
|
85
|
+
readonly x1: number;
|
|
86
|
+
readonly y1: number;
|
|
87
|
+
readonly x2: number;
|
|
88
|
+
readonly y2: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface BarReferenceLineLayout extends BarGridLine {
|
|
92
|
+
readonly label?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface BarChartLayout {
|
|
96
|
+
readonly bars: BarRect[];
|
|
97
|
+
readonly categoryLabels: BarLabelAnchor[];
|
|
98
|
+
readonly valueTicks: BarValueTick[];
|
|
99
|
+
readonly gridLines: BarGridLine[];
|
|
100
|
+
readonly referenceLines: readonly BarReferenceLineLayout[];
|
|
101
|
+
/** Dashed bridges from one waterfall step to the next, at the running total. */
|
|
102
|
+
readonly connectors: readonly BarGridLine[];
|
|
103
|
+
/** Value-axis coordinate of zero (y when vertical, x when horizontal). Bars grow from here. */
|
|
104
|
+
readonly baseline: number;
|
|
105
|
+
readonly plot: {
|
|
106
|
+
readonly x: number;
|
|
107
|
+
readonly y: number;
|
|
108
|
+
readonly width: number;
|
|
109
|
+
readonly height: number;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Share of each category slot left empty around the bars. MUI X `categoryGapRatio`. */
|
|
114
|
+
export const CATEGORY_GAP_RATIO = 0.36;
|
|
115
|
+
/** Gap between bars in one category, as a share of one bar's thickness. MUI X `barGapRatio`. */
|
|
116
|
+
export const BAR_GAP_RATIO = 0.5;
|
|
117
|
+
/** Primary bar share of the overlay slot. Comparison series are thinner and peek out. */
|
|
118
|
+
export const OVERLAY_PRIMARY_RATIO = 0.7;
|
|
119
|
+
export const OVERLAY_COMPARE_RATIO = 0.16;
|
|
120
|
+
export const OVERLAY_SPACING_RATIO = 0.08;
|
|
121
|
+
/** Pixel gap between stacked segments. `0` joins them with only the outer cap rounded. */
|
|
122
|
+
export const DEFAULT_STACK_GAP = 3;
|
|
123
|
+
export const DEFAULT_TICK_COUNT = 4;
|
|
124
|
+
/** Minimum pixels per category when scrolling. Keeps labels and bars readable. */
|
|
125
|
+
export const MIN_CATEGORY_BAND = 48;
|
|
126
|
+
/** `expand` ticks when the value axis sets no `min`, `max`, or `tickNumber`. */
|
|
127
|
+
const EXPAND_TICKS: readonly number[] = [0, 25, 50, 75, 100];
|
|
128
|
+
|
|
129
|
+
export interface BarChartGutters {
|
|
130
|
+
readonly paddingLeft: number;
|
|
131
|
+
readonly paddingRight: number;
|
|
132
|
+
readonly paddingTop: number;
|
|
133
|
+
readonly paddingBottom: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function clamp(value: number, min: number, max: number): number {
|
|
137
|
+
return Math.min(max, Math.max(min, value));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function round2(value: number): number {
|
|
141
|
+
const rounded = Math.round(value * 100) / 100;
|
|
142
|
+
return Object.is(rounded, -0) ? 0 : rounded;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Reads a finite number from `row[key]`. Anything else is a missing value. */
|
|
146
|
+
export function readBarValue(row: object, key: string): number | null {
|
|
147
|
+
const raw = (row as Record<string, unknown>)[key];
|
|
148
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return raw;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readBarFlag(row: object, key: string | undefined): boolean {
|
|
155
|
+
return key !== undefined && (row as Record<string, unknown>)[key] === true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface BarGroupMember {
|
|
159
|
+
readonly item: BarGeometrySeries;
|
|
160
|
+
readonly seriesIndex: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface BarGroup {
|
|
164
|
+
/** `undefined` for a series drawn on its own (not stacked). */
|
|
165
|
+
readonly stackId: string | undefined;
|
|
166
|
+
readonly offset: BarStackOffset | null;
|
|
167
|
+
readonly members: readonly BarGroupMember[];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* One slot per stack group, in first-appearance order. A series without `stack` gets its own
|
|
172
|
+
* slot. The first `stackOffset` set in a group applies to the whole group.
|
|
173
|
+
*/
|
|
174
|
+
export function resolveBarGroups(series: readonly BarGeometrySeries[]): readonly BarGroup[] {
|
|
175
|
+
const groups: {
|
|
176
|
+
stackId: string | undefined;
|
|
177
|
+
offset: BarStackOffset | null;
|
|
178
|
+
members: BarGroupMember[];
|
|
179
|
+
}[] = [];
|
|
180
|
+
const byStack = new Map<string, (typeof groups)[number]>();
|
|
181
|
+
series.forEach((item, seriesIndex) => {
|
|
182
|
+
if (item.stack === undefined) {
|
|
183
|
+
groups.push({ stackId: undefined, offset: null, members: [{ item, seriesIndex }] });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
let group = byStack.get(item.stack);
|
|
187
|
+
if (group === undefined) {
|
|
188
|
+
group = { stackId: item.stack, offset: null, members: [] };
|
|
189
|
+
byStack.set(item.stack, group);
|
|
190
|
+
groups.push(group);
|
|
191
|
+
}
|
|
192
|
+
group.members.push({ item, seriesIndex });
|
|
193
|
+
if (group.offset === null && item.stackOffset !== undefined) {
|
|
194
|
+
group.offset = item.stackOffset;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return groups.map((group) =>
|
|
198
|
+
group.stackId === undefined ? group : { ...group, offset: group.offset ?? 'diverging' },
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function groupDomainValues(dataset: readonly object[], group: BarGroup): number[] {
|
|
203
|
+
if (group.offset === 'expand') {
|
|
204
|
+
return [0, 100];
|
|
205
|
+
}
|
|
206
|
+
const values: number[] = [];
|
|
207
|
+
for (const row of dataset) {
|
|
208
|
+
if (group.offset === null) {
|
|
209
|
+
for (const { item } of group.members) {
|
|
210
|
+
const value = readBarValue(row, item.dataKey);
|
|
211
|
+
if (value !== null) {
|
|
212
|
+
values.push(value);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
let positive = 0;
|
|
218
|
+
let negative = 0;
|
|
219
|
+
let running = 0;
|
|
220
|
+
for (const { item } of group.members) {
|
|
221
|
+
const value = readBarValue(row, item.dataKey);
|
|
222
|
+
if (value === null) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (group.offset === 'none') {
|
|
226
|
+
running += value;
|
|
227
|
+
values.push(running);
|
|
228
|
+
} else if (value >= 0) {
|
|
229
|
+
positive += value;
|
|
230
|
+
} else {
|
|
231
|
+
negative += value;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (group.offset === 'diverging') {
|
|
235
|
+
values.push(positive, negative);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return values;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function collectWaterfallTotals(
|
|
242
|
+
dataset: readonly object[],
|
|
243
|
+
dataKey: string,
|
|
244
|
+
totalKey: string | undefined,
|
|
245
|
+
): number[] {
|
|
246
|
+
let cursor = 0;
|
|
247
|
+
const totals: number[] = [0];
|
|
248
|
+
dataset.forEach((row, index) => {
|
|
249
|
+
const raw = readBarValue(row, dataKey);
|
|
250
|
+
if (readBarFlag(row, totalKey)) {
|
|
251
|
+
if (raw !== null && index === 0) {
|
|
252
|
+
cursor = raw;
|
|
253
|
+
}
|
|
254
|
+
totals.push(cursor);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (raw === null) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
cursor += raw;
|
|
261
|
+
totals.push(cursor);
|
|
262
|
+
});
|
|
263
|
+
return totals;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface StackSegment {
|
|
267
|
+
readonly item: BarGeometrySeries;
|
|
268
|
+
readonly seriesIndex: number;
|
|
269
|
+
readonly value: number;
|
|
270
|
+
readonly display: number;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function stackSegments(
|
|
274
|
+
row: object,
|
|
275
|
+
members: readonly BarGroupMember[],
|
|
276
|
+
offset: BarStackOffset,
|
|
277
|
+
): StackSegment[] {
|
|
278
|
+
const raw: StackSegment[] = [];
|
|
279
|
+
let positiveTotal = 0;
|
|
280
|
+
for (const { item, seriesIndex } of members) {
|
|
281
|
+
const value = readBarValue(row, item.dataKey);
|
|
282
|
+
if (value === null) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (offset === 'expand' && value <= 0) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (value > 0) {
|
|
289
|
+
positiveTotal += value;
|
|
290
|
+
}
|
|
291
|
+
raw.push({ item, seriesIndex, value, display: value });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (offset !== 'expand' || positiveTotal <= 0) {
|
|
295
|
+
return raw;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return raw.map((segment) => ({
|
|
299
|
+
...segment,
|
|
300
|
+
display: (segment.value / positiveTotal) * 100,
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function computeDomain(values: readonly number[]): { min: number; max: number } {
|
|
305
|
+
if (values.length === 0) {
|
|
306
|
+
return { min: 0, max: 1 };
|
|
307
|
+
}
|
|
308
|
+
const min = Math.min(0, ...values);
|
|
309
|
+
let max = Math.max(0, ...values);
|
|
310
|
+
if (min === max) {
|
|
311
|
+
max = 1;
|
|
312
|
+
}
|
|
313
|
+
return { min, max };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function finite(value: number | undefined): number | undefined {
|
|
317
|
+
return value !== undefined && Number.isFinite(value) ? value : undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Value-axis domain and ticks. `min` / `max` pin the domain exactly; otherwise it rounds out to
|
|
322
|
+
* readable ticks. On an `expand` axis, `min` / `max` are 0–1 shares.
|
|
323
|
+
*/
|
|
324
|
+
export function resolveBarValueScale(
|
|
325
|
+
values: readonly number[],
|
|
326
|
+
axis: BarValueAxisConfig | undefined,
|
|
327
|
+
expand: boolean,
|
|
328
|
+
): { readonly min: number; readonly max: number; readonly ticks: readonly number[] } {
|
|
329
|
+
const scale = expand ? 100 : 1;
|
|
330
|
+
const axisMin = finite(axis?.min);
|
|
331
|
+
const axisMax = finite(axis?.max);
|
|
332
|
+
const tickNumber = finite(axis?.tickNumber);
|
|
333
|
+
const raw = expand ? { min: 0, max: 100 } : computeDomain(values);
|
|
334
|
+
const lo = axisMin !== undefined ? axisMin * scale : raw.min;
|
|
335
|
+
let hi = axisMax !== undefined ? axisMax * scale : raw.max;
|
|
336
|
+
if (hi <= lo) {
|
|
337
|
+
hi = lo + 1;
|
|
338
|
+
}
|
|
339
|
+
const candidates =
|
|
340
|
+
expand && axisMin === undefined && axisMax === undefined && tickNumber === undefined
|
|
341
|
+
? EXPAND_TICKS
|
|
342
|
+
: niceTicks(
|
|
343
|
+
lo,
|
|
344
|
+
hi,
|
|
345
|
+
Math.round(tickNumber ?? (expand ? EXPAND_TICKS.length : DEFAULT_TICK_COUNT)),
|
|
346
|
+
);
|
|
347
|
+
const min = axisMin !== undefined ? lo : Math.min(lo, candidates[0] ?? lo);
|
|
348
|
+
const max = axisMax !== undefined ? hi : Math.max(hi, candidates.at(-1) ?? hi);
|
|
349
|
+
const epsilon = (max - min) * 1e-9;
|
|
350
|
+
const ticks = candidates.filter((tick) => tick >= min - epsilon && tick <= max + epsilon);
|
|
351
|
+
return { min, max, ticks };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Default tick formatter when the chart passes no `formatTick`. */
|
|
355
|
+
export function formatBarValue(value: number): string {
|
|
356
|
+
if (Number.isInteger(value)) {
|
|
357
|
+
return String(value);
|
|
358
|
+
}
|
|
359
|
+
return String(round2(value));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function categoryLabelPadding(
|
|
363
|
+
categories: readonly string[],
|
|
364
|
+
orientation: BarChartOrientation,
|
|
365
|
+
): number {
|
|
366
|
+
if (orientation !== 'horizontal') {
|
|
367
|
+
return 40;
|
|
368
|
+
}
|
|
369
|
+
const longest = categories.reduce((max, label) => Math.max(max, label.length), 0);
|
|
370
|
+
return clamp(longest * 8 + 16, 64, 168);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const CATEGORY_EDGE_GUTTER = 28;
|
|
374
|
+
/** Average `text-xs` glyph width; SVG text cannot be measured before layout. */
|
|
375
|
+
const CATEGORY_LABEL_CHAR_WIDTH = 6.5;
|
|
376
|
+
const VALUE_TICK_LEFT = 40;
|
|
377
|
+
const VALUE_TICK_END = 28;
|
|
378
|
+
const TIGHT_GUTTER = 8;
|
|
379
|
+
const VALUES_END_GUTTER = 72;
|
|
380
|
+
const OVERLAY_VALUES_END_GUTTER = 156;
|
|
381
|
+
/** Room for `$1,234` above the tallest bar (`fontSize` 10 + iOS baseline). */
|
|
382
|
+
const VALUES_TOP_GUTTER = 36;
|
|
383
|
+
const PLOT_END_GUTTER = 20;
|
|
384
|
+
const NEGATIVE_VALUES_GUTTER = 16;
|
|
385
|
+
|
|
386
|
+
export function resolveCategoryLabelPosition(
|
|
387
|
+
orientation: BarChartOrientation,
|
|
388
|
+
position: BarChartCategoryLabelPosition | undefined,
|
|
389
|
+
): BarChartCategoryLabelPosition {
|
|
390
|
+
if (position === 'none') {
|
|
391
|
+
return 'none';
|
|
392
|
+
}
|
|
393
|
+
if (orientation === 'vertical') {
|
|
394
|
+
return position === 'top' ? 'top' : 'bottom';
|
|
395
|
+
}
|
|
396
|
+
return position === 'right' ? 'right' : 'left';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function resolveBarChartGutters({
|
|
400
|
+
categories,
|
|
401
|
+
hasNegative,
|
|
402
|
+
orientation,
|
|
403
|
+
barLabel,
|
|
404
|
+
categoryLabelPosition,
|
|
405
|
+
showTicks = true,
|
|
406
|
+
overlay = false,
|
|
407
|
+
}: {
|
|
408
|
+
readonly categories: readonly string[];
|
|
409
|
+
readonly hasNegative: boolean;
|
|
410
|
+
readonly orientation: BarChartOrientation;
|
|
411
|
+
readonly barLabel: boolean;
|
|
412
|
+
readonly categoryLabelPosition?: BarChartCategoryLabelPosition;
|
|
413
|
+
readonly showTicks?: boolean;
|
|
414
|
+
readonly overlay?: boolean;
|
|
415
|
+
}): BarChartGutters {
|
|
416
|
+
const labels = resolveCategoryLabelPosition(orientation, categoryLabelPosition);
|
|
417
|
+
if (orientation === 'vertical') {
|
|
418
|
+
const labelsTop = labels === 'top';
|
|
419
|
+
return {
|
|
420
|
+
paddingLeft: showTicks ? VALUE_TICK_LEFT : TIGHT_GUTTER,
|
|
421
|
+
paddingRight: showTicks ? VALUE_TICK_END : PLOT_END_GUTTER,
|
|
422
|
+
paddingTop: labelsTop ? CATEGORY_EDGE_GUTTER : barLabel ? VALUES_TOP_GUTTER : TIGHT_GUTTER,
|
|
423
|
+
paddingBottom:
|
|
424
|
+
(labelsTop || labels === 'none' ? TIGHT_GUTTER : CATEGORY_EDGE_GUTTER) +
|
|
425
|
+
(hasNegative && barLabel ? NEGATIVE_VALUES_GUTTER : 0),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const labelsRight = labels === 'right';
|
|
429
|
+
const categoryPad =
|
|
430
|
+
labels === 'none' ? TIGHT_GUTTER : categoryLabelPadding(categories, orientation);
|
|
431
|
+
const valuesEnd =
|
|
432
|
+
barLabel && overlay ? OVERLAY_VALUES_END_GUTTER : barLabel ? VALUES_END_GUTTER : TIGHT_GUTTER;
|
|
433
|
+
return {
|
|
434
|
+
paddingLeft: labelsRight ? TIGHT_GUTTER : categoryPad,
|
|
435
|
+
paddingRight: labelsRight ? categoryPad : Math.max(valuesEnd, showTicks ? VALUE_TICK_END : 0),
|
|
436
|
+
paddingTop: TIGHT_GUTTER,
|
|
437
|
+
paddingBottom: showTicks ? CATEGORY_EDGE_GUTTER : TIGHT_GUTTER,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Gutters from rows with a string `label` field. `LineChart` lays out with the same gutters.
|
|
443
|
+
*/
|
|
444
|
+
export function barChartGutters(
|
|
445
|
+
data: readonly object[],
|
|
446
|
+
orientation: BarChartOrientation,
|
|
447
|
+
barLabel: boolean,
|
|
448
|
+
categoryLabelPosition?: BarChartCategoryLabelPosition,
|
|
449
|
+
showTicks = true,
|
|
450
|
+
overlay = false,
|
|
451
|
+
): BarChartGutters {
|
|
452
|
+
return resolveBarChartGutters({
|
|
453
|
+
categories: data.map((row) => {
|
|
454
|
+
const label = (row as Record<string, unknown>).label;
|
|
455
|
+
return typeof label === 'string' ? label : '';
|
|
456
|
+
}),
|
|
457
|
+
hasNegative: data.some((row) =>
|
|
458
|
+
Object.values(row).some((value) => typeof value === 'number' && value < 0),
|
|
459
|
+
),
|
|
460
|
+
orientation,
|
|
461
|
+
barLabel,
|
|
462
|
+
categoryLabelPosition,
|
|
463
|
+
showTicks,
|
|
464
|
+
overlay,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Viewport is the visible box (screen or parent). Content grows when there are
|
|
470
|
+
* too many categories to keep a readable band (`scrollable` / `visiblePoints`).
|
|
471
|
+
*/
|
|
472
|
+
export function resolveBarChartContentSize({
|
|
473
|
+
viewportWidth,
|
|
474
|
+
viewportHeight,
|
|
475
|
+
categoryCount,
|
|
476
|
+
orientation,
|
|
477
|
+
gutters,
|
|
478
|
+
scrollable,
|
|
479
|
+
visiblePoints,
|
|
480
|
+
}: {
|
|
481
|
+
readonly viewportWidth: number;
|
|
482
|
+
readonly viewportHeight: number;
|
|
483
|
+
readonly categoryCount: number;
|
|
484
|
+
readonly orientation: BarChartOrientation;
|
|
485
|
+
readonly gutters: BarChartGutters;
|
|
486
|
+
readonly scrollable: boolean;
|
|
487
|
+
readonly visiblePoints?: number;
|
|
488
|
+
}): { readonly width: number; readonly height: number } {
|
|
489
|
+
if (!scrollable || categoryCount <= 0) {
|
|
490
|
+
return { width: viewportWidth, height: viewportHeight };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (orientation === 'vertical') {
|
|
494
|
+
const plotWidth = Math.max(viewportWidth - gutters.paddingLeft - gutters.paddingRight, 0);
|
|
495
|
+
const band =
|
|
496
|
+
visiblePoints !== undefined && visiblePoints > 0
|
|
497
|
+
? plotWidth / visiblePoints
|
|
498
|
+
: MIN_CATEGORY_BAND;
|
|
499
|
+
const contentPlot = Math.max(plotWidth, categoryCount * band);
|
|
500
|
+
return {
|
|
501
|
+
width: contentPlot + gutters.paddingLeft + gutters.paddingRight,
|
|
502
|
+
height: viewportHeight,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const plotHeight = Math.max(viewportHeight - gutters.paddingTop - gutters.paddingBottom, 0);
|
|
507
|
+
const band =
|
|
508
|
+
visiblePoints !== undefined && visiblePoints > 0
|
|
509
|
+
? plotHeight / visiblePoints
|
|
510
|
+
: MIN_CATEGORY_BAND;
|
|
511
|
+
const contentPlot = Math.max(plotHeight, categoryCount * band);
|
|
512
|
+
return {
|
|
513
|
+
width: viewportWidth,
|
|
514
|
+
height: contentPlot + gutters.paddingTop + gutters.paddingBottom,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function groupedRect({
|
|
519
|
+
index,
|
|
520
|
+
seriesIndex,
|
|
521
|
+
slot,
|
|
522
|
+
item,
|
|
523
|
+
label,
|
|
524
|
+
value,
|
|
525
|
+
orientation,
|
|
526
|
+
band,
|
|
527
|
+
groupOffset,
|
|
528
|
+
thickness,
|
|
529
|
+
gap,
|
|
530
|
+
plotX,
|
|
531
|
+
plotY,
|
|
532
|
+
valueToX,
|
|
533
|
+
valueToY,
|
|
534
|
+
}: {
|
|
535
|
+
readonly index: number;
|
|
536
|
+
readonly seriesIndex: number;
|
|
537
|
+
readonly slot: number;
|
|
538
|
+
readonly item: BarGeometrySeries;
|
|
539
|
+
readonly label: string;
|
|
540
|
+
readonly value: number;
|
|
541
|
+
readonly orientation: BarChartOrientation;
|
|
542
|
+
readonly band: number;
|
|
543
|
+
readonly groupOffset: number;
|
|
544
|
+
readonly thickness: number;
|
|
545
|
+
readonly gap: number;
|
|
546
|
+
readonly plotX: number;
|
|
547
|
+
readonly plotY: number;
|
|
548
|
+
readonly valueToX: (value: number) => number;
|
|
549
|
+
readonly valueToY: (value: number) => number;
|
|
550
|
+
}): BarRect {
|
|
551
|
+
const zeroY = valueToY(0);
|
|
552
|
+
const zeroX = valueToX(0);
|
|
553
|
+
const base = {
|
|
554
|
+
index,
|
|
555
|
+
seriesId: item.id,
|
|
556
|
+
seriesIndex,
|
|
557
|
+
label,
|
|
558
|
+
seriesLabel: item.label,
|
|
559
|
+
value,
|
|
560
|
+
plotValue: value,
|
|
561
|
+
rounded: true,
|
|
562
|
+
};
|
|
563
|
+
if (orientation === 'vertical') {
|
|
564
|
+
const x = plotX + index * band + groupOffset + slot * (thickness + gap);
|
|
565
|
+
const y = value >= 0 ? valueToY(value) : zeroY;
|
|
566
|
+
const barHeight = Math.abs(valueToY(value) - zeroY);
|
|
567
|
+
return {
|
|
568
|
+
...base,
|
|
569
|
+
x: round2(x),
|
|
570
|
+
y: round2(y),
|
|
571
|
+
width: round2(Math.max(thickness, 0)),
|
|
572
|
+
height: round2(Math.max(barHeight, 0)),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const y = plotY + index * band + groupOffset + slot * (thickness + gap);
|
|
577
|
+
const x = value >= 0 ? zeroX : valueToX(value);
|
|
578
|
+
const barWidth = Math.abs(valueToX(value) - zeroX);
|
|
579
|
+
return {
|
|
580
|
+
...base,
|
|
581
|
+
x: round2(x),
|
|
582
|
+
y: round2(y),
|
|
583
|
+
width: round2(Math.max(barWidth, 0)),
|
|
584
|
+
height: round2(Math.max(thickness, 0)),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function overlayRect({
|
|
589
|
+
index,
|
|
590
|
+
seriesIndex,
|
|
591
|
+
item,
|
|
592
|
+
label,
|
|
593
|
+
value,
|
|
594
|
+
orientation,
|
|
595
|
+
band,
|
|
596
|
+
groupOffset,
|
|
597
|
+
inner,
|
|
598
|
+
plotX,
|
|
599
|
+
plotY,
|
|
600
|
+
valueToX,
|
|
601
|
+
valueToY,
|
|
602
|
+
}: {
|
|
603
|
+
readonly index: number;
|
|
604
|
+
readonly seriesIndex: number;
|
|
605
|
+
readonly item: BarGeometrySeries;
|
|
606
|
+
readonly label: string;
|
|
607
|
+
readonly value: number;
|
|
608
|
+
readonly orientation: BarChartOrientation;
|
|
609
|
+
readonly band: number;
|
|
610
|
+
readonly groupOffset: number;
|
|
611
|
+
readonly inner: number;
|
|
612
|
+
readonly plotX: number;
|
|
613
|
+
readonly plotY: number;
|
|
614
|
+
readonly valueToX: (value: number) => number;
|
|
615
|
+
readonly valueToY: (value: number) => number;
|
|
616
|
+
}): BarRect {
|
|
617
|
+
const isPrimary = seriesIndex === 0;
|
|
618
|
+
const primaryThickness = inner * OVERLAY_PRIMARY_RATIO;
|
|
619
|
+
const thickness = isPrimary ? primaryThickness : inner * OVERLAY_COMPARE_RATIO;
|
|
620
|
+
const slot = plotX + index * band + groupOffset;
|
|
621
|
+
const slotY = plotY + index * band + groupOffset;
|
|
622
|
+
const offset = isPrimary ? 0 : primaryThickness + inner * OVERLAY_SPACING_RATIO;
|
|
623
|
+
const zeroY = valueToY(0);
|
|
624
|
+
const zeroX = valueToX(0);
|
|
625
|
+
const base = {
|
|
626
|
+
index,
|
|
627
|
+
seriesId: item.id,
|
|
628
|
+
seriesIndex,
|
|
629
|
+
label,
|
|
630
|
+
seriesLabel: item.label,
|
|
631
|
+
value,
|
|
632
|
+
plotValue: value,
|
|
633
|
+
rounded: true,
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
if (orientation === 'vertical') {
|
|
637
|
+
const x = slot + offset;
|
|
638
|
+
const y = value >= 0 ? valueToY(value) : zeroY;
|
|
639
|
+
const barHeight = Math.abs(valueToY(value) - zeroY);
|
|
640
|
+
return {
|
|
641
|
+
...base,
|
|
642
|
+
x: round2(x),
|
|
643
|
+
y: round2(y),
|
|
644
|
+
width: round2(Math.max(thickness, 0)),
|
|
645
|
+
height: round2(Math.max(barHeight, 0)),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const y = slotY + offset;
|
|
650
|
+
const x = value >= 0 ? zeroX : valueToX(value);
|
|
651
|
+
const barWidth = Math.abs(valueToX(value) - zeroX);
|
|
652
|
+
return {
|
|
653
|
+
...base,
|
|
654
|
+
x: round2(x),
|
|
655
|
+
y: round2(y),
|
|
656
|
+
width: round2(Math.max(barWidth, 0)),
|
|
657
|
+
height: round2(Math.max(thickness, 0)),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function layoutStackedRow({
|
|
662
|
+
row,
|
|
663
|
+
label,
|
|
664
|
+
index,
|
|
665
|
+
group,
|
|
666
|
+
orientation,
|
|
667
|
+
band,
|
|
668
|
+
groupOffset,
|
|
669
|
+
thickness,
|
|
670
|
+
plotX,
|
|
671
|
+
plotY,
|
|
672
|
+
valueToX,
|
|
673
|
+
valueToY,
|
|
674
|
+
bars,
|
|
675
|
+
stackGap,
|
|
676
|
+
}: {
|
|
677
|
+
readonly row: object;
|
|
678
|
+
readonly label: string;
|
|
679
|
+
readonly index: number;
|
|
680
|
+
readonly group: BarGroup;
|
|
681
|
+
readonly orientation: BarChartOrientation;
|
|
682
|
+
readonly band: number;
|
|
683
|
+
readonly groupOffset: number;
|
|
684
|
+
readonly thickness: number;
|
|
685
|
+
readonly plotX: number;
|
|
686
|
+
readonly plotY: number;
|
|
687
|
+
readonly valueToX: (value: number) => number;
|
|
688
|
+
readonly valueToY: (value: number) => number;
|
|
689
|
+
readonly bars: BarRect[];
|
|
690
|
+
readonly stackGap: number;
|
|
691
|
+
}): void {
|
|
692
|
+
const offset = group.offset ?? 'diverging';
|
|
693
|
+
const shared = offset === 'none';
|
|
694
|
+
const segments = stackSegments(row, group.members, offset);
|
|
695
|
+
const lastPositiveId = shared
|
|
696
|
+
? segments.at(-1)?.item.id
|
|
697
|
+
: [...segments].reverse().find((segment) => segment.display >= 0)?.item.id;
|
|
698
|
+
const lastNegativeId = shared
|
|
699
|
+
? segments.at(-1)?.item.id
|
|
700
|
+
: [...segments].reverse().find((segment) => segment.display < 0)?.item.id;
|
|
701
|
+
const separated = stackGap > 0;
|
|
702
|
+
let positiveCursor = 0;
|
|
703
|
+
let negativeCursor = 0;
|
|
704
|
+
let positiveCount = 0;
|
|
705
|
+
let negativeCount = 0;
|
|
706
|
+
|
|
707
|
+
segments.forEach((segment) => {
|
|
708
|
+
const positive = segment.display >= 0;
|
|
709
|
+
const countBefore = shared
|
|
710
|
+
? positiveCount + negativeCount
|
|
711
|
+
: positive
|
|
712
|
+
? positiveCount
|
|
713
|
+
: negativeCount;
|
|
714
|
+
const gap = countBefore > 0 ? stackGap : 0;
|
|
715
|
+
if (positive) {
|
|
716
|
+
positiveCount += 1;
|
|
717
|
+
} else {
|
|
718
|
+
negativeCount += 1;
|
|
719
|
+
}
|
|
720
|
+
const rounded =
|
|
721
|
+
separated ||
|
|
722
|
+
(positive && segment.item.id === lastPositiveId) ||
|
|
723
|
+
(!positive && segment.item.id === lastNegativeId);
|
|
724
|
+
const from = shared || positive ? positiveCursor : negativeCursor;
|
|
725
|
+
const to = from + segment.display;
|
|
726
|
+
if (shared) {
|
|
727
|
+
positiveCursor = to;
|
|
728
|
+
negativeCursor = to;
|
|
729
|
+
} else if (positive) {
|
|
730
|
+
positiveCursor = to;
|
|
731
|
+
} else {
|
|
732
|
+
negativeCursor = to;
|
|
733
|
+
}
|
|
734
|
+
const base = {
|
|
735
|
+
index,
|
|
736
|
+
seriesId: segment.item.id,
|
|
737
|
+
seriesIndex: segment.seriesIndex,
|
|
738
|
+
label,
|
|
739
|
+
seriesLabel: segment.item.label,
|
|
740
|
+
value: segment.value,
|
|
741
|
+
plotValue: segment.display,
|
|
742
|
+
rounded,
|
|
743
|
+
roundAll: separated && countBefore > 0,
|
|
744
|
+
stackId: group.stackId,
|
|
745
|
+
...(offset === 'expand' ? { expand: true } : {}),
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
if (orientation === 'vertical') {
|
|
749
|
+
const top = valueToY(Math.max(from, to));
|
|
750
|
+
const bottom = valueToY(Math.min(from, to));
|
|
751
|
+
bars.push({
|
|
752
|
+
...base,
|
|
753
|
+
x: round2(plotX + index * band + groupOffset),
|
|
754
|
+
y: round2(positive ? top : top + gap),
|
|
755
|
+
width: round2(Math.max(thickness, 0)),
|
|
756
|
+
height: round2(Math.max(bottom - top - gap, 0)),
|
|
757
|
+
});
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const left = valueToX(Math.min(from, to));
|
|
762
|
+
const right = valueToX(Math.max(from, to));
|
|
763
|
+
bars.push({
|
|
764
|
+
...base,
|
|
765
|
+
x: round2(positive ? left + gap : left),
|
|
766
|
+
y: round2(plotY + index * band + groupOffset),
|
|
767
|
+
width: round2(Math.max(right - left - gap, 0)),
|
|
768
|
+
height: round2(Math.max(thickness, 0)),
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function waterfallRole(isTotal: boolean, delta: number): BarWaterfallRole {
|
|
774
|
+
if (isTotal) {
|
|
775
|
+
return 'total';
|
|
776
|
+
}
|
|
777
|
+
return delta >= 0 ? 'increase' : 'decrease';
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function layoutWaterfallBar({
|
|
781
|
+
item,
|
|
782
|
+
label,
|
|
783
|
+
index,
|
|
784
|
+
orientation,
|
|
785
|
+
band,
|
|
786
|
+
groupOffset,
|
|
787
|
+
thickness,
|
|
788
|
+
plotX,
|
|
789
|
+
plotY,
|
|
790
|
+
from,
|
|
791
|
+
to,
|
|
792
|
+
delta,
|
|
793
|
+
role,
|
|
794
|
+
valueToX,
|
|
795
|
+
valueToY,
|
|
796
|
+
}: {
|
|
797
|
+
readonly item: BarGeometrySeries;
|
|
798
|
+
readonly label: string;
|
|
799
|
+
readonly index: number;
|
|
800
|
+
readonly orientation: BarChartOrientation;
|
|
801
|
+
readonly band: number;
|
|
802
|
+
readonly groupOffset: number;
|
|
803
|
+
readonly thickness: number;
|
|
804
|
+
readonly plotX: number;
|
|
805
|
+
readonly plotY: number;
|
|
806
|
+
readonly from: number;
|
|
807
|
+
readonly to: number;
|
|
808
|
+
readonly delta: number;
|
|
809
|
+
readonly role: BarWaterfallRole;
|
|
810
|
+
readonly valueToX: (value: number) => number;
|
|
811
|
+
readonly valueToY: (value: number) => number;
|
|
812
|
+
}): BarRect {
|
|
813
|
+
const low = Math.min(from, to);
|
|
814
|
+
const high = Math.max(from, to);
|
|
815
|
+
const base = {
|
|
816
|
+
index,
|
|
817
|
+
seriesId: item.id,
|
|
818
|
+
seriesIndex: index,
|
|
819
|
+
label,
|
|
820
|
+
seriesLabel: item.label,
|
|
821
|
+
value: role === 'total' ? to : delta,
|
|
822
|
+
plotValue: to - from,
|
|
823
|
+
rounded: true,
|
|
824
|
+
// Totals sit on the axis, so only the free end is rounded.
|
|
825
|
+
roundAll: role !== 'total',
|
|
826
|
+
waterfallRole: role,
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
if (orientation === 'vertical') {
|
|
830
|
+
const top = valueToY(high);
|
|
831
|
+
const bottom = valueToY(low);
|
|
832
|
+
return {
|
|
833
|
+
...base,
|
|
834
|
+
x: round2(plotX + index * band + groupOffset),
|
|
835
|
+
y: round2(top),
|
|
836
|
+
width: round2(Math.max(thickness, 0)),
|
|
837
|
+
height: round2(Math.max(bottom - top, 0)),
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const left = valueToX(low);
|
|
842
|
+
const right = valueToX(high);
|
|
843
|
+
return {
|
|
844
|
+
...base,
|
|
845
|
+
x: round2(left),
|
|
846
|
+
y: round2(plotY + index * band + groupOffset),
|
|
847
|
+
width: round2(Math.max(right - left, 0)),
|
|
848
|
+
height: round2(Math.max(thickness, 0)),
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function waterfallConnector(
|
|
853
|
+
fromBar: BarRect,
|
|
854
|
+
toBar: BarRect,
|
|
855
|
+
value: number,
|
|
856
|
+
orientation: BarChartOrientation,
|
|
857
|
+
valueToX: (value: number) => number,
|
|
858
|
+
valueToY: (value: number) => number,
|
|
859
|
+
): BarGridLine {
|
|
860
|
+
if (orientation === 'vertical') {
|
|
861
|
+
const y = round2(valueToY(value));
|
|
862
|
+
return {
|
|
863
|
+
x1: round2(fromBar.x + fromBar.width),
|
|
864
|
+
y1: y,
|
|
865
|
+
x2: round2(toBar.x),
|
|
866
|
+
y2: y,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
const x = round2(valueToX(value));
|
|
870
|
+
return {
|
|
871
|
+
x1: x,
|
|
872
|
+
y1: round2(fromBar.y + fromBar.height),
|
|
873
|
+
x2: x,
|
|
874
|
+
y2: round2(toBar.y),
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
export type BarShape = Pick<
|
|
879
|
+
BarRect,
|
|
880
|
+
'x' | 'y' | 'width' | 'height' | 'rounded' | 'value' | 'roundAll'
|
|
881
|
+
>;
|
|
882
|
+
|
|
883
|
+
export function barPath(bar: BarShape, orientation: BarChartOrientation, radius: number): string {
|
|
884
|
+
'worklet';
|
|
885
|
+
const { x, y, width, height } = bar;
|
|
886
|
+
if (width <= 0 || height <= 0) {
|
|
887
|
+
return '';
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (!bar.rounded) {
|
|
891
|
+
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`;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (bar.roundAll) {
|
|
895
|
+
const r = Math.min(radius, width / 2, height / 2);
|
|
896
|
+
const x2 = x + width;
|
|
897
|
+
const y2 = y + height;
|
|
898
|
+
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`;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const r =
|
|
902
|
+
orientation === 'vertical'
|
|
903
|
+
? Math.min(radius, width / 2, height)
|
|
904
|
+
: Math.min(radius, height / 2, width);
|
|
905
|
+
const x2 = x + width;
|
|
906
|
+
const y2 = y + height;
|
|
907
|
+
const capPositive = bar.value >= 0;
|
|
908
|
+
|
|
909
|
+
if (orientation === 'vertical') {
|
|
910
|
+
if (capPositive) {
|
|
911
|
+
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`;
|
|
912
|
+
}
|
|
913
|
+
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`;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (capPositive) {
|
|
917
|
+
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`;
|
|
918
|
+
}
|
|
919
|
+
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`;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const EMPTY_LAYOUT: BarChartLayout = {
|
|
923
|
+
bars: [],
|
|
924
|
+
categoryLabels: [],
|
|
925
|
+
valueTicks: [],
|
|
926
|
+
gridLines: [],
|
|
927
|
+
referenceLines: [],
|
|
928
|
+
connectors: [],
|
|
929
|
+
baseline: 0,
|
|
930
|
+
plot: { x: 0, y: 0, width: 0, height: 0 },
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
/** True when every stack group uses `expand`, so the value axis is a 0–1 share axis. */
|
|
934
|
+
export function isExpandAxis(kind: BarChartKind, groups: readonly BarGroup[]): boolean {
|
|
935
|
+
return kind === 'bars' && groups.length > 0 && groups.every((group) => group.offset === 'expand');
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export function layoutBarChart({
|
|
939
|
+
dataset,
|
|
940
|
+
categories,
|
|
941
|
+
series,
|
|
942
|
+
kind = 'bars',
|
|
943
|
+
waterfallTotalKey,
|
|
944
|
+
width,
|
|
945
|
+
height,
|
|
946
|
+
orientation,
|
|
947
|
+
barLabel,
|
|
948
|
+
categoryLabelPosition,
|
|
949
|
+
showTicks = true,
|
|
950
|
+
referenceLines = [],
|
|
951
|
+
valueAxis,
|
|
952
|
+
formatTick = formatBarValue,
|
|
953
|
+
stackGap = DEFAULT_STACK_GAP,
|
|
954
|
+
categoryGapRatio = CATEGORY_GAP_RATIO,
|
|
955
|
+
barGapRatio = BAR_GAP_RATIO,
|
|
956
|
+
showCategoryLabel,
|
|
957
|
+
}: {
|
|
958
|
+
readonly dataset: readonly object[];
|
|
959
|
+
/** One name per row, from the category axis. Defaults to the row index. */
|
|
960
|
+
readonly categories?: readonly string[];
|
|
961
|
+
readonly series: readonly BarGeometrySeries[];
|
|
962
|
+
readonly kind?: BarChartKind;
|
|
963
|
+
/** Waterfall: row key whose `true` value marks a total bar. */
|
|
964
|
+
readonly waterfallTotalKey?: string;
|
|
965
|
+
readonly width: number;
|
|
966
|
+
readonly height: number;
|
|
967
|
+
readonly orientation: BarChartOrientation;
|
|
968
|
+
readonly barLabel: boolean;
|
|
969
|
+
readonly categoryLabelPosition?: BarChartCategoryLabelPosition;
|
|
970
|
+
readonly showTicks?: boolean;
|
|
971
|
+
readonly referenceLines?: readonly BarChartReferenceLine[];
|
|
972
|
+
readonly valueAxis?: BarValueAxisConfig;
|
|
973
|
+
/** Tick text. Receives a 0–1 share on an `expand` axis. */
|
|
974
|
+
readonly formatTick?: (value: number) => string;
|
|
975
|
+
readonly stackGap?: number;
|
|
976
|
+
readonly categoryGapRatio?: number;
|
|
977
|
+
readonly barGapRatio?: number;
|
|
978
|
+
/** Keeps the category name at `index` when it returns `true`. Others draw no text. */
|
|
979
|
+
readonly showCategoryLabel?: (index: number) => boolean;
|
|
980
|
+
}): BarChartLayout {
|
|
981
|
+
if (width <= 0 || height <= 0 || dataset.length === 0 || series.length === 0) {
|
|
982
|
+
return EMPTY_LAYOUT;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const categoryOf = (index: number) => categories?.[index] ?? String(index);
|
|
986
|
+
const resolvedCategories = dataset.map((_, index) => categoryOf(index));
|
|
987
|
+
const overlay = kind === 'overlay';
|
|
988
|
+
const waterfall = kind === 'waterfall';
|
|
989
|
+
const groups = resolveBarGroups(series);
|
|
990
|
+
const expand = isExpandAxis(kind, groups);
|
|
991
|
+
const labels = resolveCategoryLabelPosition(orientation, categoryLabelPosition);
|
|
992
|
+
const { paddingLeft, paddingRight, paddingTop, paddingBottom } = resolveBarChartGutters({
|
|
993
|
+
categories: resolvedCategories,
|
|
994
|
+
hasNegative: dataset.some((row) =>
|
|
995
|
+
series.some((item) => (readBarValue(row, item.dataKey) ?? 0) < 0),
|
|
996
|
+
),
|
|
997
|
+
orientation,
|
|
998
|
+
barLabel,
|
|
999
|
+
categoryLabelPosition: labels,
|
|
1000
|
+
showTicks,
|
|
1001
|
+
overlay,
|
|
1002
|
+
});
|
|
1003
|
+
const plot = {
|
|
1004
|
+
x: paddingLeft,
|
|
1005
|
+
y: paddingTop,
|
|
1006
|
+
width: Math.max(width - paddingLeft - paddingRight, 0),
|
|
1007
|
+
height: Math.max(height - paddingTop - paddingBottom, 0),
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
const waterfallSeries = series.at(0);
|
|
1011
|
+
const dataValues = waterfall
|
|
1012
|
+
? collectWaterfallTotals(dataset, waterfallSeries?.dataKey ?? '', waterfallTotalKey)
|
|
1013
|
+
: overlay
|
|
1014
|
+
? groupDomainValues(dataset, {
|
|
1015
|
+
stackId: undefined,
|
|
1016
|
+
offset: null,
|
|
1017
|
+
members: series.map((item, seriesIndex) => ({ item, seriesIndex })),
|
|
1018
|
+
})
|
|
1019
|
+
: groups.flatMap((group) => groupDomainValues(dataset, group));
|
|
1020
|
+
const values = [
|
|
1021
|
+
...dataValues,
|
|
1022
|
+
...referenceLines.flatMap((line) => (Number.isFinite(line.value) ? [line.value] : [])),
|
|
1023
|
+
];
|
|
1024
|
+
const scale = resolveBarValueScale(values, valueAxis, expand);
|
|
1025
|
+
const domainMin = scale.min;
|
|
1026
|
+
const domainMax = scale.max;
|
|
1027
|
+
const span = domainMax - domainMin || 1;
|
|
1028
|
+
const categoryCount = dataset.length;
|
|
1029
|
+
const slotCount = overlay || waterfall ? 1 : groups.length;
|
|
1030
|
+
const band =
|
|
1031
|
+
orientation === 'vertical' ? plot.width / categoryCount : plot.height / categoryCount;
|
|
1032
|
+
const gapShare = clamp(
|
|
1033
|
+
Number.isFinite(categoryGapRatio) ? categoryGapRatio : CATEGORY_GAP_RATIO,
|
|
1034
|
+
0,
|
|
1035
|
+
0.95,
|
|
1036
|
+
);
|
|
1037
|
+
const inner = band * (1 - gapShare);
|
|
1038
|
+
const barGap = Math.max(Number.isFinite(barGapRatio) ? barGapRatio : BAR_GAP_RATIO, 0);
|
|
1039
|
+
const gapSlots = Math.max(slotCount - 1, 0);
|
|
1040
|
+
const thickness = inner / (slotCount + gapSlots * barGap);
|
|
1041
|
+
const gap = gapSlots > 0 ? thickness * barGap : 0;
|
|
1042
|
+
const groupOffset = (band - inner) / 2;
|
|
1043
|
+
|
|
1044
|
+
const toPlot = (value: number) => (clamp(value, domainMin, domainMax) - domainMin) / span;
|
|
1045
|
+
const valueToY = (value: number) => plot.y + plot.height * (1 - toPlot(value));
|
|
1046
|
+
const valueToX = (value: number) => plot.x + plot.width * toPlot(value);
|
|
1047
|
+
|
|
1048
|
+
const bars: BarRect[] = [];
|
|
1049
|
+
const categoryLabels: BarLabelAnchor[] = [];
|
|
1050
|
+
const connectors: BarGridLine[] = [];
|
|
1051
|
+
let waterfallCursor = 0;
|
|
1052
|
+
let waterfallPrev: { readonly bar: BarRect; readonly total: number } | null = null;
|
|
1053
|
+
|
|
1054
|
+
dataset.forEach((row, index) => {
|
|
1055
|
+
const label = categoryOf(index);
|
|
1056
|
+
const shownLabel = showCategoryLabel === undefined || showCategoryLabel(index) ? label : '';
|
|
1057
|
+
// With `none`, category names still reach the tooltip and screen readers through the bars.
|
|
1058
|
+
if (labels !== 'none' && orientation === 'vertical') {
|
|
1059
|
+
const center = plot.x + index * band + band / 2;
|
|
1060
|
+
const halfWidth = (shownLabel.length * CATEGORY_LABEL_CHAR_WIDTH) / 2;
|
|
1061
|
+
const anchor =
|
|
1062
|
+
center - halfWidth < plot.x
|
|
1063
|
+
? { x: plot.x, textAnchor: 'start' as const }
|
|
1064
|
+
: center + halfWidth > plot.x + plot.width
|
|
1065
|
+
? { x: plot.x + plot.width, textAnchor: 'end' as const }
|
|
1066
|
+
: { x: center, textAnchor: 'middle' as const };
|
|
1067
|
+
categoryLabels.push({
|
|
1068
|
+
label: shownLabel,
|
|
1069
|
+
x: round2(anchor.x),
|
|
1070
|
+
y: round2(labels === 'top' ? paddingTop / 2 : height - 8),
|
|
1071
|
+
textAnchor: anchor.textAnchor,
|
|
1072
|
+
});
|
|
1073
|
+
} else if (labels !== 'none') {
|
|
1074
|
+
categoryLabels.push({
|
|
1075
|
+
label: shownLabel,
|
|
1076
|
+
x: round2(labels === 'right' ? plot.x + plot.width + 8 : plot.x - 8),
|
|
1077
|
+
y: round2(plot.y + index * band + band / 2),
|
|
1078
|
+
textAnchor: labels === 'right' ? 'start' : 'end',
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (waterfall) {
|
|
1083
|
+
if (waterfallSeries === undefined) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const isTotal = readBarFlag(row, waterfallTotalKey);
|
|
1087
|
+
const raw = readBarValue(row, waterfallSeries.dataKey);
|
|
1088
|
+
if (!isTotal && raw === null) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const from = isTotal ? 0 : waterfallCursor;
|
|
1092
|
+
if (isTotal) {
|
|
1093
|
+
if (raw !== null && index === 0) {
|
|
1094
|
+
waterfallCursor = raw;
|
|
1095
|
+
}
|
|
1096
|
+
} else if (raw !== null) {
|
|
1097
|
+
waterfallCursor += raw;
|
|
1098
|
+
}
|
|
1099
|
+
const to = waterfallCursor;
|
|
1100
|
+
const delta = isTotal ? to : (raw ?? 0);
|
|
1101
|
+
const bar = layoutWaterfallBar({
|
|
1102
|
+
item: waterfallSeries,
|
|
1103
|
+
label,
|
|
1104
|
+
index,
|
|
1105
|
+
orientation,
|
|
1106
|
+
band,
|
|
1107
|
+
groupOffset,
|
|
1108
|
+
thickness,
|
|
1109
|
+
plotX: plot.x,
|
|
1110
|
+
plotY: plot.y,
|
|
1111
|
+
from,
|
|
1112
|
+
to,
|
|
1113
|
+
delta,
|
|
1114
|
+
role: waterfallRole(isTotal, delta),
|
|
1115
|
+
valueToX,
|
|
1116
|
+
valueToY,
|
|
1117
|
+
});
|
|
1118
|
+
if (waterfallPrev !== null) {
|
|
1119
|
+
connectors.push(
|
|
1120
|
+
waterfallConnector(
|
|
1121
|
+
waterfallPrev.bar,
|
|
1122
|
+
bar,
|
|
1123
|
+
waterfallPrev.total,
|
|
1124
|
+
orientation,
|
|
1125
|
+
valueToX,
|
|
1126
|
+
valueToY,
|
|
1127
|
+
),
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
bars.push(bar);
|
|
1131
|
+
waterfallPrev = { bar, total: to };
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (overlay) {
|
|
1136
|
+
series.forEach((item, seriesIndex) => {
|
|
1137
|
+
const value = readBarValue(row, item.dataKey);
|
|
1138
|
+
if (value === null) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
bars.push(
|
|
1142
|
+
overlayRect({
|
|
1143
|
+
index,
|
|
1144
|
+
seriesIndex,
|
|
1145
|
+
item,
|
|
1146
|
+
label,
|
|
1147
|
+
value,
|
|
1148
|
+
orientation,
|
|
1149
|
+
band,
|
|
1150
|
+
groupOffset,
|
|
1151
|
+
inner,
|
|
1152
|
+
plotX: plot.x,
|
|
1153
|
+
plotY: plot.y,
|
|
1154
|
+
valueToX,
|
|
1155
|
+
valueToY,
|
|
1156
|
+
}),
|
|
1157
|
+
);
|
|
1158
|
+
});
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
groups.forEach((group, slot) => {
|
|
1163
|
+
const slotOffset = groupOffset + slot * (thickness + gap);
|
|
1164
|
+
if (group.stackId !== undefined) {
|
|
1165
|
+
layoutStackedRow({
|
|
1166
|
+
row,
|
|
1167
|
+
label,
|
|
1168
|
+
index,
|
|
1169
|
+
group,
|
|
1170
|
+
orientation,
|
|
1171
|
+
band,
|
|
1172
|
+
groupOffset: slotOffset,
|
|
1173
|
+
thickness,
|
|
1174
|
+
plotX: plot.x,
|
|
1175
|
+
plotY: plot.y,
|
|
1176
|
+
valueToX,
|
|
1177
|
+
valueToY,
|
|
1178
|
+
bars,
|
|
1179
|
+
stackGap: Math.max(stackGap, 0),
|
|
1180
|
+
});
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
const member = group.members.at(0);
|
|
1184
|
+
if (member === undefined) {
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const value = readBarValue(row, member.item.dataKey);
|
|
1188
|
+
if (value === null) {
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
bars.push(
|
|
1192
|
+
groupedRect({
|
|
1193
|
+
index,
|
|
1194
|
+
seriesIndex: member.seriesIndex,
|
|
1195
|
+
slot,
|
|
1196
|
+
item: member.item,
|
|
1197
|
+
label,
|
|
1198
|
+
value,
|
|
1199
|
+
orientation,
|
|
1200
|
+
band,
|
|
1201
|
+
groupOffset,
|
|
1202
|
+
thickness,
|
|
1203
|
+
gap,
|
|
1204
|
+
plotX: plot.x,
|
|
1205
|
+
plotY: plot.y,
|
|
1206
|
+
valueToX,
|
|
1207
|
+
valueToY,
|
|
1208
|
+
}),
|
|
1209
|
+
);
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
const tickText = (value: number) => formatTick(expand ? value / 100 : value);
|
|
1214
|
+
const valueTicks: BarValueTick[] = scale.ticks.map((value) => {
|
|
1215
|
+
if (orientation === 'vertical') {
|
|
1216
|
+
return {
|
|
1217
|
+
value,
|
|
1218
|
+
x: round2(plot.x - 8),
|
|
1219
|
+
y: round2(valueToY(value)),
|
|
1220
|
+
text: tickText(value),
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
return {
|
|
1224
|
+
value,
|
|
1225
|
+
x: round2(valueToX(value)),
|
|
1226
|
+
y: round2(height - 8),
|
|
1227
|
+
text: tickText(value),
|
|
1228
|
+
};
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
const gridLines: BarGridLine[] = scale.ticks.map((value) => {
|
|
1232
|
+
if (orientation === 'vertical') {
|
|
1233
|
+
const y = round2(valueToY(value));
|
|
1234
|
+
return { x1: round2(plot.x), y1: y, x2: round2(plot.x + plot.width), y2: y };
|
|
1235
|
+
}
|
|
1236
|
+
const x = round2(valueToX(value));
|
|
1237
|
+
return { x1: x, y1: round2(plot.y), x2: x, y2: round2(plot.y + plot.height) };
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
const zero = clamp(0, domainMin, domainMax);
|
|
1241
|
+
const baseline = round2(orientation === 'vertical' ? valueToY(zero) : valueToX(zero));
|
|
1242
|
+
|
|
1243
|
+
const laidReferenceLines: BarReferenceLineLayout[] = referenceLines.flatMap((line) => {
|
|
1244
|
+
if (!Number.isFinite(line.value) || line.value < domainMin || line.value > domainMax) {
|
|
1245
|
+
return [];
|
|
1246
|
+
}
|
|
1247
|
+
if (orientation === 'vertical') {
|
|
1248
|
+
const y = round2(valueToY(line.value));
|
|
1249
|
+
return [
|
|
1250
|
+
{
|
|
1251
|
+
x1: round2(plot.x),
|
|
1252
|
+
y1: y,
|
|
1253
|
+
x2: round2(plot.x + plot.width),
|
|
1254
|
+
y2: y,
|
|
1255
|
+
label: line.label,
|
|
1256
|
+
},
|
|
1257
|
+
];
|
|
1258
|
+
}
|
|
1259
|
+
const x = round2(valueToX(line.value));
|
|
1260
|
+
return [
|
|
1261
|
+
{
|
|
1262
|
+
x1: x,
|
|
1263
|
+
y1: round2(plot.y),
|
|
1264
|
+
x2: x,
|
|
1265
|
+
y2: round2(plot.y + plot.height),
|
|
1266
|
+
label: line.label,
|
|
1267
|
+
},
|
|
1268
|
+
];
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
return {
|
|
1272
|
+
bars,
|
|
1273
|
+
categoryLabels,
|
|
1274
|
+
valueTicks,
|
|
1275
|
+
gridLines,
|
|
1276
|
+
referenceLines: laidReferenceLines,
|
|
1277
|
+
connectors,
|
|
1278
|
+
baseline,
|
|
1279
|
+
plot,
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
export function overlayDelta(bars: readonly BarRect[], index: number): number | null {
|
|
1284
|
+
const group = bars.filter((bar) => bar.index === index);
|
|
1285
|
+
const primary = group.find((bar) => bar.seriesIndex === 0);
|
|
1286
|
+
const comparison = group.find((bar) => bar.seriesIndex === 1);
|
|
1287
|
+
if (primary === undefined || comparison === undefined) {
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
return round2(primary.value - comparison.value);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/** Waterfall steps read as signed changes; totals read as the running total. */
|
|
1294
|
+
export function formatWaterfallBarValue(
|
|
1295
|
+
bar: Pick<BarRect, 'value' | 'waterfallRole'>,
|
|
1296
|
+
formatValue: (value: number) => string,
|
|
1297
|
+
): string {
|
|
1298
|
+
if (bar.waterfallRole === 'increase') {
|
|
1299
|
+
return `+${formatValue(bar.value)}`;
|
|
1300
|
+
}
|
|
1301
|
+
if (bar.waterfallRole === 'decrease') {
|
|
1302
|
+
return `-${formatValue(Math.abs(bar.value))}`;
|
|
1303
|
+
}
|
|
1304
|
+
return formatValue(bar.value);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
export function waterfallValueAnchor(
|
|
1308
|
+
bar: BarRect,
|
|
1309
|
+
orientation: BarChartOrientation,
|
|
1310
|
+
): { readonly x: number; readonly y: number; readonly textAnchor: 'start' | 'middle' | 'end' } {
|
|
1311
|
+
if (orientation === 'vertical') {
|
|
1312
|
+
return {
|
|
1313
|
+
x: round2(bar.x + bar.width / 2),
|
|
1314
|
+
y: round2(bar.y - 4),
|
|
1315
|
+
textAnchor: 'middle',
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
return {
|
|
1319
|
+
x: round2(bar.x + bar.width + 4),
|
|
1320
|
+
y: round2(bar.y + bar.height / 2),
|
|
1321
|
+
textAnchor: 'start',
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
export function barValueAnchor(
|
|
1326
|
+
bar: BarRect,
|
|
1327
|
+
orientation: BarChartOrientation,
|
|
1328
|
+
): { readonly x: number; readonly y: number; readonly textAnchor: 'start' | 'middle' | 'end' } {
|
|
1329
|
+
if (orientation === 'vertical') {
|
|
1330
|
+
return {
|
|
1331
|
+
x: round2(bar.x + bar.width / 2),
|
|
1332
|
+
y: round2(bar.plotValue >= 0 ? Math.max(bar.y - 12, 12) : bar.y + bar.height + 12),
|
|
1333
|
+
textAnchor: 'middle',
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
return {
|
|
1337
|
+
x: round2(bar.plotValue >= 0 ? bar.x + bar.width + 4 : bar.x - 4),
|
|
1338
|
+
y: round2(bar.y + bar.height / 2),
|
|
1339
|
+
textAnchor: bar.plotValue >= 0 ? 'start' : 'end',
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
export interface BarStackTotalAnchor {
|
|
1344
|
+
readonly key: string;
|
|
1345
|
+
readonly index: number;
|
|
1346
|
+
readonly x: number;
|
|
1347
|
+
readonly y: number;
|
|
1348
|
+
readonly textAnchor: 'start' | 'middle' | 'end';
|
|
1349
|
+
/** Sum of the stack's plotted values (a 0–100 share total when `expand`). */
|
|
1350
|
+
readonly total: number;
|
|
1351
|
+
readonly expand: boolean;
|
|
1352
|
+
/** Series of the outer segment. Its formatter labels the total. */
|
|
1353
|
+
readonly seriesId: string;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/** One total label per stack in each category. */
|
|
1357
|
+
export function stackedTotalAnchors(
|
|
1358
|
+
bars: readonly BarRect[],
|
|
1359
|
+
orientation: BarChartOrientation,
|
|
1360
|
+
): readonly BarStackTotalAnchor[] {
|
|
1361
|
+
const byStack = new Map<string, BarRect[]>();
|
|
1362
|
+
bars.forEach((bar) => {
|
|
1363
|
+
if (bar.stackId === undefined) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
const key = `${String(bar.index)}:${bar.stackId}`;
|
|
1367
|
+
const group = byStack.get(key) ?? [];
|
|
1368
|
+
group.push(bar);
|
|
1369
|
+
byStack.set(key, group);
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
return [...byStack.entries()].flatMap(([key, group]) => {
|
|
1373
|
+
const outer = group.filter((bar) => bar.plotValue >= 0).at(-1) ?? group.at(-1);
|
|
1374
|
+
if (outer === undefined) {
|
|
1375
|
+
return [];
|
|
1376
|
+
}
|
|
1377
|
+
const total = group.reduce((sum, bar) => sum + bar.plotValue, 0);
|
|
1378
|
+
const anchor = barValueAnchor({ ...outer, plotValue: total, value: total }, orientation);
|
|
1379
|
+
return [
|
|
1380
|
+
{
|
|
1381
|
+
key,
|
|
1382
|
+
index: outer.index,
|
|
1383
|
+
...anchor,
|
|
1384
|
+
total,
|
|
1385
|
+
expand: outer.expand === true,
|
|
1386
|
+
seriesId: outer.seriesId,
|
|
1387
|
+
},
|
|
1388
|
+
];
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Next bar for keyboard movement. Along the category axis keeps the same series; along the
|
|
1394
|
+
* value axis steps series inside the category. Home / End jump the category axis.
|
|
1395
|
+
*/
|
|
1396
|
+
export function barKeyboardNeighbor(
|
|
1397
|
+
bars: readonly Pick<BarRect, 'index' | 'seriesIndex'>[],
|
|
1398
|
+
from: number,
|
|
1399
|
+
key: string,
|
|
1400
|
+
orientation: BarChartOrientation,
|
|
1401
|
+
): number | null {
|
|
1402
|
+
const current = bars[from];
|
|
1403
|
+
if (current === undefined) {
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
const categories = [...new Set(bars.map((bar) => bar.index))];
|
|
1407
|
+
const categoryPos = categories.indexOf(current.index);
|
|
1408
|
+
const seriesInCategory = bars
|
|
1409
|
+
.map((bar, index) => ({ bar, index }))
|
|
1410
|
+
.filter((item) => item.bar.index === current.index);
|
|
1411
|
+
const seriesPos = seriesInCategory.findIndex((item) => item.index === from);
|
|
1412
|
+
const alongCategory =
|
|
1413
|
+
key === 'Tab'
|
|
1414
|
+
? 1
|
|
1415
|
+
: key === 'Shift+Tab'
|
|
1416
|
+
? -1
|
|
1417
|
+
: orientation === 'vertical'
|
|
1418
|
+
? key === 'ArrowRight' || key === 'End'
|
|
1419
|
+
? 1
|
|
1420
|
+
: key === 'ArrowLeft' || key === 'Home'
|
|
1421
|
+
? -1
|
|
1422
|
+
: 0
|
|
1423
|
+
: key === 'ArrowDown' || key === 'End'
|
|
1424
|
+
? 1
|
|
1425
|
+
: key === 'ArrowUp' || key === 'Home'
|
|
1426
|
+
? -1
|
|
1427
|
+
: 0;
|
|
1428
|
+
const alongSeries =
|
|
1429
|
+
orientation === 'vertical'
|
|
1430
|
+
? key === 'ArrowDown'
|
|
1431
|
+
? 1
|
|
1432
|
+
: key === 'ArrowUp'
|
|
1433
|
+
? -1
|
|
1434
|
+
: 0
|
|
1435
|
+
: key === 'ArrowRight'
|
|
1436
|
+
? 1
|
|
1437
|
+
: key === 'ArrowLeft'
|
|
1438
|
+
? -1
|
|
1439
|
+
: 0;
|
|
1440
|
+
|
|
1441
|
+
if (key === 'Home' || key === 'End') {
|
|
1442
|
+
const nextCategory = alongCategory > 0 ? categories.at(-1) : categories[0];
|
|
1443
|
+
if (nextCategory === undefined) {
|
|
1444
|
+
return from;
|
|
1445
|
+
}
|
|
1446
|
+
const match = bars.findIndex(
|
|
1447
|
+
(bar) => bar.index === nextCategory && bar.seriesIndex === current.seriesIndex,
|
|
1448
|
+
);
|
|
1449
|
+
return match >= 0 ? match : bars.findIndex((bar) => bar.index === nextCategory);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if (alongCategory !== 0) {
|
|
1453
|
+
const nextCategory = categories[categoryPos + alongCategory];
|
|
1454
|
+
if (nextCategory === undefined) {
|
|
1455
|
+
return from;
|
|
1456
|
+
}
|
|
1457
|
+
const match = bars.findIndex(
|
|
1458
|
+
(bar) => bar.index === nextCategory && bar.seriesIndex === current.seriesIndex,
|
|
1459
|
+
);
|
|
1460
|
+
return match >= 0 ? match : bars.findIndex((bar) => bar.index === nextCategory);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
if (alongSeries !== 0) {
|
|
1464
|
+
const next = seriesInCategory[seriesPos + alongSeries];
|
|
1465
|
+
return next === undefined ? from : next.index;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|