@tensor_1/react 2.37.0 → 2.39.0
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 +50 -0
- package/dist/{chart-theme-Cl0IrkKQ.d.cts → ChartCore-BKK2HEfi.d.cts} +77 -50
- package/dist/chart-analytic-guide.d.ts +6 -0
- package/dist/chart-analytic-guide.js +10 -0
- package/dist/chart-analytic-model.d.ts +141 -0
- package/dist/chart-analytic-model.js +252 -0
- package/dist/chart-analytic.cjs +1372 -0
- package/dist/chart-analytic.d.cts +335 -0
- package/dist/chart-analytic.d.ts +19 -0
- package/dist/chart-analytic.js +95 -0
- package/dist/chart-panels-model.d.ts +92 -0
- package/dist/chart-panels-model.js +284 -0
- package/dist/chart-preset.cjs +41 -4
- package/dist/chart-preset.d.cts +2 -2
- package/dist/chart-preset.d.ts +1 -1
- package/dist/chart-stats-o4zDhpVm.d.cts +46 -0
- package/dist/chart-stats.d.ts +46 -0
- package/dist/chart-stats.js +89 -0
- package/dist/chart-theme.d.ts +3 -0
- package/dist/chart-theme.js +2 -0
- package/dist/chart.cjs +41 -4
- package/dist/chart.d.cts +2 -2
- package/dist/chart.d.ts +1 -1
- package/dist/components/Chart.d.ts +1 -1
- package/dist/components/ChartCore.d.ts +23 -3
- package/dist/components/ChartCore.js +40 -5
- package/dist/components/ChartPanels.d.ts +52 -0
- package/dist/components/ChartPanels.js +170 -0
- package/dist/components/InsightCard.d.ts +86 -0
- package/dist/components/InsightCard.js +118 -0
- package/dist/components/Stat.d.ts +7 -0
- package/dist/components/Stat.js +6 -3
- package/dist/correlation-scatter-model.d.ts +68 -0
- package/dist/correlation-scatter-model.js +179 -0
- package/dist/icons/Icon.d.ts +23 -23
- package/dist/index.cjs +1185 -961
- package/dist/index.d.cts +91 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -198,12 +198,15 @@ Radial mode still shortens long labels to fit its ring, but every node carries i
|
|
|
198
198
|
- **`Tabs overflow="scroll"`** keeps a long tab row on one line that scrolls sideways. The selected tab is always scrolled into view, and edge buttons appear only on the side that has hidden tabs. The buttons are pointer-only: keyboard users move with Arrow, Home and End. Combine it with `TabPanel keepMounted` on a record page with many sections.
|
|
199
199
|
- **`Stat tone`** (`warning`, `danger`) draws a toned outline and value; the label stays muted, so the tone never replaces words. `size="compact"` fits dense fact bands.
|
|
200
200
|
- **`Stat onActivate`** makes the whole tile a button that opens an editor (`aria-haspopup="dialog"`). Pass `activateLabel` (e.g. "change") so the name reads "Priority: P1, change", and `expanded` while the editor is open. The ref points at the button, for anchoring a popover.
|
|
201
|
+
- **`Stat pending`** is for a tile whose action is still running (a save, a state change). The button gets `aria-busy="true"` and `aria-disabled="true"`, is dimmed with a progress cursor, and calls neither `onActivate` nor your `onClick` / `onKeyDown` until `pending` is false again. It is never natively `disabled`, so it keeps focus and a screen reader can still read it. Only a tile with `onActivate` has the state; a read-only tile and a link tile ignore the prop.
|
|
202
|
+
- **Tiles in one row line up.** A grid or flex row stretches its tiles to the tallest one. Read-only, button and link tiles all start their label and value at the same offset from the top edge, whatever the height (before 2.38.0 a button tile centred its rows).
|
|
201
203
|
- **`CopyButton`** copies `value`. Name it for what it copies (`label="Copy email"`), pass `copiedLabel`, and pass `failedLabel` so a refused copy is explained rather than silent.
|
|
202
204
|
|
|
203
205
|
```tsx
|
|
204
206
|
<Tabs value={tab} onChange={setTab} overflow="scroll">{tabs}</Tabs>
|
|
205
207
|
<Stat label={t('sla')} value={remaining} tone={breached ? 'danger' : 'neutral'} size="compact" />
|
|
206
208
|
<Stat ref={anchor} label={t('priority')} value="P1" onActivate={openEditor} activateLabel={t('change')} expanded={open} size="compact" />
|
|
209
|
+
<Stat label={t('state')} value={state} onActivate={resolve} activateLabel={t('resolve')} haspopup={false} pending={saving} size="compact" />
|
|
207
210
|
<CopyButton value={email} label={t('copyEmail')} copiedLabel={t('copied')} failedLabel={t('copyFailed')} />
|
|
208
211
|
```
|
|
209
212
|
|
|
@@ -673,6 +676,53 @@ A two-way diff of two texts (HAR-1368; TENSOR's knowledge-article version diff).
|
|
|
673
676
|
- **Long texts:** `context` keeps that many unchanged lines around each change. Longer runs fold behind "Show n unchanged lines", and focus moves to the first revealed line. Over `maxTokens` (20 000) a note replaces the diff. Past `maxEdits` (2 000) the changed middle shows as one replacement instead of stalling.
|
|
674
677
|
- Pure model for tests and servers: `diffText`, `diffTokens`, `textDiffRows`, `tokenizeText`.
|
|
675
678
|
|
|
679
|
+
### Decision-grade charts: `chart/analytic`, `ChartPanels`, `CorrelationScatter`, `InsightCard`
|
|
680
|
+
|
|
681
|
+
Charts that answer a decision (HAR-1553, HAR-1554, HAR-1556). The product computes the data — series, thresholds, events, forecasts with bounds, coverage — and writes the words; UIx draws. `@tensor_1/react/chart/analytic` is its own tree-shaken ECharts build (≤ 185 kB Brotli).
|
|
682
|
+
|
|
683
|
+
```tsx
|
|
684
|
+
import { ChartPanels, CorrelationScatter } from '@tensor_1/react/chart/analytic';
|
|
685
|
+
import { InsightCard } from '@tensor_1/react';
|
|
686
|
+
|
|
687
|
+
<InsightCard
|
|
688
|
+
eyebrow="Delivery health · last 15 days"
|
|
689
|
+
headline="Review is the bottleneck: 18.7 h, over budget for 3 days"
|
|
690
|
+
sowhat="Cap work in Review at 8 or add a reviewer this week."
|
|
691
|
+
status={{ tone: 'warning', label: 'Over budget' }}
|
|
692
|
+
kpis={[{ value: '18.7 h', label: 'Residence in Review',
|
|
693
|
+
comparison: { text: '↑ 3.7 h over the 15 h budget', tone: 'negative' },
|
|
694
|
+
target: { actual: 18.7, target: 15, ranges: [[0, 10], [10, 15], [15, 24]] } }]}
|
|
695
|
+
coverage={{ covered: 412, total: 431, note: 'items timed', partialPeriod: 'Today is partial.' }}
|
|
696
|
+
actions={<Link href="/review?state=waiting">Open the 10 items waiting in Review</Link>}
|
|
697
|
+
layout="split"
|
|
698
|
+
aside={<CorrelationScatter points={points} xLabel="Items in Review" yLabel="Residence (h)"
|
|
699
|
+
breakpoint={{ x: 9, label: 'Knee ≈ 9 items' }} ariaLabel="…" />}
|
|
700
|
+
>
|
|
701
|
+
<ChartPanels
|
|
702
|
+
x={{ type: 'category', values: days }}
|
|
703
|
+
partialLast
|
|
704
|
+
events={[{ at: 'Sep 1', kind: 'Release', label: 'Release freeze starts' }]}
|
|
705
|
+
panels={[
|
|
706
|
+
{ id: 'residence', title: 'Residence', unit: 'h', kind: 'line',
|
|
707
|
+
series: [{ id: 'review', label: 'Review', values: review }],
|
|
708
|
+
thresholds: [{ value: 15, kind: 'budget', tone: 'warning', label: '15 h budget', badSide: 'above' }],
|
|
709
|
+
forecast: { from: 'Today', level: 0.8, method: 'linear', points: projection },
|
|
710
|
+
comparison: { label: 'Previous 15 days', values: previous } },
|
|
711
|
+
{ id: 'wip', title: 'Work in Review', unit: 'items', kind: 'bar', series: [{ id: 'wip', label: 'Items', values: wip }] },
|
|
712
|
+
]}
|
|
713
|
+
ariaLabel="Residence and work in Review, daily for 15 days"
|
|
714
|
+
/>
|
|
715
|
+
</InsightCard>
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
- **One axis, never two.** A panel has one unit and one y axis; two measures of different scale go on two panels on the same x axis, read with one crosshair and one tooltip that lists every panel at the hovered x. `group` links the crosshair across cards.
|
|
719
|
+
- **Only a projection is dashed.** A threshold is a solid reference line labelled in the right gutter with its bad side shaded; the first point past it is ringed and named ("Crossed Sep 2"). The previous period is a thin solid ghost. Events sit in a lane of pins above the plot, which owns their tooltip and click (`onEventClick`, else the event's `href`), with unlabeled lines through every panel. `partialLast` shades the still-filling period.
|
|
720
|
+
- **Honest correlation.** `CorrelationScatter` shows r and n with "association, not cause", refuses a trend below `minPoints` (default 8) and gives every point a 26 px hit target.
|
|
721
|
+
- **Words.** Every word is a label: `labels` on `ChartPanels` (`DEFAULT_CHART_PANELS_LABELS`, including `formatX` / `formatValue`), on `CorrelationScatter` (`DEFAULT_CORRELATION_SCATTER_LABELS`) and on `InsightCard` (`DEFAULT_INSIGHT_CARD_LABELS`).
|
|
722
|
+
- **Text alternatives.** One hidden table per panel (with lower, upper, threshold and event columns when given) and one for the scatter; `ariaLabel` is required.
|
|
723
|
+
- **Building blocks.** The overlay helpers (`thresholdMarks`, `eventMarks`, `forecastSeries`, `comparisonSeries`, `partialPeriod`, `crossingPoints`, `combineMarks`) return option fragments for a hand-built `Chart` option; `chartPanelsModel` and `correlationScatterModel` are the pure models the components draw. `linearFit`, `quantiles` and `predictionBand` are on the root entry, so a server computes the same fit the chart draws.
|
|
724
|
+
- `Chart`'s `option` may be a function of the theme's chart tokens; it is re-run when the theme flips, so colours you pass from `uixChartTokens` follow light and dark.
|
|
725
|
+
|
|
676
726
|
### DashboardGrid
|
|
677
727
|
|
|
678
728
|
The layout grid for dashboard widgets (HAR-1555; TENSOR's role dashboards). Pure layout: drag-and-drop, persistence and widget chrome stay in the product. Server-renderable.
|
|
@@ -2,6 +2,74 @@ import * as react from 'react';
|
|
|
2
2
|
import { CSSProperties, ReactNode, HTMLAttributes } from 'react';
|
|
3
3
|
import { EChartsOption, ECharts } from 'echarts';
|
|
4
4
|
|
|
5
|
+
/** Where tokens are read from: an element (its computed style), or a reader for tests and non-DOM use. */
|
|
6
|
+
type ChartTokenSource = Element | ((token: string) => string);
|
|
7
|
+
/** The analytic chart roles: reference lines, threshold zones, forecast bands, events, partial periods. */
|
|
8
|
+
interface UixChartTokens {
|
|
9
|
+
palette: string[];
|
|
10
|
+
neutral: string;
|
|
11
|
+
grid: string;
|
|
12
|
+
axis: string;
|
|
13
|
+
reference: string;
|
|
14
|
+
zoneWarning: string;
|
|
15
|
+
zoneDanger: string;
|
|
16
|
+
forecastBand: string;
|
|
17
|
+
event: string;
|
|
18
|
+
partial: string;
|
|
19
|
+
comparison: string;
|
|
20
|
+
/** Status hues for an emphasised mark (a point that breaches); never a series colour. */
|
|
21
|
+
warning: string;
|
|
22
|
+
danger: string;
|
|
23
|
+
surface: string;
|
|
24
|
+
text: string;
|
|
25
|
+
textMuted: string;
|
|
26
|
+
border: string;
|
|
27
|
+
fontSans: string;
|
|
28
|
+
fontMono: string;
|
|
29
|
+
}
|
|
30
|
+
/** Resolve the chart tokens once (current theme). */
|
|
31
|
+
declare function uixChartTokens(source?: ChartTokenSource): UixChartTokens;
|
|
32
|
+
/**
|
|
33
|
+
* The base option every UIx chart merges under the consumer's option (the consumer wins).
|
|
34
|
+
* Axis defaults apply to every x/y axis, whether the consumer passes one axis or an array.
|
|
35
|
+
*/
|
|
36
|
+
declare function uixChartTheme(source?: ChartTokenSource): EChartsOption;
|
|
37
|
+
/** A vertical area fill that fades the series colour (28 % → 6 % → 0) toward the baseline. */
|
|
38
|
+
declare function uixChartAreaGradient(color: string): {
|
|
39
|
+
type: "linear";
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
x2: number;
|
|
43
|
+
y2: number;
|
|
44
|
+
colorStops: {
|
|
45
|
+
offset: number;
|
|
46
|
+
color: string;
|
|
47
|
+
}[];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Merge the theme under a consumer option: plain objects merge deeply, everything else (arrays,
|
|
51
|
+
* series, data, functions) is the consumer's. Array-valued components (`xAxis: [...]`, `title: [...]`)
|
|
52
|
+
* take the theme entry per element.
|
|
53
|
+
*/
|
|
54
|
+
declare function mergeChartTheme(theme: EChartsOption, option: EChartsOption): EChartsOption;
|
|
55
|
+
|
|
56
|
+
interface ChartEngine {
|
|
57
|
+
init(element: HTMLElement, theme?: string | null, options?: {
|
|
58
|
+
renderer?: 'svg' | 'canvas';
|
|
59
|
+
}): ECharts;
|
|
60
|
+
/** Optional: links charts that share a `group` (one crosshair and tooltip across cards). */
|
|
61
|
+
connect?(group: string): void;
|
|
62
|
+
}
|
|
63
|
+
/** An option, or a function that builds it from the chart tokens of the current theme (re-run on a theme flip). */
|
|
64
|
+
type ChartOption = EChartsOption | ((tokens: UixChartTokens) => EChartsOption);
|
|
65
|
+
/** A further text alternative: one visually hidden table (ChartPanels gives one per panel). */
|
|
66
|
+
interface ChartTable {
|
|
67
|
+
caption: string;
|
|
68
|
+
headers: string[];
|
|
69
|
+
rows: ChartTableRow[];
|
|
70
|
+
}
|
|
71
|
+
/** An ECharts event handler; `params` is ECharts' event object. */
|
|
72
|
+
type ChartEventHandler = (params: unknown) => void;
|
|
5
73
|
declare function uixChartPalette(): string[];
|
|
6
74
|
interface ChartTableRow {
|
|
7
75
|
[key: string]: string | number;
|
|
@@ -20,7 +88,8 @@ interface ChartLabels {
|
|
|
20
88
|
}
|
|
21
89
|
declare const DEFAULT_CHART_LABELS: ChartLabels;
|
|
22
90
|
interface ChartProps {
|
|
23
|
-
option
|
|
91
|
+
/** The ECharts option, or a function of the theme's chart tokens (re-run when the theme flips). */
|
|
92
|
+
option: ChartOption;
|
|
24
93
|
title?: string;
|
|
25
94
|
subtitle?: string;
|
|
26
95
|
height?: string | number;
|
|
@@ -42,6 +111,12 @@ interface ChartProps {
|
|
|
42
111
|
* grid and the UIx tooltip — and re-applies it when the page theme changes. `'none'` renders `option` as is.
|
|
43
112
|
*/
|
|
44
113
|
theme?: 'uix' | 'none';
|
|
114
|
+
/** Charts with the same group share one crosshair and tooltip (`echarts.connect`). */
|
|
115
|
+
group?: string;
|
|
116
|
+
/** ECharts event handlers by event name (`click` for drill-down). */
|
|
117
|
+
onEvents?: Record<string, ChartEventHandler>;
|
|
118
|
+
/** Further hidden data tables (after `tableData`), each named in the chart's description. */
|
|
119
|
+
tables?: ChartTable[];
|
|
45
120
|
}
|
|
46
121
|
interface ChartMetricProps extends HTMLAttributes<HTMLDivElement> {
|
|
47
122
|
value: ReactNode;
|
|
@@ -61,52 +136,4 @@ interface ChartLegendItemProps extends HTMLAttributes<HTMLLIElement> {
|
|
|
61
136
|
}
|
|
62
137
|
declare function ChartLegendItem({ label, color, swatch, className, style, ...props }: ChartLegendItemProps): react.JSX.Element;
|
|
63
138
|
|
|
64
|
-
|
|
65
|
-
type ChartTokenSource = Element | ((token: string) => string);
|
|
66
|
-
/** The analytic chart roles: reference lines, threshold zones, forecast bands, events, partial periods. */
|
|
67
|
-
interface UixChartTokens {
|
|
68
|
-
palette: string[];
|
|
69
|
-
neutral: string;
|
|
70
|
-
grid: string;
|
|
71
|
-
axis: string;
|
|
72
|
-
reference: string;
|
|
73
|
-
zoneWarning: string;
|
|
74
|
-
zoneDanger: string;
|
|
75
|
-
forecastBand: string;
|
|
76
|
-
event: string;
|
|
77
|
-
partial: string;
|
|
78
|
-
comparison: string;
|
|
79
|
-
surface: string;
|
|
80
|
-
text: string;
|
|
81
|
-
textMuted: string;
|
|
82
|
-
border: string;
|
|
83
|
-
fontSans: string;
|
|
84
|
-
fontMono: string;
|
|
85
|
-
}
|
|
86
|
-
/** Resolve the chart tokens once (current theme). */
|
|
87
|
-
declare function uixChartTokens(source?: ChartTokenSource): UixChartTokens;
|
|
88
|
-
/**
|
|
89
|
-
* The base option every UIx chart merges under the consumer's option (the consumer wins).
|
|
90
|
-
* Axis defaults apply to every x/y axis, whether the consumer passes one axis or an array.
|
|
91
|
-
*/
|
|
92
|
-
declare function uixChartTheme(source?: ChartTokenSource): EChartsOption;
|
|
93
|
-
/** A vertical area fill that fades the series colour (28 % → 6 % → 0) toward the baseline. */
|
|
94
|
-
declare function uixChartAreaGradient(color: string): {
|
|
95
|
-
type: "linear";
|
|
96
|
-
x: number;
|
|
97
|
-
y: number;
|
|
98
|
-
x2: number;
|
|
99
|
-
y2: number;
|
|
100
|
-
colorStops: {
|
|
101
|
-
offset: number;
|
|
102
|
-
color: string;
|
|
103
|
-
}[];
|
|
104
|
-
};
|
|
105
|
-
/**
|
|
106
|
-
* Merge the theme under a consumer option: plain objects merge deeply, everything else (arrays,
|
|
107
|
-
* series, data, functions) is the consumer's. Array-valued components (`xAxis: [...]`, `title: [...]`)
|
|
108
|
-
* take the theme entry per element.
|
|
109
|
-
*/
|
|
110
|
-
declare function mergeChartTheme(theme: EChartsOption, option: EChartsOption): EChartsOption;
|
|
111
|
-
|
|
112
|
-
export { type ChartProps as C, DEFAULT_CHART_LABELS as D, type UixChartTokens as U, type ChartLabels as a, ChartLegend as b, ChartLegendItem as c, type ChartLegendItemProps as d, type ChartLegendProps as e, ChartMetric as f, type ChartMetricProps as g, type ChartTableRow as h, type ChartTokenSource as i, uixChartPalette as j, uixChartTheme as k, uixChartTokens as l, mergeChartTheme as m, uixChartAreaGradient as u };
|
|
139
|
+
export { type ChartLabels as C, DEFAULT_CHART_LABELS as D, type UixChartTokens as U, type ChartProps as a, type ChartEngine as b, type ChartEventHandler as c, ChartLegend as d, ChartLegendItem as e, type ChartLegendItemProps as f, type ChartLegendProps as g, ChartMetric as h, type ChartMetricProps as i, type ChartOption as j, type ChartTable as k, type ChartTableRow as l, type ChartTokenSource as m, mergeChartTheme as n, uixChartPalette as o, uixChartTheme as p, uixChartTokens as q, uixChartAreaGradient as u };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { DEFAULT_CHART_PANELS_LABELS, chartPanelsModel } from './chart-panels-model.js';
|
|
2
|
+
export { DEFAULT_CORRELATION_SCATTER_LABELS, correlationScatterModel, formatCorrelation } from './correlation-scatter-model.js';
|
|
3
|
+
import 'echarts';
|
|
4
|
+
import './chart-theme.js';
|
|
5
|
+
import './chart-analytic-model.js';
|
|
6
|
+
import './chart-stats.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { chartPanelsModel } from "./chart-panels-model.js";
|
|
2
|
+
import { correlationScatterModel, formatCorrelation, DEFAULT_CORRELATION_SCATTER_LABELS } from "./correlation-scatter-model.js";
|
|
3
|
+
import { DEFAULT_CHART_PANELS_LABELS } from "./chart-panels-model.js";
|
|
4
|
+
export {
|
|
5
|
+
DEFAULT_CHART_PANELS_LABELS,
|
|
6
|
+
DEFAULT_CORRELATION_SCATTER_LABELS,
|
|
7
|
+
chartPanelsModel,
|
|
8
|
+
correlationScatterModel,
|
|
9
|
+
formatCorrelation
|
|
10
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { ScatterSeriesOption, MarkLineComponentOption, MarkAreaComponentOption, MarkPointComponentOption, LineSeriesOption } from 'echarts';
|
|
2
|
+
import { UixChartTokens } from './chart-theme.js';
|
|
3
|
+
|
|
4
|
+
/** A position on the x axis: a category name, or a time value (ms or ISO string) on a time axis. */
|
|
5
|
+
type ChartX = string | number;
|
|
6
|
+
/** A target line, a limit or a zone. `to` turns a line into a zone between `value` and `to`. */
|
|
7
|
+
interface ChartThreshold {
|
|
8
|
+
value: number;
|
|
9
|
+
to?: number;
|
|
10
|
+
kind: 'target' | 'limit' | 'floor' | 'budget';
|
|
11
|
+
tone: 'neutral' | 'warning' | 'danger';
|
|
12
|
+
label: string;
|
|
13
|
+
/** The side that is bad: shades from the line to the edge of the axis on that side. */
|
|
14
|
+
badSide?: 'above' | 'below';
|
|
15
|
+
}
|
|
16
|
+
/** Something that happened at (or from `at` to `to`) on the time axis. */
|
|
17
|
+
interface ChartEvent {
|
|
18
|
+
at: ChartX;
|
|
19
|
+
to?: ChartX;
|
|
20
|
+
kind: string;
|
|
21
|
+
label: string;
|
|
22
|
+
href?: string;
|
|
23
|
+
tone?: 'neutral' | 'warning' | 'danger';
|
|
24
|
+
}
|
|
25
|
+
/** A projection with its uncertainty, computed by the consumer. `from` is the last actual x. */
|
|
26
|
+
interface ChartForecast {
|
|
27
|
+
from: ChartX;
|
|
28
|
+
points: {
|
|
29
|
+
x: ChartX;
|
|
30
|
+
y: number;
|
|
31
|
+
lower: number;
|
|
32
|
+
upper: number;
|
|
33
|
+
}[];
|
|
34
|
+
/** Central coverage of the band, 0–1 (0.8 = 80 %). */
|
|
35
|
+
level: number;
|
|
36
|
+
/** How it was computed, for the tooltip and the table ("linear over 8 weeks"). */
|
|
37
|
+
method: string;
|
|
38
|
+
}
|
|
39
|
+
/** The previous period's values on the same x positions (a ghost line). */
|
|
40
|
+
interface ChartComparison {
|
|
41
|
+
label: string;
|
|
42
|
+
values: (number | null)[];
|
|
43
|
+
}
|
|
44
|
+
/** Every word the overlays draw. English defaults in {@link DEFAULT_ANALYTIC_LABELS}. */
|
|
45
|
+
interface ChartAnalyticLabels {
|
|
46
|
+
projection: string;
|
|
47
|
+
/** The band's name in the legend, the tooltip and the table, e.g. "80 % range". */
|
|
48
|
+
range: (level: number) => string;
|
|
49
|
+
partial: string;
|
|
50
|
+
crossed: (x: string) => string;
|
|
51
|
+
lower: string;
|
|
52
|
+
upper: string;
|
|
53
|
+
threshold: string;
|
|
54
|
+
event: string;
|
|
55
|
+
/** How an x value is written in labels and tables. */
|
|
56
|
+
formatX: (x: ChartX) => string;
|
|
57
|
+
}
|
|
58
|
+
declare const DEFAULT_ANALYTIC_LABELS: ChartAnalyticLabels;
|
|
59
|
+
/** Series-level mark fragments; spread into one series, or combine several with {@link combineMarks}. */
|
|
60
|
+
interface ChartMarks {
|
|
61
|
+
markLine?: MarkLineComponentOption;
|
|
62
|
+
markArea?: MarkAreaComponentOption;
|
|
63
|
+
markPoint?: MarkPointComponentOption;
|
|
64
|
+
}
|
|
65
|
+
/** Merge mark fragments into one (a series carries one markLine, one markArea and one markPoint). */
|
|
66
|
+
declare function combineMarks(...fragments: (ChartMarks | null | undefined)[]): ChartMarks;
|
|
67
|
+
/**
|
|
68
|
+
* Threshold lines and zones. A line is solid reference ink with its label in the right gutter
|
|
69
|
+
* (the plot needs `grid.right` ≥ 96 px; ChartPanels reserves it). A zone (`to`, or `badSide` with an
|
|
70
|
+
* axis `extent`) is a token wash. Lines never take a status hue: the zone carries the tone.
|
|
71
|
+
*/
|
|
72
|
+
declare function thresholdMarks(thresholds: readonly ChartThreshold[], options: {
|
|
73
|
+
tokens: UixChartTokens;
|
|
74
|
+
extent?: [number, number];
|
|
75
|
+
}): ChartMarks;
|
|
76
|
+
/** One pin in the event lane: [x, 0, index into the events]. */
|
|
77
|
+
type ChartEventPin = [ChartX, number, number];
|
|
78
|
+
/** The event lane: a thin row above the plot that owns the events' tooltip and click-through. */
|
|
79
|
+
interface ChartEventLane {
|
|
80
|
+
series: ScatterSeriesOption;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Events: unlabeled solid lines through the plot (ranges as a light wash) plus the pins of the lane.
|
|
84
|
+
* The tone is carried by the pin's shape (circle, triangle, diamond) in event ink, never by a status
|
|
85
|
+
* hue, so the lane stays calm. Put `lane.series` on the lane's grid (ChartPanels does).
|
|
86
|
+
*/
|
|
87
|
+
declare function eventMarks(events: readonly ChartEvent[], options: {
|
|
88
|
+
tokens: UixChartTokens;
|
|
89
|
+
xAxisIndex?: number;
|
|
90
|
+
yAxisIndex?: number;
|
|
91
|
+
seriesId?: string;
|
|
92
|
+
}): ChartMarks & {
|
|
93
|
+
lane: ChartEventLane;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* A forecast as three series: a hidden lower bound, the band stacked on it (upper − lower) in
|
|
97
|
+
* `--uix-chart-forecast-band`, and the dashed centre line in the series colour, starting at `from`.
|
|
98
|
+
* The centre carries a light "Projection" wash from `from` to the last point.
|
|
99
|
+
*/
|
|
100
|
+
declare function forecastSeries(forecast: ChartForecast, options: {
|
|
101
|
+
name: string;
|
|
102
|
+
colorIndex?: number;
|
|
103
|
+
tokens: UixChartTokens;
|
|
104
|
+
labels?: Partial<ChartAnalyticLabels>;
|
|
105
|
+
/** The last actual value, so the dashed line starts where the solid one ends. */
|
|
106
|
+
fromValue?: number | null;
|
|
107
|
+
xAxisIndex?: number;
|
|
108
|
+
yAxisIndex?: number;
|
|
109
|
+
idPrefix?: string;
|
|
110
|
+
}): LineSeriesOption[];
|
|
111
|
+
/** The previous period as a thin SOLID ghost line (dashes are reserved for projections). */
|
|
112
|
+
declare function comparisonSeries(comparison: ChartComparison, options: {
|
|
113
|
+
x: readonly ChartX[];
|
|
114
|
+
tokens: UixChartTokens;
|
|
115
|
+
xAxisIndex?: number;
|
|
116
|
+
yAxisIndex?: number;
|
|
117
|
+
id?: string;
|
|
118
|
+
}): LineSeriesOption;
|
|
119
|
+
/**
|
|
120
|
+
* The last, still-filling period: a shaded wash from the previous bucket (`from`) to `lastX`,
|
|
121
|
+
* labelled "Partial" at the foot of the plot. On a category line axis the points sit on the
|
|
122
|
+
* categories, so the wash spans the segment that leads into the partial point.
|
|
123
|
+
*/
|
|
124
|
+
declare function partialPeriod(lastX: ChartX, options: {
|
|
125
|
+
tokens: UixChartTokens;
|
|
126
|
+
from?: ChartX;
|
|
127
|
+
labels?: Partial<ChartAnalyticLabels>;
|
|
128
|
+
}): ChartMarks;
|
|
129
|
+
/**
|
|
130
|
+
* Indexes where the series first enters the bad side of a threshold (after being on the good side
|
|
131
|
+
* or at the start of a run of nulls). `badSide` defaults to `above` (`floor` thresholds: `below`).
|
|
132
|
+
*/
|
|
133
|
+
declare function findCrossings(values: readonly (number | null)[], threshold: Pick<ChartThreshold, 'value' | 'badSide' | 'kind'>): number[];
|
|
134
|
+
/** A ringed point at each first crossing, labelled "Crossed <x>" above it. */
|
|
135
|
+
declare function crossingPoints(values: readonly (number | null)[], threshold: ChartThreshold, options: {
|
|
136
|
+
x: readonly ChartX[];
|
|
137
|
+
tokens: UixChartTokens;
|
|
138
|
+
labels?: Partial<ChartAnalyticLabels>;
|
|
139
|
+
}): ChartMarks;
|
|
140
|
+
|
|
141
|
+
export { type ChartAnalyticLabels, type ChartComparison, type ChartEvent, type ChartEventLane, type ChartEventPin, type ChartForecast, type ChartMarks, type ChartThreshold, type ChartX, DEFAULT_ANALYTIC_LABELS, combineMarks, comparisonSeries, crossingPoints, eventMarks, findCrossings, forecastSeries, partialPeriod, thresholdMarks };
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
const DEFAULT_ANALYTIC_LABELS = {
|
|
2
|
+
projection: "Projection",
|
|
3
|
+
range: (level) => `${Math.round(level * 100)}% range`,
|
|
4
|
+
partial: "Partial",
|
|
5
|
+
crossed: (x) => `Crossed ${x}`,
|
|
6
|
+
lower: "Lower",
|
|
7
|
+
upper: "Upper",
|
|
8
|
+
threshold: "Threshold",
|
|
9
|
+
event: "Event",
|
|
10
|
+
formatX: (x) => String(x)
|
|
11
|
+
};
|
|
12
|
+
const GUTTER_LABEL_WIDTH = 88;
|
|
13
|
+
const markLineBase = (data) => ({
|
|
14
|
+
silent: true,
|
|
15
|
+
symbol: "none",
|
|
16
|
+
animation: false,
|
|
17
|
+
data
|
|
18
|
+
});
|
|
19
|
+
const markAreaBase = (data) => ({ silent: true, animation: false, data });
|
|
20
|
+
function combineMarks(...fragments) {
|
|
21
|
+
const lines = [];
|
|
22
|
+
const areas = [];
|
|
23
|
+
const points = [];
|
|
24
|
+
for (const fragment of fragments) {
|
|
25
|
+
if (!fragment) continue;
|
|
26
|
+
if (fragment.markLine?.data) lines.push(...fragment.markLine.data);
|
|
27
|
+
if (fragment.markArea?.data) areas.push(...fragment.markArea.data);
|
|
28
|
+
if (fragment.markPoint?.data) points.push(...fragment.markPoint.data);
|
|
29
|
+
}
|
|
30
|
+
const out = {};
|
|
31
|
+
if (lines.length) out.markLine = markLineBase(lines);
|
|
32
|
+
if (areas.length) out.markArea = markAreaBase(areas);
|
|
33
|
+
if (points.length) out.markPoint = { animation: false, data: points };
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
const zoneColor = (tone, tokens) => tone === "danger" ? tokens.zoneDanger : tone === "warning" ? tokens.zoneWarning : tokens.forecastBand;
|
|
37
|
+
function thresholdMarks(thresholds, options) {
|
|
38
|
+
const { tokens, extent } = options;
|
|
39
|
+
const lines = [];
|
|
40
|
+
const areas = [];
|
|
41
|
+
for (const threshold of thresholds) {
|
|
42
|
+
if (!Number.isFinite(threshold.value)) continue;
|
|
43
|
+
const zoneTo = threshold.to ?? (threshold.badSide && extent ? threshold.badSide === "above" ? extent[1] : extent[0] : void 0);
|
|
44
|
+
if (zoneTo != null && Number.isFinite(zoneTo) && zoneTo !== threshold.value) {
|
|
45
|
+
areas.push([
|
|
46
|
+
{ yAxis: Math.min(threshold.value, zoneTo), itemStyle: { color: zoneColor(threshold.tone, tokens) } },
|
|
47
|
+
{ yAxis: Math.max(threshold.value, zoneTo) }
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
lines.push({
|
|
51
|
+
yAxis: threshold.value,
|
|
52
|
+
lineStyle: { color: tokens.reference, width: 1, type: "solid" },
|
|
53
|
+
label: {
|
|
54
|
+
show: true,
|
|
55
|
+
position: "end",
|
|
56
|
+
formatter: threshold.label,
|
|
57
|
+
color: tokens.textMuted,
|
|
58
|
+
fontFamily: tokens.fontSans,
|
|
59
|
+
fontSize: 11,
|
|
60
|
+
width: GUTTER_LABEL_WIDTH,
|
|
61
|
+
overflow: "truncate",
|
|
62
|
+
distance: 6
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return combineMarks(
|
|
67
|
+
lines.length ? { markLine: markLineBase(lines) } : null,
|
|
68
|
+
areas.length ? { markArea: markAreaBase(areas) } : null
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const PIN_SYMBOL = {
|
|
72
|
+
neutral: "circle",
|
|
73
|
+
warning: "triangle",
|
|
74
|
+
danger: "diamond"
|
|
75
|
+
};
|
|
76
|
+
function eventMarks(events, options) {
|
|
77
|
+
const { tokens } = options;
|
|
78
|
+
const lines = [];
|
|
79
|
+
const areas = [];
|
|
80
|
+
events.forEach((event) => {
|
|
81
|
+
if (event.to != null) {
|
|
82
|
+
areas.push([{ xAxis: event.at, itemStyle: { color: tokens.forecastBand } }, { xAxis: event.to }]);
|
|
83
|
+
}
|
|
84
|
+
lines.push({ xAxis: event.at, lineStyle: { color: tokens.event, width: 1, type: "solid", opacity: 0.7 }, label: { show: false } });
|
|
85
|
+
});
|
|
86
|
+
const lane = {
|
|
87
|
+
series: {
|
|
88
|
+
id: options.seriesId ?? "uix-event-lane",
|
|
89
|
+
type: "scatter",
|
|
90
|
+
xAxisIndex: options.xAxisIndex ?? 0,
|
|
91
|
+
yAxisIndex: options.yAxisIndex ?? 0,
|
|
92
|
+
data: events.map((event, index) => ({
|
|
93
|
+
value: [event.at, 0, index],
|
|
94
|
+
symbol: PIN_SYMBOL[event.tone ?? "neutral"]
|
|
95
|
+
})),
|
|
96
|
+
symbolSize: 10,
|
|
97
|
+
itemStyle: { color: tokens.event, borderColor: tokens.surface, borderWidth: 2 },
|
|
98
|
+
emphasis: { scale: 1.3 },
|
|
99
|
+
cursor: "pointer",
|
|
100
|
+
animation: false,
|
|
101
|
+
z: 5
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
...combineMarks(
|
|
106
|
+
lines.length ? { markLine: markLineBase(lines) } : null,
|
|
107
|
+
areas.length ? { markArea: markAreaBase(areas) } : null
|
|
108
|
+
),
|
|
109
|
+
lane
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function forecastSeries(forecast, options) {
|
|
113
|
+
const labels = { ...DEFAULT_ANALYTIC_LABELS, ...options.labels };
|
|
114
|
+
const { tokens } = options;
|
|
115
|
+
const color = tokens.palette[(options.colorIndex ?? 0) % tokens.palette.length] || tokens.palette[0];
|
|
116
|
+
const stack = `${options.idPrefix ?? "forecast"}-band`;
|
|
117
|
+
const axes = { xAxisIndex: options.xAxisIndex ?? 0, yAxisIndex: options.yAxisIndex ?? 0 };
|
|
118
|
+
const points = forecast.points.filter((p) => [p.y, p.lower, p.upper].every(Number.isFinite));
|
|
119
|
+
const start = options.fromValue != null && Number.isFinite(options.fromValue) ? [{ x: forecast.from, y: options.fromValue, lower: options.fromValue, upper: options.fromValue }] : [];
|
|
120
|
+
const all = [...start, ...points];
|
|
121
|
+
const last = points[points.length - 1];
|
|
122
|
+
const prefix = options.idPrefix ?? "forecast";
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
id: `${prefix}-lower`,
|
|
126
|
+
name: `${options.name} \xB7 ${labels.lower}`,
|
|
127
|
+
type: "line",
|
|
128
|
+
...axes,
|
|
129
|
+
stack,
|
|
130
|
+
data: all.map((p) => [p.x, p.lower]),
|
|
131
|
+
lineStyle: { opacity: 0 },
|
|
132
|
+
symbol: "none",
|
|
133
|
+
silent: true,
|
|
134
|
+
tooltip: { show: false },
|
|
135
|
+
animation: false,
|
|
136
|
+
z: 1
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: `${prefix}-band`,
|
|
140
|
+
name: `${options.name} \xB7 ${labels.range(forecast.level)}`,
|
|
141
|
+
type: "line",
|
|
142
|
+
...axes,
|
|
143
|
+
stack,
|
|
144
|
+
data: all.map((p) => [p.x, p.upper - p.lower]),
|
|
145
|
+
lineStyle: { opacity: 0 },
|
|
146
|
+
areaStyle: { color: tokens.forecastBand, opacity: 1 },
|
|
147
|
+
symbol: "none",
|
|
148
|
+
silent: true,
|
|
149
|
+
tooltip: { show: false },
|
|
150
|
+
animation: false,
|
|
151
|
+
z: 1
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: `${prefix}-centre`,
|
|
155
|
+
name: `${options.name} \xB7 ${labels.projection}`,
|
|
156
|
+
type: "line",
|
|
157
|
+
...axes,
|
|
158
|
+
data: all.map((p) => [p.x, p.y]),
|
|
159
|
+
lineStyle: { color, width: 2, type: "dashed" },
|
|
160
|
+
itemStyle: { color },
|
|
161
|
+
symbol: "none",
|
|
162
|
+
animation: false,
|
|
163
|
+
z: 3,
|
|
164
|
+
...last ? {
|
|
165
|
+
markArea: markAreaBase([[
|
|
166
|
+
{
|
|
167
|
+
xAxis: forecast.from,
|
|
168
|
+
itemStyle: { color: "transparent" },
|
|
169
|
+
label: { show: true, position: "insideTop", formatter: labels.projection, color: tokens.textMuted, fontFamily: tokens.fontSans, fontSize: 11 }
|
|
170
|
+
},
|
|
171
|
+
{ xAxis: last.x }
|
|
172
|
+
]])
|
|
173
|
+
} : {}
|
|
174
|
+
}
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
function comparisonSeries(comparison, options) {
|
|
178
|
+
return {
|
|
179
|
+
id: options.id ?? "comparison",
|
|
180
|
+
name: comparison.label,
|
|
181
|
+
type: "line",
|
|
182
|
+
xAxisIndex: options.xAxisIndex ?? 0,
|
|
183
|
+
yAxisIndex: options.yAxisIndex ?? 0,
|
|
184
|
+
data: options.x.map((x, i) => [x, comparison.values[i] ?? null]),
|
|
185
|
+
lineStyle: { color: options.tokens.comparison, width: 1.5, type: "solid" },
|
|
186
|
+
itemStyle: { color: options.tokens.comparison },
|
|
187
|
+
symbol: "none",
|
|
188
|
+
connectNulls: false,
|
|
189
|
+
animation: false,
|
|
190
|
+
z: 1
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function partialPeriod(lastX, options) {
|
|
194
|
+
const labels = { ...DEFAULT_ANALYTIC_LABELS, ...options.labels };
|
|
195
|
+
return {
|
|
196
|
+
markArea: markAreaBase([[
|
|
197
|
+
{
|
|
198
|
+
xAxis: options.from ?? lastX,
|
|
199
|
+
itemStyle: { color: options.tokens.partial },
|
|
200
|
+
label: { show: true, position: "insideBottom", formatter: labels.partial, color: options.tokens.textMuted, fontFamily: options.tokens.fontSans, fontSize: 11 }
|
|
201
|
+
},
|
|
202
|
+
{ xAxis: lastX }
|
|
203
|
+
]])
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function findCrossings(values, threshold) {
|
|
207
|
+
const side = threshold.badSide ?? (threshold.kind === "floor" ? "below" : "above");
|
|
208
|
+
const bad = (v) => side === "above" ? v > threshold.value : v < threshold.value;
|
|
209
|
+
const out = [];
|
|
210
|
+
let previous = null;
|
|
211
|
+
values.forEach((value, index) => {
|
|
212
|
+
if (value == null || !Number.isFinite(value)) {
|
|
213
|
+
previous = null;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const isBad = bad(value);
|
|
217
|
+
if (isBad && previous === false) out.push(index);
|
|
218
|
+
previous = isBad;
|
|
219
|
+
});
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
function crossingPoints(values, threshold, options) {
|
|
223
|
+
const labels = { ...DEFAULT_ANALYTIC_LABELS, ...options.labels };
|
|
224
|
+
const data = findCrossings(values, threshold).map((index) => ({
|
|
225
|
+
name: labels.crossed(labels.formatX(options.x[index])),
|
|
226
|
+
coord: [options.x[index], values[index]],
|
|
227
|
+
symbol: "circle",
|
|
228
|
+
symbolSize: 10,
|
|
229
|
+
itemStyle: { color: options.tokens.surface, borderColor: options.tokens.text, borderWidth: 2 },
|
|
230
|
+
label: {
|
|
231
|
+
show: true,
|
|
232
|
+
position: "top",
|
|
233
|
+
distance: 8,
|
|
234
|
+
formatter: labels.crossed(labels.formatX(options.x[index])),
|
|
235
|
+
color: options.tokens.text,
|
|
236
|
+
fontFamily: options.tokens.fontSans,
|
|
237
|
+
fontSize: 11
|
|
238
|
+
}
|
|
239
|
+
}));
|
|
240
|
+
return data.length ? { markPoint: { animation: false, data } } : {};
|
|
241
|
+
}
|
|
242
|
+
export {
|
|
243
|
+
DEFAULT_ANALYTIC_LABELS,
|
|
244
|
+
combineMarks,
|
|
245
|
+
comparisonSeries,
|
|
246
|
+
crossingPoints,
|
|
247
|
+
eventMarks,
|
|
248
|
+
findCrossings,
|
|
249
|
+
forecastSeries,
|
|
250
|
+
partialPeriod,
|
|
251
|
+
thresholdMarks
|
|
252
|
+
};
|