@chipmobilesdk/rn-chart 0.2.0 → 0.2.1
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/CHANGELOG.md +126 -115
- package/README.md +381 -381
- package/package.json +46 -49
- package/skills/sdk-rn-chart/SKILL.md +68 -69
- package/src/a11y/summary.ts +170 -177
- package/src/contract/datasource.ts +126 -136
- package/src/contract/errors.ts +129 -133
- package/src/contract/format.ts +162 -168
- package/src/contract/interaction.ts +70 -71
- package/src/contract/marker.ts +75 -75
- package/src/contract/props.ts +57 -60
- package/src/contract/state.ts +88 -94
- package/src/contract/theme.ts +169 -177
- package/src/contract/value.ts +121 -125
- package/src/geometry/bar.ts +271 -273
- package/src/geometry/distribution.ts +197 -203
- package/src/geometry/heatmap.ts +189 -195
- package/src/geometry/hitTest.ts +117 -120
- package/src/geometry/layout.ts +162 -173
- package/src/geometry/markers.ts +55 -57
- package/src/geometry/path.ts +193 -199
- package/src/geometry/progress.ts +223 -230
- package/src/geometry/scale.ts +140 -146
- package/src/geometry/ticks.ts +145 -153
- package/src/geometry/timeSeries.ts +168 -168
- package/src/geometry/types.ts +94 -94
- package/src/index.ts +158 -176
- package/src/react/ChartBoundary.tsx +96 -96
- package/src/react/ChartFrame.tsx +583 -622
- package/src/react/ChartLegend.tsx +211 -215
- package/src/react/ChartThemeProvider.tsx +44 -48
- package/src/react/charts.tsx +154 -161
- package/src/react/useChartLayout.ts +124 -124
- package/src/react/useChartSelection.ts +152 -170
- package/src/render/Axis.tsx +120 -122
- package/src/render/Marks.tsx +173 -205
- package/src/validation/budgets.ts +103 -110
- package/src/validation/validateDatasource.ts +174 -174
package/src/contract/props.ts
CHANGED
|
@@ -1,60 +1,57 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Props shared by every chart family.
|
|
3
|
-
*
|
|
4
|
-
* See specs/007-rn-chart-package/contracts/public-api.md §2 for the contract
|
|
5
|
-
* behind each field.
|
|
6
|
-
*/
|
|
7
|
-
import type { ReactNode } from 'react';
|
|
8
|
-
import type { StyleProp, ViewStyle } from 'react-native';
|
|
9
|
-
|
|
10
|
-
import type { ChartDatasource } from './datasource';
|
|
11
|
-
import type { ChartFailureHandler } from './errors';
|
|
12
|
-
import type { ChartFormatters, FormatContext } from './format';
|
|
13
|
-
import type { InteractionProps } from './interaction';
|
|
14
|
-
import type { ChartStateContent, DeclarableChartStatus } from './state';
|
|
15
|
-
import type { ChartTheme } from './theme';
|
|
16
|
-
import type { ValueState } from './value';
|
|
17
|
-
|
|
18
|
-
export interface ChartAccessibility<M = unknown> {
|
|
19
|
-
/** Replaces the composed summary entirely. */
|
|
20
|
-
readonly label?: string;
|
|
21
|
-
readonly summaryFormatter?: (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
readonly
|
|
41
|
-
readonly
|
|
42
|
-
|
|
43
|
-
readonly
|
|
44
|
-
readonly
|
|
45
|
-
|
|
46
|
-
readonly
|
|
47
|
-
|
|
48
|
-
readonly
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
readonly
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
readonly
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export type ValueFormatter = (value: ValueState) => string;
|
|
1
|
+
/**
|
|
2
|
+
* Props shared by every chart family.
|
|
3
|
+
*
|
|
4
|
+
* See specs/007-rn-chart-package/contracts/public-api.md §2 for the contract
|
|
5
|
+
* behind each field.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReactNode } from 'react';
|
|
8
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
9
|
+
|
|
10
|
+
import type { ChartDatasource } from './datasource';
|
|
11
|
+
import type { ChartFailureHandler } from './errors';
|
|
12
|
+
import type { ChartFormatters, FormatContext } from './format';
|
|
13
|
+
import type { InteractionProps } from './interaction';
|
|
14
|
+
import type { ChartStateContent, DeclarableChartStatus } from './state';
|
|
15
|
+
import type { ChartTheme } from './theme';
|
|
16
|
+
import type { ValueState } from './value';
|
|
17
|
+
|
|
18
|
+
export interface ChartAccessibility<M = unknown> {
|
|
19
|
+
/** Replaces the composed summary entirely. */
|
|
20
|
+
readonly label?: string;
|
|
21
|
+
readonly summaryFormatter?: (datasource: ChartDatasource<M>, ctx: FormatContext) => string;
|
|
22
|
+
/** Defaults to true when `onSelectionChange` is supplied. */
|
|
23
|
+
readonly interactive?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BaseChartProps<M = unknown> extends InteractionProps<M> {
|
|
27
|
+
/**
|
|
28
|
+
* Must be referentially stable across renders, or memoization is defeated.
|
|
29
|
+
* Hold it in state or a memo rather than building it inline.
|
|
30
|
+
*/
|
|
31
|
+
readonly datasource: ChartDatasource<M>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `empty` and `partial` are derived from the data and are not accepted here
|
|
35
|
+
* (FR-036).
|
|
36
|
+
*/
|
|
37
|
+
readonly status?: DeclarableChartStatus;
|
|
38
|
+
readonly stateContent?: ChartStateContent;
|
|
39
|
+
|
|
40
|
+
readonly chartId?: string;
|
|
41
|
+
readonly onFailure?: ChartFailureHandler;
|
|
42
|
+
|
|
43
|
+
readonly theme?: ChartTheme;
|
|
44
|
+
readonly formatters?: ChartFormatters;
|
|
45
|
+
readonly title?: string;
|
|
46
|
+
readonly legend?: boolean | { readonly position: 'top' | 'bottom' };
|
|
47
|
+
|
|
48
|
+
readonly accessibility?: ChartAccessibility<M>;
|
|
49
|
+
|
|
50
|
+
/** Width always comes from the container. */
|
|
51
|
+
readonly height?: number;
|
|
52
|
+
readonly style?: StyleProp<ViewStyle>;
|
|
53
|
+
/** Rendered below the plot, inside the chart's own bounds. */
|
|
54
|
+
readonly footer?: ReactNode;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type ValueFormatter = (value: ValueState) => string;
|
package/src/contract/state.ts
CHANGED
|
@@ -1,94 +1,88 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chart status contract (FR-029 to FR-038).
|
|
3
|
-
*
|
|
4
|
-
* Ownership is split on a simple principle: the package derives what only the
|
|
5
|
-
* data can determine, and the app declares what only the app can know.
|
|
6
|
-
*
|
|
7
|
-
* empty, partial -> derived from the datasource
|
|
8
|
-
* loading, stale, -> declared by the app: it alone knows about its own
|
|
9
|
-
* noPermission, fetches, cache age, access rules, and failures
|
|
10
|
-
* error
|
|
11
|
-
*/
|
|
12
|
-
import type { ReactNode } from 'react';
|
|
13
|
-
|
|
14
|
-
import type { ChartError } from './errors';
|
|
15
|
-
|
|
16
|
-
export type ChartStatus =
|
|
17
|
-
| '
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
readonly
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (inputs.
|
|
60
|
-
candidates.add(
|
|
61
|
-
}
|
|
62
|
-
if (inputs.
|
|
63
|
-
candidates.add('
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
export function suppressesInteraction(status: ChartStatus): boolean {
|
|
90
|
-
return !drawsData(status);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Per-status content the app supplies, replacing any package wording (FR-035). */
|
|
94
|
-
export type ChartStateContent = Partial<Record<ChartStatus, ReactNode>>;
|
|
1
|
+
/**
|
|
2
|
+
* Chart status contract (FR-029 to FR-038).
|
|
3
|
+
*
|
|
4
|
+
* Ownership is split on a simple principle: the package derives what only the
|
|
5
|
+
* data can determine, and the app declares what only the app can know.
|
|
6
|
+
*
|
|
7
|
+
* empty, partial -> derived from the datasource
|
|
8
|
+
* loading, stale, -> declared by the app: it alone knows about its own
|
|
9
|
+
* noPermission, fetches, cache age, access rules, and failures
|
|
10
|
+
* error
|
|
11
|
+
*/
|
|
12
|
+
import type { ReactNode } from 'react';
|
|
13
|
+
|
|
14
|
+
import type { ChartError } from './errors';
|
|
15
|
+
|
|
16
|
+
export type ChartStatus =
|
|
17
|
+
'loading' | 'empty' | 'ready' | 'error' | 'stale' | 'noPermission' | 'partial';
|
|
18
|
+
|
|
19
|
+
/** The statuses an app may declare. `empty` and `partial` are derived. */
|
|
20
|
+
export type DeclarableChartStatus = Exclude<ChartStatus, 'empty' | 'partial'>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolution precedence, highest first.
|
|
24
|
+
*
|
|
25
|
+
* `noPermission` outranks everything so restricted values can never slip
|
|
26
|
+
* through some other state. `stale` and `partial` sit below the blocking
|
|
27
|
+
* states because they are modifiers on a chart that still draws its data.
|
|
28
|
+
*/
|
|
29
|
+
export const STATUS_PRECEDENCE: readonly ChartStatus[] = [
|
|
30
|
+
'noPermission',
|
|
31
|
+
'error',
|
|
32
|
+
'loading',
|
|
33
|
+
'empty',
|
|
34
|
+
'stale',
|
|
35
|
+
'partial',
|
|
36
|
+
'ready',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export interface StatusInputs {
|
|
40
|
+
/** What the app declared, if anything. */
|
|
41
|
+
readonly declared?: DeclarableChartStatus;
|
|
42
|
+
/** True when the datasource carries no points at all. */
|
|
43
|
+
readonly isEmpty: boolean;
|
|
44
|
+
/** True when some points are valid and others are unknown/n-a/withheld. */
|
|
45
|
+
readonly isPartial: boolean;
|
|
46
|
+
/** A validation failure or a contained render exception. */
|
|
47
|
+
readonly error?: ChartError;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveStatus(inputs: StatusInputs): ChartStatus {
|
|
51
|
+
const candidates = new Set<ChartStatus>(['ready']);
|
|
52
|
+
|
|
53
|
+
if (inputs.declared) {
|
|
54
|
+
candidates.add(inputs.declared);
|
|
55
|
+
}
|
|
56
|
+
if (inputs.error) {
|
|
57
|
+
candidates.add('error');
|
|
58
|
+
}
|
|
59
|
+
if (inputs.isEmpty) {
|
|
60
|
+
candidates.add('empty');
|
|
61
|
+
}
|
|
62
|
+
if (inputs.isPartial) {
|
|
63
|
+
candidates.add('partial');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const status of STATUS_PRECEDENCE) {
|
|
67
|
+
if (candidates.has(status)) {
|
|
68
|
+
return status;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return 'ready';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Statuses in which the chart draws its data (possibly with an indicator). */
|
|
75
|
+
export function drawsData(status: ChartStatus): boolean {
|
|
76
|
+
return status === 'ready' || status === 'stale' || status === 'partial';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Statuses in which no data-selection callback may fire, because the chart is
|
|
81
|
+
* not displaying the values a callback would carry (FR-053).
|
|
82
|
+
*/
|
|
83
|
+
export function suppressesInteraction(status: ChartStatus): boolean {
|
|
84
|
+
return !drawsData(status);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Per-status content the app supplies, replacing any package wording (FR-035). */
|
|
88
|
+
export type ChartStateContent = Partial<Record<ChartStatus, ReactNode>>;
|
package/src/contract/theme.ts
CHANGED
|
@@ -1,177 +1,169 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Theme contract (FR-060 to FR-065).
|
|
3
|
-
*
|
|
4
|
-
* A plain value type rather than a dependency. The package declares what it
|
|
5
|
-
* needs and any design system can satisfy it — which is what keeps
|
|
6
|
-
* `@chipmobilesdk/rn-theme` and `react-native-unistyles` out of this package's
|
|
7
|
-
* dependency list entirely. The demo app owns the mapping adapter.
|
|
8
|
-
*
|
|
9
|
-
* Non-colour encodings are first-class here, because meaning must never ride on
|
|
10
|
-
* hue alone (FR-064).
|
|
11
|
-
*/
|
|
12
|
-
import type { ValueKind } from './value';
|
|
13
|
-
|
|
14
|
-
export interface TextStyleTokens {
|
|
15
|
-
readonly fontSize: number;
|
|
16
|
-
readonly fontWeight?: '400' | '500' | '600' | '700';
|
|
17
|
-
readonly color: string;
|
|
18
|
-
readonly letterSpacing?: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** How the three non-value classes are drawn: texture, never a hue difference. */
|
|
22
|
-
export type NonValueTexture = 'gap' | 'hatch' | 'lockedHatch';
|
|
23
|
-
|
|
24
|
-
export interface ChartTheme {
|
|
25
|
-
readonly colorScheme: 'light' | 'dark';
|
|
26
|
-
/** Categorical fallback, at least 8 entries, distinguishable in greyscale. */
|
|
27
|
-
readonly palette: readonly string[];
|
|
28
|
-
readonly surface: string;
|
|
29
|
-
readonly onSurface: string;
|
|
30
|
-
readonly axis: string;
|
|
31
|
-
readonly grid: string;
|
|
32
|
-
readonly muted: string;
|
|
33
|
-
readonly typography: {
|
|
34
|
-
readonly axisLabel: TextStyleTokens;
|
|
35
|
-
readonly legendLabel: TextStyleTokens;
|
|
36
|
-
readonly valueLabel: TextStyleTokens;
|
|
37
|
-
};
|
|
38
|
-
readonly spacing: {
|
|
39
|
-
readonly xs: number;
|
|
40
|
-
readonly sm: number;
|
|
41
|
-
readonly md: number;
|
|
42
|
-
};
|
|
43
|
-
readonly radius: {
|
|
44
|
-
readonly bar: number;
|
|
45
|
-
readonly cell: number;
|
|
46
|
-
};
|
|
47
|
-
readonly nonValue: Readonly<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
readonly
|
|
56
|
-
|
|
57
|
-
readonly
|
|
58
|
-
readonly
|
|
59
|
-
readonly
|
|
60
|
-
readonly
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
'#
|
|
75
|
-
'#
|
|
76
|
-
'#
|
|
77
|
-
'#
|
|
78
|
-
'#
|
|
79
|
-
'#
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
]
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'#
|
|
86
|
-
'#
|
|
87
|
-
'#
|
|
88
|
-
'#
|
|
89
|
-
'#
|
|
90
|
-
'#
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
for (const point of series.points) {
|
|
171
|
-
const override = point.color ?? (series.points.length === 1 ? series.color : undefined);
|
|
172
|
-
colors[segmentKey(series.id, point.id)] = resolveSeriesColor(theme, index, override);
|
|
173
|
-
index += 1;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return colors;
|
|
177
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Theme contract (FR-060 to FR-065).
|
|
3
|
+
*
|
|
4
|
+
* A plain value type rather than a dependency. The package declares what it
|
|
5
|
+
* needs and any design system can satisfy it — which is what keeps
|
|
6
|
+
* `@chipmobilesdk/rn-theme` and `react-native-unistyles` out of this package's
|
|
7
|
+
* dependency list entirely. The demo app owns the mapping adapter.
|
|
8
|
+
*
|
|
9
|
+
* Non-colour encodings are first-class here, because meaning must never ride on
|
|
10
|
+
* hue alone (FR-064).
|
|
11
|
+
*/
|
|
12
|
+
import type { ValueKind } from './value';
|
|
13
|
+
|
|
14
|
+
export interface TextStyleTokens {
|
|
15
|
+
readonly fontSize: number;
|
|
16
|
+
readonly fontWeight?: '400' | '500' | '600' | '700';
|
|
17
|
+
readonly color: string;
|
|
18
|
+
readonly letterSpacing?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** How the three non-value classes are drawn: texture, never a hue difference. */
|
|
22
|
+
export type NonValueTexture = 'gap' | 'hatch' | 'lockedHatch';
|
|
23
|
+
|
|
24
|
+
export interface ChartTheme {
|
|
25
|
+
readonly colorScheme: 'light' | 'dark';
|
|
26
|
+
/** Categorical fallback, at least 8 entries, distinguishable in greyscale. */
|
|
27
|
+
readonly palette: readonly string[];
|
|
28
|
+
readonly surface: string;
|
|
29
|
+
readonly onSurface: string;
|
|
30
|
+
readonly axis: string;
|
|
31
|
+
readonly grid: string;
|
|
32
|
+
readonly muted: string;
|
|
33
|
+
readonly typography: {
|
|
34
|
+
readonly axisLabel: TextStyleTokens;
|
|
35
|
+
readonly legendLabel: TextStyleTokens;
|
|
36
|
+
readonly valueLabel: TextStyleTokens;
|
|
37
|
+
};
|
|
38
|
+
readonly spacing: {
|
|
39
|
+
readonly xs: number;
|
|
40
|
+
readonly sm: number;
|
|
41
|
+
readonly md: number;
|
|
42
|
+
};
|
|
43
|
+
readonly radius: {
|
|
44
|
+
readonly bar: number;
|
|
45
|
+
readonly cell: number;
|
|
46
|
+
};
|
|
47
|
+
readonly nonValue: Readonly<Record<Exclude<ValueKind, 'known'>, NonValueTexture>>;
|
|
48
|
+
/** Ramp for heatmap intensity, low to high. */
|
|
49
|
+
readonly heatmapRamp: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface LegendEntry {
|
|
53
|
+
readonly seriesId: string;
|
|
54
|
+
/** Set on distribution entries, which describe one segment rather than a series. */
|
|
55
|
+
readonly pointId?: string;
|
|
56
|
+
readonly label: string;
|
|
57
|
+
readonly color: string;
|
|
58
|
+
readonly pattern: 'solid' | 'dashed' | 'dotted' | 'dashDot';
|
|
59
|
+
readonly visible: boolean;
|
|
60
|
+
readonly toggleable: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Eight categorical colours per scheme.
|
|
65
|
+
*
|
|
66
|
+
* Chosen for separation in hue *and* lightness, so they stay distinguishable
|
|
67
|
+
* under both common colour-vision deficiencies and a greyscale review. Colour
|
|
68
|
+
* is never the only carrier of meaning regardless — series also differ by
|
|
69
|
+
* legend label and, for lines, by dash pattern.
|
|
70
|
+
*/
|
|
71
|
+
const LIGHT_PALETTE: readonly string[] = [
|
|
72
|
+
'#2563EB',
|
|
73
|
+
'#D97706',
|
|
74
|
+
'#059669',
|
|
75
|
+
'#DB2777',
|
|
76
|
+
'#7C3AED',
|
|
77
|
+
'#0891B2',
|
|
78
|
+
'#B45309',
|
|
79
|
+
'#4B5563',
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const DARK_PALETTE: readonly string[] = [
|
|
83
|
+
'#60A5FA',
|
|
84
|
+
'#FBBF24',
|
|
85
|
+
'#34D399',
|
|
86
|
+
'#F472B6',
|
|
87
|
+
'#A78BFA',
|
|
88
|
+
'#22D3EE',
|
|
89
|
+
'#FCD34D',
|
|
90
|
+
'#9CA3AF',
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const NON_VALUE_TEXTURES = {
|
|
94
|
+
unknown: 'gap',
|
|
95
|
+
notApplicable: 'hatch',
|
|
96
|
+
withheld: 'lockedHatch',
|
|
97
|
+
} as const;
|
|
98
|
+
|
|
99
|
+
export function defaultChartTheme(colorScheme: 'light' | 'dark' = 'light'): ChartTheme {
|
|
100
|
+
const dark = colorScheme === 'dark';
|
|
101
|
+
|
|
102
|
+
const onSurface = dark ? '#F3F4F6' : '#111827';
|
|
103
|
+
const muted = dark ? '#9CA3AF' : '#6B7280';
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
colorScheme,
|
|
107
|
+
palette: dark ? DARK_PALETTE : LIGHT_PALETTE,
|
|
108
|
+
surface: dark ? '#111827' : '#FFFFFF',
|
|
109
|
+
onSurface,
|
|
110
|
+
axis: dark ? '#4B5563' : '#D1D5DB',
|
|
111
|
+
grid: dark ? '#374151' : '#E5E7EB',
|
|
112
|
+
muted,
|
|
113
|
+
typography: {
|
|
114
|
+
axisLabel: { fontSize: 11, fontWeight: '400', color: muted },
|
|
115
|
+
legendLabel: { fontSize: 12, fontWeight: '500', color: onSurface },
|
|
116
|
+
valueLabel: { fontSize: 13, fontWeight: '600', color: onSurface },
|
|
117
|
+
},
|
|
118
|
+
spacing: { xs: 4, sm: 8, md: 16 },
|
|
119
|
+
radius: { bar: 3, cell: 2 },
|
|
120
|
+
nonValue: NON_VALUE_TEXTURES,
|
|
121
|
+
heatmapRamp: dark
|
|
122
|
+
? ['#1F2937', '#1E3A8A', '#1D4ED8', '#3B82F6', '#93C5FD']
|
|
123
|
+
: ['#EFF6FF', '#BFDBFE', '#60A5FA', '#2563EB', '#1E3A8A'],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Assigns a colour to a series: explicit override wins, otherwise the palette
|
|
129
|
+
* cycles by index so the remaining series stay distinguishable (FR-062, FR-063).
|
|
130
|
+
*/
|
|
131
|
+
export function resolveSeriesColor(theme: ChartTheme, index: number, override?: string): string {
|
|
132
|
+
return override ?? theme.palette[index % theme.palette.length];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Key of one distribution segment: point ids are only unique within a series. */
|
|
136
|
+
export function segmentKey(seriesId: string, pointId: string): string {
|
|
137
|
+
return `${seriesId}/${pointId}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Colours for distribution segments, keyed by `segmentKey`. The single source
|
|
142
|
+
* for both the arcs and the legend, so the two cannot disagree (CHT-02).
|
|
143
|
+
*
|
|
144
|
+
* Segments take palette entries in order across the whole datasource, so one
|
|
145
|
+
* series of four points and four series of one point both give four colours.
|
|
146
|
+
* A point's own `color` wins; a series with a single point is that segment, so
|
|
147
|
+
* the series `color` applies to it.
|
|
148
|
+
*/
|
|
149
|
+
export function resolveSegmentColors(
|
|
150
|
+
datasource: {
|
|
151
|
+
readonly series: readonly {
|
|
152
|
+
readonly id: string;
|
|
153
|
+
readonly color?: string;
|
|
154
|
+
readonly points: readonly { readonly id: string; readonly color?: string }[];
|
|
155
|
+
}[];
|
|
156
|
+
},
|
|
157
|
+
theme: ChartTheme,
|
|
158
|
+
): Record<string, string> {
|
|
159
|
+
const colors: Record<string, string> = {};
|
|
160
|
+
let index = 0;
|
|
161
|
+
for (const series of datasource.series) {
|
|
162
|
+
for (const point of series.points) {
|
|
163
|
+
const override = point.color ?? (series.points.length === 1 ? series.color : undefined);
|
|
164
|
+
colors[segmentKey(series.id, point.id)] = resolveSeriesColor(theme, index, override);
|
|
165
|
+
index += 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return colors;
|
|
169
|
+
}
|