@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,957 @@
|
|
|
1
|
+
import { niceTicks } from '../chart/ticks';
|
|
2
|
+
import type { ChartAxis, ChartReferenceLine, ChartSeriesBase } from '../chart/types';
|
|
3
|
+
import type { ChartColor } from '../chart/palette';
|
|
4
|
+
|
|
5
|
+
/** How points connect. Names match MUI X `curve`. */
|
|
6
|
+
export type LineChartCurve = 'linear' | 'monotoneX' | 'stepAfter';
|
|
7
|
+
|
|
8
|
+
export interface LineChartSeries<TRow extends object> extends ChartSeriesBase<TRow> {
|
|
9
|
+
/** Series type. Optional on `LineChart`; reserved for charts that mix bars and lines. */
|
|
10
|
+
type?: 'line';
|
|
11
|
+
/**
|
|
12
|
+
* How points connect. `stepAfter` holds each value until the next point.
|
|
13
|
+
* @default 'monotoneX'
|
|
14
|
+
*/
|
|
15
|
+
curve?: LineChartCurve;
|
|
16
|
+
/**
|
|
17
|
+
* Fills under the line toward zero (or the domain edge nearest it) with a fade of the
|
|
18
|
+
* series color.
|
|
19
|
+
* @default false
|
|
20
|
+
*/
|
|
21
|
+
area?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Draws a mark on each defined point. A function picks points by `index` (the `dataset` row).
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
showMark?: boolean | ((params: { index: number }) => boolean);
|
|
27
|
+
/**
|
|
28
|
+
* Dashes the whole line.
|
|
29
|
+
* @default false
|
|
30
|
+
*/
|
|
31
|
+
dashed?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* `dataset` row where the line switches from solid to dashed (e.g. actual → forecast). The
|
|
34
|
+
* curve still uses every point, so the join stays smooth.
|
|
35
|
+
*/
|
|
36
|
+
dashedFrom?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LineChartMarker {
|
|
40
|
+
/** `dataset` row to pin to. Rows with the same category label resolve independently. */
|
|
41
|
+
dataIndex: number;
|
|
42
|
+
/** Series to pin to. Defaults to the first series with a value at `dataIndex`. */
|
|
43
|
+
seriesId?: string;
|
|
44
|
+
/** Caption above the point, e.g. "Rent". */
|
|
45
|
+
label?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Draw the formatted value beside the point.
|
|
48
|
+
* @default false
|
|
49
|
+
*/
|
|
50
|
+
showValue?: boolean;
|
|
51
|
+
/** Chart token for the dot. Defaults to that series' color. */
|
|
52
|
+
color?: ChartColor;
|
|
53
|
+
/** Extra legend row, e.g. "Today". */
|
|
54
|
+
legend?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Vertical dashed guide through the point.
|
|
57
|
+
* @default true
|
|
58
|
+
*/
|
|
59
|
+
guide?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Filled dot on the line.
|
|
62
|
+
* @default true
|
|
63
|
+
*/
|
|
64
|
+
dot?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface LineCategory {
|
|
68
|
+
/** Axis label. */
|
|
69
|
+
readonly label: string;
|
|
70
|
+
/** Tooltip and screen reader name. */
|
|
71
|
+
readonly title: string;
|
|
72
|
+
readonly tick: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface LineSeriesInput {
|
|
76
|
+
readonly id: string;
|
|
77
|
+
readonly label: string;
|
|
78
|
+
readonly curve: LineChartCurve;
|
|
79
|
+
readonly area: boolean;
|
|
80
|
+
readonly dashed: boolean;
|
|
81
|
+
/** Row where the dash starts, or `-1`. */
|
|
82
|
+
readonly dashAt: number;
|
|
83
|
+
readonly values: readonly (number | null)[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface LineDomainOptions {
|
|
87
|
+
readonly min?: number;
|
|
88
|
+
readonly max?: number;
|
|
89
|
+
readonly tickNumber?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface LineChartGutters {
|
|
93
|
+
readonly paddingLeft: number;
|
|
94
|
+
readonly paddingRight: number;
|
|
95
|
+
readonly paddingTop: number;
|
|
96
|
+
readonly paddingBottom: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface LineChartMarkerLayout {
|
|
100
|
+
readonly key: string;
|
|
101
|
+
readonly index: number;
|
|
102
|
+
readonly seriesIndex: number;
|
|
103
|
+
readonly seriesId: string;
|
|
104
|
+
readonly x: number;
|
|
105
|
+
readonly y: number;
|
|
106
|
+
readonly value: number;
|
|
107
|
+
readonly label?: string;
|
|
108
|
+
readonly showValue: boolean;
|
|
109
|
+
readonly color?: ChartColor;
|
|
110
|
+
readonly legend?: string;
|
|
111
|
+
readonly guide: boolean;
|
|
112
|
+
readonly dot: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface LinePoint {
|
|
116
|
+
readonly index: number;
|
|
117
|
+
readonly value: number | null;
|
|
118
|
+
readonly x: number;
|
|
119
|
+
readonly y: number | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface LineSeriesLayout {
|
|
123
|
+
readonly key: string;
|
|
124
|
+
readonly label: string;
|
|
125
|
+
readonly curve: LineChartCurve;
|
|
126
|
+
readonly area: boolean;
|
|
127
|
+
readonly dashed: boolean;
|
|
128
|
+
readonly path: string;
|
|
129
|
+
readonly dashedPath: string;
|
|
130
|
+
readonly dashAt: number;
|
|
131
|
+
readonly areaPath: string;
|
|
132
|
+
readonly points: readonly LinePoint[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface LineLabelAnchor {
|
|
136
|
+
readonly label: string;
|
|
137
|
+
readonly title: string;
|
|
138
|
+
readonly x: number;
|
|
139
|
+
readonly y: number;
|
|
140
|
+
readonly tick: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface LineValueTick {
|
|
144
|
+
readonly value: number;
|
|
145
|
+
readonly x: number;
|
|
146
|
+
readonly y: number;
|
|
147
|
+
readonly text: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface LineGridLine {
|
|
151
|
+
readonly x1: number;
|
|
152
|
+
readonly y1: number;
|
|
153
|
+
readonly x2: number;
|
|
154
|
+
readonly y2: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface LineChartLayout {
|
|
158
|
+
readonly series: readonly LineSeriesLayout[];
|
|
159
|
+
readonly categoryLabels: readonly LineLabelAnchor[];
|
|
160
|
+
readonly valueTicks: readonly LineValueTick[];
|
|
161
|
+
readonly gridLines: readonly LineGridLine[];
|
|
162
|
+
readonly referenceLines: readonly (LineGridLine & { readonly label?: string })[];
|
|
163
|
+
/** Where areas close: zero, or the domain edge nearest it. Lines rise from here on entry. */
|
|
164
|
+
readonly baselineY: number;
|
|
165
|
+
readonly plot: {
|
|
166
|
+
readonly x: number;
|
|
167
|
+
readonly y: number;
|
|
168
|
+
readonly width: number;
|
|
169
|
+
readonly height: number;
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const MIN_LINE_POINT_BAND = 32;
|
|
174
|
+
/** Max distance from a series stroke (overlay px) that still counts as hovering the line. */
|
|
175
|
+
export const LINE_POINTER_HIT_PX = 20;
|
|
176
|
+
/** Extra radius around a plotted point (dots / markers) so a valley like Lowest is easy to hit. */
|
|
177
|
+
export const LINE_POINT_HIT_PX = 28;
|
|
178
|
+
/** Extra plot padding when a marker draws a caption or value. */
|
|
179
|
+
export const LINE_MARKER_PADDING_TOP = 28;
|
|
180
|
+
export const LINE_MARKER_PADDING_RIGHT = 36;
|
|
181
|
+
export const DEFAULT_LINE_TICK_NUMBER = 4;
|
|
182
|
+
|
|
183
|
+
const VALUE_TICK_LEFT = 40;
|
|
184
|
+
const VALUE_TICK_END = 28;
|
|
185
|
+
const TIGHT_GUTTER = 8;
|
|
186
|
+
const PLOT_END_GUTTER = 20;
|
|
187
|
+
const CATEGORY_GUTTER = 28;
|
|
188
|
+
|
|
189
|
+
function round2(value: number): number {
|
|
190
|
+
'worklet';
|
|
191
|
+
const rounded = Math.round(value * 100) / 100;
|
|
192
|
+
return Object.is(rounded, -0) ? 0 : rounded;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function readNumber(row: object, key: string): number | null {
|
|
196
|
+
const raw = (row as Record<string, unknown>)[key];
|
|
197
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return raw;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function rawCategory(row: object | undefined, key: string | undefined, index: number): unknown {
|
|
204
|
+
if (key === undefined || row === undefined) {
|
|
205
|
+
return index;
|
|
206
|
+
}
|
|
207
|
+
return (row as Record<string, unknown>)[key];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function categoryText(
|
|
211
|
+
raw: unknown,
|
|
212
|
+
index: number,
|
|
213
|
+
location: 'tick' | 'tooltip',
|
|
214
|
+
axis: ChartAxis<object> | undefined,
|
|
215
|
+
): string {
|
|
216
|
+
if (typeof raw === 'string') {
|
|
217
|
+
return raw;
|
|
218
|
+
}
|
|
219
|
+
if (typeof raw === 'number') {
|
|
220
|
+
return axis?.valueFormatter?.(raw, { location, dataIndex: index }) ?? String(raw);
|
|
221
|
+
}
|
|
222
|
+
if (raw === null || raw === undefined) {
|
|
223
|
+
return '';
|
|
224
|
+
}
|
|
225
|
+
return String(raw as string | boolean | bigint);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Maps the public `dataset` + `series` + `xAxis` props onto geometry inputs. Row keys are read
|
|
230
|
+
* only through `dataKey`, so no key is reserved.
|
|
231
|
+
*/
|
|
232
|
+
export function resolveLineChartData<TRow extends object>({
|
|
233
|
+
dataset,
|
|
234
|
+
series,
|
|
235
|
+
xAxis,
|
|
236
|
+
}: {
|
|
237
|
+
readonly dataset: readonly TRow[];
|
|
238
|
+
readonly series: readonly LineChartSeries<TRow>[];
|
|
239
|
+
readonly xAxis?: ChartAxis<TRow>;
|
|
240
|
+
}): { readonly categories: LineCategory[]; readonly series: LineSeriesInput[] } {
|
|
241
|
+
const axis = xAxis as ChartAxis<object> | undefined;
|
|
242
|
+
const interval = axis?.tickLabelInterval;
|
|
243
|
+
const hidden = axis?.position === 'none';
|
|
244
|
+
const categories = dataset.map((row, index): LineCategory => {
|
|
245
|
+
const raw = rawCategory(row, axis?.dataKey, index);
|
|
246
|
+
const label = categoryText(raw, index, 'tick', axis);
|
|
247
|
+
const shown = typeof interval === 'function' ? interval(raw, index) : true;
|
|
248
|
+
return {
|
|
249
|
+
label,
|
|
250
|
+
title: categoryText(raw, index, 'tooltip', axis),
|
|
251
|
+
tick: !hidden && shown && label.length > 0,
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
const inputs = series.map((item): LineSeriesInput => {
|
|
255
|
+
const dashed = item.dashed === true;
|
|
256
|
+
const dashAt =
|
|
257
|
+
dashed || item.dashedFrom === undefined || !Number.isInteger(item.dashedFrom)
|
|
258
|
+
? -1
|
|
259
|
+
: item.dashedFrom;
|
|
260
|
+
return {
|
|
261
|
+
id: item.id,
|
|
262
|
+
label: item.label ?? '',
|
|
263
|
+
curve: item.curve ?? 'monotoneX',
|
|
264
|
+
area: item.area === true,
|
|
265
|
+
dashed,
|
|
266
|
+
dashAt,
|
|
267
|
+
values: dataset.map((row) => readNumber(row, item.dataKey)),
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
return { categories, series: inputs };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function lineChartGutters({
|
|
274
|
+
showValueTicks = true,
|
|
275
|
+
showCategoryLabels = true,
|
|
276
|
+
}: {
|
|
277
|
+
readonly showValueTicks?: boolean;
|
|
278
|
+
readonly showCategoryLabels?: boolean;
|
|
279
|
+
} = {}): LineChartGutters {
|
|
280
|
+
return {
|
|
281
|
+
paddingLeft: showValueTicks ? VALUE_TICK_LEFT : TIGHT_GUTTER,
|
|
282
|
+
paddingRight: showValueTicks ? VALUE_TICK_END : PLOT_END_GUTTER,
|
|
283
|
+
paddingTop: TIGHT_GUTTER,
|
|
284
|
+
paddingBottom: showCategoryLabels ? CATEGORY_GUTTER : TIGHT_GUTTER,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function resolveLineChartContentSize({
|
|
289
|
+
viewportWidth,
|
|
290
|
+
viewportHeight,
|
|
291
|
+
categoryCount,
|
|
292
|
+
gutters,
|
|
293
|
+
scrollable,
|
|
294
|
+
visiblePoints,
|
|
295
|
+
}: {
|
|
296
|
+
readonly viewportWidth: number;
|
|
297
|
+
readonly viewportHeight: number;
|
|
298
|
+
readonly categoryCount: number;
|
|
299
|
+
readonly gutters: LineChartGutters;
|
|
300
|
+
readonly scrollable: boolean;
|
|
301
|
+
readonly visiblePoints?: number;
|
|
302
|
+
}): { readonly width: number; readonly height: number } {
|
|
303
|
+
if (!scrollable || categoryCount <= 0) {
|
|
304
|
+
return { width: viewportWidth, height: viewportHeight };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const plotWidth = Math.max(viewportWidth - gutters.paddingLeft - gutters.paddingRight, 0);
|
|
308
|
+
const band =
|
|
309
|
+
visiblePoints !== undefined && visiblePoints > 0
|
|
310
|
+
? plotWidth / Math.max(visiblePoints - 1, 1)
|
|
311
|
+
: MIN_LINE_POINT_BAND;
|
|
312
|
+
const contentPlot = Math.max(plotWidth, Math.max(categoryCount - 1, 1) * band);
|
|
313
|
+
return {
|
|
314
|
+
width: contentPlot + gutters.paddingLeft + gutters.paddingRight,
|
|
315
|
+
height: viewportHeight,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Value domain and ticks. `min` / `max` pin that end exactly; an open end rounds to a readable
|
|
321
|
+
* tick. `tickNumber` is approximate.
|
|
322
|
+
*/
|
|
323
|
+
export function lineValueDomain(
|
|
324
|
+
values: readonly number[],
|
|
325
|
+
{ min: minOption, max: maxOption, tickNumber = DEFAULT_LINE_TICK_NUMBER }: LineDomainOptions = {},
|
|
326
|
+
): { readonly min: number; readonly max: number; readonly ticks: readonly number[] } {
|
|
327
|
+
const min = minOption !== undefined && Number.isFinite(minOption) ? minOption : undefined;
|
|
328
|
+
const max = maxOption !== undefined && Number.isFinite(maxOption) ? maxOption : undefined;
|
|
329
|
+
let low = min ?? (values.length > 0 ? Math.min(...values) : 0);
|
|
330
|
+
let high = max ?? (values.length > 0 ? Math.max(...values) : 1);
|
|
331
|
+
if (low > high) {
|
|
332
|
+
[low, high] = [high, low];
|
|
333
|
+
}
|
|
334
|
+
if (low === high) {
|
|
335
|
+
if (min === undefined) {
|
|
336
|
+
low -= 1;
|
|
337
|
+
}
|
|
338
|
+
if (max === undefined || min !== undefined) {
|
|
339
|
+
high += 1;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const nice = niceTicks(low, high, tickNumber);
|
|
343
|
+
const domainMin = min ?? nice[0] ?? low;
|
|
344
|
+
const domainMax = max ?? nice[nice.length - 1] ?? high;
|
|
345
|
+
const epsilon = (domainMax - domainMin) * 1e-9;
|
|
346
|
+
const ticks = nice.filter((tick) => tick >= domainMin - epsilon && tick <= domainMax + epsilon);
|
|
347
|
+
return { min: domainMin, max: domainMax, ticks };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function coord(x: number, y: number): string {
|
|
351
|
+
'worklet';
|
|
352
|
+
return `${String(round2(x))} ${String(round2(y))}`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function linearPath(points: readonly { x: number; y: number }[]): string {
|
|
356
|
+
'worklet';
|
|
357
|
+
return points
|
|
358
|
+
.map((point, index) => `${index === 0 ? 'M' : 'L'} ${coord(point.x, point.y)}`)
|
|
359
|
+
.join(' ');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function stepPath(points: readonly { x: number; y: number }[]): string {
|
|
363
|
+
'worklet';
|
|
364
|
+
if (points.length === 0) {
|
|
365
|
+
return '';
|
|
366
|
+
}
|
|
367
|
+
const parts = [`M ${coord(points[0].x, points[0].y)}`];
|
|
368
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
369
|
+
const prev = points[index - 1];
|
|
370
|
+
const point = points[index];
|
|
371
|
+
parts.push(`L ${coord(point.x, prev.y)}`, `L ${coord(point.x, point.y)}`);
|
|
372
|
+
}
|
|
373
|
+
return parts.join(' ');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function monotoneControls(points: readonly { x: number; y: number }[]): {
|
|
377
|
+
readonly dx: number[];
|
|
378
|
+
readonly tangent: number[];
|
|
379
|
+
} {
|
|
380
|
+
'worklet';
|
|
381
|
+
const count = points.length;
|
|
382
|
+
const dx: number[] = [];
|
|
383
|
+
const slope: number[] = [];
|
|
384
|
+
for (let index = 0; index < count - 1; index += 1) {
|
|
385
|
+
const deltaX = points[index + 1].x - points[index].x;
|
|
386
|
+
dx.push(deltaX);
|
|
387
|
+
slope.push(deltaX === 0 ? 0 : (points[index + 1].y - points[index].y) / deltaX);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const tangent: number[] = [slope[0]];
|
|
391
|
+
for (let index = 1; index < count - 1; index += 1) {
|
|
392
|
+
tangent.push(slope[index - 1] * slope[index] <= 0 ? 0 : (slope[index - 1] + slope[index]) / 2);
|
|
393
|
+
}
|
|
394
|
+
tangent.push(slope[count - 2]);
|
|
395
|
+
|
|
396
|
+
for (let index = 0; index < count - 1; index += 1) {
|
|
397
|
+
if (Math.abs(slope[index]) < 1e-8) {
|
|
398
|
+
tangent[index] = 0;
|
|
399
|
+
tangent[index + 1] = 0;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const alpha = tangent[index] / slope[index];
|
|
403
|
+
const beta = tangent[index + 1] / slope[index];
|
|
404
|
+
const magnitude = alpha * alpha + beta * beta;
|
|
405
|
+
if (magnitude > 9) {
|
|
406
|
+
const scale = 3 / Math.sqrt(magnitude);
|
|
407
|
+
tangent[index] = scale * alpha * slope[index];
|
|
408
|
+
tangent[index + 1] = scale * beta * slope[index];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return { dx, tangent };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function monotonePathSlice(
|
|
415
|
+
points: readonly { x: number; y: number }[],
|
|
416
|
+
from: number,
|
|
417
|
+
to: number,
|
|
418
|
+
): string {
|
|
419
|
+
'worklet';
|
|
420
|
+
const count = points.length;
|
|
421
|
+
if (count === 0 || to < from || from < 0 || to > count - 1) {
|
|
422
|
+
return '';
|
|
423
|
+
}
|
|
424
|
+
if (from === to) {
|
|
425
|
+
return `M ${coord(points[from].x, points[from].y)}`;
|
|
426
|
+
}
|
|
427
|
+
if (count === 2) {
|
|
428
|
+
return linearPath(points.slice(from, to + 1));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const { dx, tangent } = monotoneControls(points);
|
|
432
|
+
const parts = [`M ${coord(points[from].x, points[from].y)}`];
|
|
433
|
+
for (let index = from; index < to; index += 1) {
|
|
434
|
+
const start = points[index];
|
|
435
|
+
const end = points[index + 1];
|
|
436
|
+
const handle = dx[index] / 3;
|
|
437
|
+
parts.push(
|
|
438
|
+
`C ${coord(start.x + handle, start.y + tangent[index] * handle)} ${coord(end.x - handle, end.y - tangent[index + 1] * handle)} ${coord(end.x, end.y)}`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
return parts.join(' ');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function monotonePath(points: readonly { x: number; y: number }[]): string {
|
|
445
|
+
'worklet';
|
|
446
|
+
if (points.length === 0) {
|
|
447
|
+
return '';
|
|
448
|
+
}
|
|
449
|
+
return monotonePathSlice(points, 0, points.length - 1);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function pathForCurve(points: readonly { x: number; y: number }[], curve: LineChartCurve): string {
|
|
453
|
+
'worklet';
|
|
454
|
+
if (curve === 'stepAfter') {
|
|
455
|
+
return stepPath(points);
|
|
456
|
+
}
|
|
457
|
+
if (curve === 'monotoneX') {
|
|
458
|
+
return monotonePath(points);
|
|
459
|
+
}
|
|
460
|
+
return linearPath(points);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function pathSlice(
|
|
464
|
+
points: readonly { x: number; y: number }[],
|
|
465
|
+
curve: LineChartCurve,
|
|
466
|
+
from: number,
|
|
467
|
+
to: number,
|
|
468
|
+
): string {
|
|
469
|
+
'worklet';
|
|
470
|
+
if (to <= from) {
|
|
471
|
+
return '';
|
|
472
|
+
}
|
|
473
|
+
if (curve === 'monotoneX') {
|
|
474
|
+
return monotonePathSlice(points, from, to);
|
|
475
|
+
}
|
|
476
|
+
return pathForCurve(points.slice(from, to + 1), curve);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function areaFromLine(
|
|
480
|
+
linePath: string,
|
|
481
|
+
run: readonly { x: number; y: number }[],
|
|
482
|
+
baselineY: number,
|
|
483
|
+
): string {
|
|
484
|
+
'worklet';
|
|
485
|
+
if (linePath.length === 0 || run.length === 0) {
|
|
486
|
+
return '';
|
|
487
|
+
}
|
|
488
|
+
const first = run[0];
|
|
489
|
+
const last = run[run.length - 1];
|
|
490
|
+
return `${linePath} L ${coord(last.x, baselineY)} L ${coord(first.x, baselineY)} Z`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Line and area paths for one series. `ys` uses `NaN` for missing values, which
|
|
495
|
+
* open a gap. Runs on the UI thread while a chart animates.
|
|
496
|
+
* `dashAt` is a category index; cubics before it stay solid, after it dashed,
|
|
497
|
+
* using the same monotone tangents so the join stays round.
|
|
498
|
+
*/
|
|
499
|
+
export function lineSeriesPaths(
|
|
500
|
+
xs: readonly number[],
|
|
501
|
+
ys: readonly number[],
|
|
502
|
+
curve: LineChartCurve,
|
|
503
|
+
baselineY: number,
|
|
504
|
+
dashAt = -1,
|
|
505
|
+
): { readonly path: string; readonly dashedPath: string; readonly areaPath: string } {
|
|
506
|
+
'worklet';
|
|
507
|
+
const runs: { x: number; y: number; index: number }[][] = [];
|
|
508
|
+
let current: { x: number; y: number; index: number }[] = [];
|
|
509
|
+
for (let index = 0; index < ys.length; index += 1) {
|
|
510
|
+
const y = ys[index];
|
|
511
|
+
if (!Number.isFinite(y)) {
|
|
512
|
+
if (current.length > 0) {
|
|
513
|
+
runs.push(current);
|
|
514
|
+
current = [];
|
|
515
|
+
}
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
current.push({ x: xs[index] ?? 0, y, index });
|
|
519
|
+
}
|
|
520
|
+
if (current.length > 0) {
|
|
521
|
+
runs.push(current);
|
|
522
|
+
}
|
|
523
|
+
const solids: string[] = [];
|
|
524
|
+
const dashes: string[] = [];
|
|
525
|
+
const areas: string[] = [];
|
|
526
|
+
for (const run of runs) {
|
|
527
|
+
const line = pathForCurve(run, curve);
|
|
528
|
+
areas.push(areaFromLine(line, run, baselineY));
|
|
529
|
+
const first = run[0]?.index ?? -1;
|
|
530
|
+
const last = run[run.length - 1]?.index ?? -1;
|
|
531
|
+
if (dashAt < 0 || dashAt <= first || dashAt >= last) {
|
|
532
|
+
if (dashAt >= 0 && dashAt <= first) {
|
|
533
|
+
dashes.push(line);
|
|
534
|
+
} else {
|
|
535
|
+
solids.push(line);
|
|
536
|
+
}
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const split = run.findIndex((point) => point.index === dashAt);
|
|
540
|
+
if (split <= 0) {
|
|
541
|
+
solids.push(line);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
solids.push(pathSlice(run, curve, 0, split));
|
|
545
|
+
dashes.push(pathSlice(run, curve, split, run.length - 1));
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
path: solids.join(' '),
|
|
549
|
+
dashedPath: dashes.join(' '),
|
|
550
|
+
areaPath: areas.join(' '),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Pins each marker to its `dataIndex` row; markers that cannot resolve are dropped. */
|
|
555
|
+
export function layoutLineChartMarkers(
|
|
556
|
+
markers: readonly LineChartMarker[],
|
|
557
|
+
series: readonly LineSeriesLayout[],
|
|
558
|
+
): readonly LineChartMarkerLayout[] {
|
|
559
|
+
return markers.flatMap((marker, markerIndex) => {
|
|
560
|
+
const index = marker.dataIndex;
|
|
561
|
+
if (!Number.isInteger(index)) {
|
|
562
|
+
return [];
|
|
563
|
+
}
|
|
564
|
+
const seriesItem =
|
|
565
|
+
marker.seriesId !== undefined
|
|
566
|
+
? series.find((item) => item.key === marker.seriesId)
|
|
567
|
+
: series.find((item) => item.points[index]?.y != null);
|
|
568
|
+
const point = seriesItem?.points[index];
|
|
569
|
+
if (seriesItem === undefined || point?.y == null || point.value == null) {
|
|
570
|
+
return [];
|
|
571
|
+
}
|
|
572
|
+
return [
|
|
573
|
+
{
|
|
574
|
+
key: `${seriesItem.key}-${String(index)}-${String(markerIndex)}`,
|
|
575
|
+
index,
|
|
576
|
+
seriesIndex: series.indexOf(seriesItem),
|
|
577
|
+
seriesId: seriesItem.key,
|
|
578
|
+
x: point.x,
|
|
579
|
+
y: point.y,
|
|
580
|
+
value: point.value,
|
|
581
|
+
label: marker.label,
|
|
582
|
+
showValue: marker.showValue === true,
|
|
583
|
+
color: marker.color,
|
|
584
|
+
legend: marker.legend,
|
|
585
|
+
guide: marker.guide !== false,
|
|
586
|
+
dot: marker.dot !== false,
|
|
587
|
+
},
|
|
588
|
+
];
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function distanceToSegment(
|
|
593
|
+
px: number,
|
|
594
|
+
py: number,
|
|
595
|
+
ax: number,
|
|
596
|
+
ay: number,
|
|
597
|
+
bx: number,
|
|
598
|
+
by: number,
|
|
599
|
+
): number {
|
|
600
|
+
const dx = bx - ax;
|
|
601
|
+
const dy = by - ay;
|
|
602
|
+
const length = dx * dx + dy * dy;
|
|
603
|
+
if (length === 0) {
|
|
604
|
+
return Math.hypot(px - ax, py - ay);
|
|
605
|
+
}
|
|
606
|
+
const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / length));
|
|
607
|
+
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
interface StrokePoint {
|
|
611
|
+
readonly x: number;
|
|
612
|
+
readonly y: number;
|
|
613
|
+
readonly index: number;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function cubicPoint(
|
|
617
|
+
t: number,
|
|
618
|
+
p0: StrokePoint,
|
|
619
|
+
p1: { readonly x: number; readonly y: number },
|
|
620
|
+
p2: { readonly x: number; readonly y: number },
|
|
621
|
+
p3: StrokePoint,
|
|
622
|
+
): { readonly x: number; readonly y: number } {
|
|
623
|
+
const u = 1 - t;
|
|
624
|
+
const a = u * u * u;
|
|
625
|
+
const b = 3 * u * u * t;
|
|
626
|
+
const c = 3 * u * t * t;
|
|
627
|
+
const d = t * t * t;
|
|
628
|
+
return {
|
|
629
|
+
x: a * p0.x + b * p1.x + c * p2.x + d * p3.x,
|
|
630
|
+
y: a * p0.y + b * p1.y + c * p2.y + d * p3.y,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function sampleMonotoneRun(points: readonly StrokePoint[]): StrokePoint[] {
|
|
635
|
+
if (points.length < 3) {
|
|
636
|
+
return [...points];
|
|
637
|
+
}
|
|
638
|
+
const count = points.length;
|
|
639
|
+
const dx: number[] = [];
|
|
640
|
+
const slope: number[] = [];
|
|
641
|
+
for (let index = 0; index < count - 1; index += 1) {
|
|
642
|
+
const from = points[index];
|
|
643
|
+
const to = points[index + 1];
|
|
644
|
+
if (from === undefined || to === undefined) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const deltaX = to.x - from.x;
|
|
648
|
+
dx.push(deltaX);
|
|
649
|
+
slope.push(deltaX === 0 ? 0 : (to.y - from.y) / deltaX);
|
|
650
|
+
}
|
|
651
|
+
const tangent: number[] = [slope[0] ?? 0];
|
|
652
|
+
for (let index = 1; index < count - 1; index += 1) {
|
|
653
|
+
tangent.push(
|
|
654
|
+
(slope[index - 1] ?? 0) * (slope[index] ?? 0) <= 0
|
|
655
|
+
? 0
|
|
656
|
+
: ((slope[index - 1] ?? 0) + (slope[index] ?? 0)) / 2,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
tangent.push(slope[count - 2] ?? 0);
|
|
660
|
+
for (let index = 0; index < count - 1; index += 1) {
|
|
661
|
+
if (Math.abs(slope[index] ?? 0) < 1e-8) {
|
|
662
|
+
tangent[index] = 0;
|
|
663
|
+
tangent[index + 1] = 0;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const sampled: StrokePoint[] = [];
|
|
668
|
+
const steps = 8;
|
|
669
|
+
for (let index = 0; index < count - 1; index += 1) {
|
|
670
|
+
const start = points[index];
|
|
671
|
+
const end = points[index + 1];
|
|
672
|
+
if (start === undefined || end === undefined) {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const handle = (dx[index] ?? 0) / 3;
|
|
676
|
+
const c1 = { x: start.x + handle, y: start.y + (tangent[index] ?? 0) * handle };
|
|
677
|
+
const c2 = { x: end.x - handle, y: end.y - (tangent[index + 1] ?? 0) * handle };
|
|
678
|
+
for (let step = 0; step < steps; step += 1) {
|
|
679
|
+
const t = step / steps;
|
|
680
|
+
const point = cubicPoint(t, start, c1, c2, end);
|
|
681
|
+
sampled.push({
|
|
682
|
+
x: point.x,
|
|
683
|
+
y: point.y,
|
|
684
|
+
index: t < 0.5 ? start.index : end.index,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const last = points[count - 1];
|
|
689
|
+
if (last !== undefined) {
|
|
690
|
+
sampled.push(last);
|
|
691
|
+
}
|
|
692
|
+
return sampled;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function strokeSamples(points: readonly StrokePoint[], curve: LineChartCurve): StrokePoint[] {
|
|
696
|
+
if (curve === 'monotoneX') {
|
|
697
|
+
return sampleMonotoneRun(points);
|
|
698
|
+
}
|
|
699
|
+
if (curve === 'stepAfter') {
|
|
700
|
+
const sampled: StrokePoint[] = [];
|
|
701
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
702
|
+
const from = points[index];
|
|
703
|
+
const to = points[index + 1];
|
|
704
|
+
if (from === undefined) {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
sampled.push(from);
|
|
708
|
+
if (to !== undefined) {
|
|
709
|
+
sampled.push({ x: to.x, y: from.y, index: from.index });
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return sampled;
|
|
713
|
+
}
|
|
714
|
+
return [...points];
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Category index of the closest point on any series stroke, or `null` when the pointer is
|
|
719
|
+
* farther than `maxDistance` from every line. Overlay coordinates (`plot.x` already subtracted).
|
|
720
|
+
* Uses the same monotone cubics as the painted path so a valley (Lowest) still hits.
|
|
721
|
+
*/
|
|
722
|
+
export function nearestCategoryOnLines(
|
|
723
|
+
series: readonly LineSeriesLayout[],
|
|
724
|
+
overlayX: number,
|
|
725
|
+
overlayY: number,
|
|
726
|
+
plotOffsetX: number,
|
|
727
|
+
maxDistance: number,
|
|
728
|
+
pointDistance: number = maxDistance,
|
|
729
|
+
): number | null {
|
|
730
|
+
let bestDistance = Math.max(maxDistance, pointDistance);
|
|
731
|
+
let bestIndex: number | null = null;
|
|
732
|
+
|
|
733
|
+
for (const item of series) {
|
|
734
|
+
const points: StrokePoint[] = item.points.flatMap((point) =>
|
|
735
|
+
point.y == null ? [] : [{ x: point.x - plotOffsetX, y: point.y, index: point.index }],
|
|
736
|
+
);
|
|
737
|
+
for (const point of points) {
|
|
738
|
+
const distance = Math.hypot(overlayX - point.x, overlayY - point.y);
|
|
739
|
+
if (distance <= pointDistance && distance <= bestDistance) {
|
|
740
|
+
bestDistance = distance;
|
|
741
|
+
bestIndex = point.index;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const sampled = strokeSamples(points, item.curve);
|
|
745
|
+
for (let i = 0; i < sampled.length; i += 1) {
|
|
746
|
+
const from = sampled[i];
|
|
747
|
+
const to = sampled[i + 1];
|
|
748
|
+
if (from === undefined) {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
const distance =
|
|
752
|
+
to === undefined
|
|
753
|
+
? Math.hypot(overlayX - from.x, overlayY - from.y)
|
|
754
|
+
: distanceToSegment(overlayX, overlayY, from.x, from.y, to.x, to.y);
|
|
755
|
+
if (distance > maxDistance || distance > bestDistance) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
bestDistance = distance;
|
|
759
|
+
if (to === undefined) {
|
|
760
|
+
bestIndex = from.index;
|
|
761
|
+
} else {
|
|
762
|
+
const closerToStart =
|
|
763
|
+
Math.hypot(overlayX - from.x, overlayY - from.y) <=
|
|
764
|
+
Math.hypot(overlayX - to.x, overlayY - to.y);
|
|
765
|
+
bestIndex = closerToStart ? from.index : to.index;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return bestIndex;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Category under the pointer when it is inside the plot box. Snaps to the nearest category on X.
|
|
775
|
+
* Overlay coordinates (`plot.x` already subtracted).
|
|
776
|
+
*/
|
|
777
|
+
export function nearestCategoryInPlot(
|
|
778
|
+
labels: readonly { readonly x: number }[],
|
|
779
|
+
plot: { readonly x: number; readonly y: number; readonly width: number; readonly height: number },
|
|
780
|
+
overlayX: number,
|
|
781
|
+
overlayY: number,
|
|
782
|
+
plotOffsetX: number,
|
|
783
|
+
): number | null {
|
|
784
|
+
if (labels.length === 0) {
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
if (overlayY < plot.y || overlayY > plot.y + plot.height) {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
if (overlayX < 0 || overlayX > plot.width) {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
let bestIndex = 0;
|
|
794
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
795
|
+
labels.forEach((item, index) => {
|
|
796
|
+
const dist = Math.abs(item.x - plotOffsetX - overlayX);
|
|
797
|
+
if (dist < bestDist) {
|
|
798
|
+
bestDist = dist;
|
|
799
|
+
bestIndex = index;
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
return bestIndex;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export function lineChartMarkerGutters(
|
|
806
|
+
categoryCount: number,
|
|
807
|
+
markers: readonly LineChartMarker[] | undefined,
|
|
808
|
+
base: LineChartGutters = lineChartGutters(),
|
|
809
|
+
): LineChartGutters {
|
|
810
|
+
if (markers === undefined || markers.length === 0) {
|
|
811
|
+
return base;
|
|
812
|
+
}
|
|
813
|
+
const hasCaption = markers.some(
|
|
814
|
+
(marker) =>
|
|
815
|
+
(marker.label !== undefined && marker.label.length > 0) || marker.showValue === true,
|
|
816
|
+
);
|
|
817
|
+
const hasEndValue = markers.some(
|
|
818
|
+
(marker) => marker.showValue === true && marker.dataIndex === categoryCount - 1,
|
|
819
|
+
);
|
|
820
|
+
return {
|
|
821
|
+
...base,
|
|
822
|
+
paddingTop: hasCaption ? LINE_MARKER_PADDING_TOP : base.paddingTop,
|
|
823
|
+
paddingRight: hasEndValue ? LINE_MARKER_PADDING_RIGHT : base.paddingRight,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export function layoutLineChart({
|
|
828
|
+
categories,
|
|
829
|
+
series,
|
|
830
|
+
width,
|
|
831
|
+
height,
|
|
832
|
+
referenceLines = [],
|
|
833
|
+
yAxis,
|
|
834
|
+
formatTick = String,
|
|
835
|
+
gutters: guttersProp,
|
|
836
|
+
}: {
|
|
837
|
+
readonly categories: readonly LineCategory[];
|
|
838
|
+
readonly series: readonly LineSeriesInput[];
|
|
839
|
+
readonly width: number;
|
|
840
|
+
readonly height: number;
|
|
841
|
+
readonly referenceLines?: readonly ChartReferenceLine[];
|
|
842
|
+
readonly yAxis?: LineDomainOptions;
|
|
843
|
+
readonly formatTick?: (value: number) => string;
|
|
844
|
+
readonly gutters?: LineChartGutters;
|
|
845
|
+
}): LineChartLayout {
|
|
846
|
+
const empty = {
|
|
847
|
+
series: [],
|
|
848
|
+
categoryLabels: [],
|
|
849
|
+
valueTicks: [],
|
|
850
|
+
gridLines: [],
|
|
851
|
+
referenceLines: [],
|
|
852
|
+
baselineY: 0,
|
|
853
|
+
plot: { x: 0, y: 0, width: 0, height: 0 },
|
|
854
|
+
};
|
|
855
|
+
if (width <= 0 || height <= 0 || categories.length === 0 || series.length === 0) {
|
|
856
|
+
return empty;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const gutters = guttersProp ?? lineChartGutters();
|
|
860
|
+
const plot = {
|
|
861
|
+
x: gutters.paddingLeft,
|
|
862
|
+
y: gutters.paddingTop,
|
|
863
|
+
width: Math.max(width - gutters.paddingLeft - gutters.paddingRight, 0),
|
|
864
|
+
height: Math.max(height - gutters.paddingTop - gutters.paddingBottom, 0),
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
const values = [
|
|
868
|
+
...series.flatMap((item) => item.values.filter((value): value is number => value !== null)),
|
|
869
|
+
...referenceLines.flatMap((line) => (Number.isFinite(line.value) ? [line.value] : [])),
|
|
870
|
+
];
|
|
871
|
+
const domain = lineValueDomain(values, yAxis);
|
|
872
|
+
const span = domain.max - domain.min || 1;
|
|
873
|
+
const lastIndex = Math.max(categories.length - 1, 1);
|
|
874
|
+
|
|
875
|
+
const valueToY = (value: number) => plot.y + plot.height * (1 - (value - domain.min) / span);
|
|
876
|
+
const indexToX = (index: number) =>
|
|
877
|
+
categories.length === 1 ? plot.x + plot.width / 2 : plot.x + (index / lastIndex) * plot.width;
|
|
878
|
+
const baselineY = valueToY(Math.min(Math.max(0, domain.min), domain.max));
|
|
879
|
+
|
|
880
|
+
const categoryLabels: LineLabelAnchor[] = categories.map((category, index) => ({
|
|
881
|
+
label: category.label,
|
|
882
|
+
title: category.title,
|
|
883
|
+
x: round2(indexToX(index)),
|
|
884
|
+
y: round2(height - 8),
|
|
885
|
+
tick: category.tick,
|
|
886
|
+
}));
|
|
887
|
+
|
|
888
|
+
const seriesLayouts: LineSeriesLayout[] = series.map((item) => {
|
|
889
|
+
const points: LinePoint[] = categories.map((_, index) => {
|
|
890
|
+
const value = item.values[index] ?? null;
|
|
891
|
+
return {
|
|
892
|
+
index,
|
|
893
|
+
value,
|
|
894
|
+
x: round2(indexToX(index)),
|
|
895
|
+
y: value === null ? null : round2(valueToY(value)),
|
|
896
|
+
};
|
|
897
|
+
});
|
|
898
|
+
const dashAt = item.dashAt < categories.length ? item.dashAt : -1;
|
|
899
|
+
const { path, dashedPath, areaPath } = lineSeriesPaths(
|
|
900
|
+
points.map((point) => point.x),
|
|
901
|
+
points.map((point) => point.y ?? Number.NaN),
|
|
902
|
+
item.curve,
|
|
903
|
+
baselineY,
|
|
904
|
+
dashAt,
|
|
905
|
+
);
|
|
906
|
+
return {
|
|
907
|
+
key: item.id,
|
|
908
|
+
label: item.label,
|
|
909
|
+
curve: item.curve,
|
|
910
|
+
area: item.area,
|
|
911
|
+
dashed: item.dashed,
|
|
912
|
+
path,
|
|
913
|
+
dashedPath,
|
|
914
|
+
dashAt,
|
|
915
|
+
areaPath,
|
|
916
|
+
points,
|
|
917
|
+
};
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
const valueTicks: LineValueTick[] = domain.ticks.map((value) => ({
|
|
921
|
+
value,
|
|
922
|
+
x: round2(plot.x - 8),
|
|
923
|
+
y: round2(valueToY(value)),
|
|
924
|
+
text: formatTick(value),
|
|
925
|
+
}));
|
|
926
|
+
|
|
927
|
+
const gridLines: LineGridLine[] = domain.ticks.map((value) => {
|
|
928
|
+
const y = round2(valueToY(value));
|
|
929
|
+
return { x1: round2(plot.x), y1: y, x2: round2(plot.x + plot.width), y2: y };
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
const laidReference = referenceLines.flatMap((line) => {
|
|
933
|
+
if (!Number.isFinite(line.value)) {
|
|
934
|
+
return [];
|
|
935
|
+
}
|
|
936
|
+
const y = round2(valueToY(line.value));
|
|
937
|
+
return [
|
|
938
|
+
{
|
|
939
|
+
x1: round2(plot.x),
|
|
940
|
+
y1: y,
|
|
941
|
+
x2: round2(plot.x + plot.width),
|
|
942
|
+
y2: y,
|
|
943
|
+
label: line.label,
|
|
944
|
+
},
|
|
945
|
+
];
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
return {
|
|
949
|
+
series: seriesLayouts,
|
|
950
|
+
categoryLabels,
|
|
951
|
+
valueTicks,
|
|
952
|
+
gridLines,
|
|
953
|
+
referenceLines: laidReference,
|
|
954
|
+
baselineY: round2(baselineY),
|
|
955
|
+
plot,
|
|
956
|
+
};
|
|
957
|
+
}
|