@opendata-ai/openchart-core 2.0.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 +130 -0
- package/dist/index.d.ts +2030 -0
- package/dist/index.js +1176 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +757 -0
- package/package.json +61 -0
- package/src/accessibility/__tests__/alt-text.test.ts +110 -0
- package/src/accessibility/__tests__/aria.test.ts +125 -0
- package/src/accessibility/alt-text.ts +120 -0
- package/src/accessibility/aria.ts +73 -0
- package/src/accessibility/index.ts +6 -0
- package/src/colors/__tests__/colorblind.test.ts +63 -0
- package/src/colors/__tests__/contrast.test.ts +71 -0
- package/src/colors/__tests__/palettes.test.ts +54 -0
- package/src/colors/colorblind.ts +122 -0
- package/src/colors/contrast.ts +94 -0
- package/src/colors/index.ts +27 -0
- package/src/colors/palettes.ts +118 -0
- package/src/helpers/__tests__/spec-builders.test.ts +336 -0
- package/src/helpers/spec-builders.ts +410 -0
- package/src/index.ts +129 -0
- package/src/labels/__tests__/collision.test.ts +197 -0
- package/src/labels/collision.ts +154 -0
- package/src/labels/index.ts +6 -0
- package/src/layout/__tests__/chrome.test.ts +114 -0
- package/src/layout/__tests__/text-measure.test.ts +49 -0
- package/src/layout/chrome.ts +223 -0
- package/src/layout/index.ts +6 -0
- package/src/layout/text-measure.ts +54 -0
- package/src/locale/__tests__/format.test.ts +90 -0
- package/src/locale/format.ts +132 -0
- package/src/locale/index.ts +6 -0
- package/src/responsive/__tests__/breakpoints.test.ts +58 -0
- package/src/responsive/breakpoints.ts +92 -0
- package/src/responsive/index.ts +18 -0
- package/src/styles/viz.css +757 -0
- package/src/theme/__tests__/dark-mode.test.ts +68 -0
- package/src/theme/__tests__/defaults.test.ts +47 -0
- package/src/theme/__tests__/resolve.test.ts +61 -0
- package/src/theme/dark-mode.ts +123 -0
- package/src/theme/defaults.ts +85 -0
- package/src/theme/index.ts +7 -0
- package/src/theme/resolve.ts +190 -0
- package/src/types/__tests__/spec.test.ts +387 -0
- package/src/types/encoding.ts +144 -0
- package/src/types/events.ts +96 -0
- package/src/types/index.ts +141 -0
- package/src/types/layout.ts +794 -0
- package/src/types/spec.ts +563 -0
- package/src/types/table.ts +105 -0
- package/src/types/theme.ts +159 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1176 @@
|
|
|
1
|
+
// src/types/encoding.ts
|
|
2
|
+
function required(...types) {
|
|
3
|
+
return { required: true, allowedTypes: types };
|
|
4
|
+
}
|
|
5
|
+
function optional(...types) {
|
|
6
|
+
return { required: false, allowedTypes: types };
|
|
7
|
+
}
|
|
8
|
+
var CHART_ENCODING_RULES = {
|
|
9
|
+
line: {
|
|
10
|
+
x: required("temporal", "ordinal"),
|
|
11
|
+
y: required("quantitative"),
|
|
12
|
+
color: optional("nominal", "ordinal"),
|
|
13
|
+
size: optional("quantitative"),
|
|
14
|
+
detail: optional("nominal")
|
|
15
|
+
},
|
|
16
|
+
area: {
|
|
17
|
+
x: required("temporal", "ordinal"),
|
|
18
|
+
y: required("quantitative"),
|
|
19
|
+
color: optional("nominal", "ordinal"),
|
|
20
|
+
size: optional("quantitative"),
|
|
21
|
+
detail: optional("nominal")
|
|
22
|
+
},
|
|
23
|
+
bar: {
|
|
24
|
+
x: required("quantitative"),
|
|
25
|
+
y: required("nominal", "ordinal"),
|
|
26
|
+
color: optional("nominal", "ordinal", "quantitative"),
|
|
27
|
+
size: optional("quantitative"),
|
|
28
|
+
detail: optional("nominal")
|
|
29
|
+
},
|
|
30
|
+
column: {
|
|
31
|
+
x: required("nominal", "ordinal", "temporal"),
|
|
32
|
+
y: required("quantitative"),
|
|
33
|
+
color: optional("nominal", "ordinal", "quantitative"),
|
|
34
|
+
size: optional("quantitative"),
|
|
35
|
+
detail: optional("nominal")
|
|
36
|
+
},
|
|
37
|
+
pie: {
|
|
38
|
+
x: optional(),
|
|
39
|
+
y: required("quantitative"),
|
|
40
|
+
color: required("nominal", "ordinal"),
|
|
41
|
+
size: optional("quantitative"),
|
|
42
|
+
detail: optional("nominal")
|
|
43
|
+
},
|
|
44
|
+
donut: {
|
|
45
|
+
x: optional(),
|
|
46
|
+
y: required("quantitative"),
|
|
47
|
+
color: required("nominal", "ordinal"),
|
|
48
|
+
size: optional("quantitative"),
|
|
49
|
+
detail: optional("nominal")
|
|
50
|
+
},
|
|
51
|
+
dot: {
|
|
52
|
+
x: required("quantitative"),
|
|
53
|
+
y: required("nominal", "ordinal"),
|
|
54
|
+
color: optional("nominal", "ordinal"),
|
|
55
|
+
size: optional("quantitative"),
|
|
56
|
+
detail: optional("nominal")
|
|
57
|
+
},
|
|
58
|
+
scatter: {
|
|
59
|
+
x: required("quantitative"),
|
|
60
|
+
y: required("quantitative"),
|
|
61
|
+
color: optional("nominal", "ordinal"),
|
|
62
|
+
size: optional("quantitative"),
|
|
63
|
+
detail: optional("nominal")
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var GRAPH_ENCODING_RULES = {
|
|
67
|
+
nodeColor: { required: false, allowedTypes: ["nominal", "ordinal"] },
|
|
68
|
+
nodeSize: { required: false, allowedTypes: ["quantitative"] },
|
|
69
|
+
edgeColor: { required: false, allowedTypes: ["nominal", "ordinal"] },
|
|
70
|
+
edgeWidth: { required: false, allowedTypes: ["quantitative"] },
|
|
71
|
+
nodeLabel: { required: false, allowedTypes: [] }
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/types/spec.ts
|
|
75
|
+
var CHART_TYPES = /* @__PURE__ */ new Set([
|
|
76
|
+
"line",
|
|
77
|
+
"area",
|
|
78
|
+
"bar",
|
|
79
|
+
"column",
|
|
80
|
+
"pie",
|
|
81
|
+
"donut",
|
|
82
|
+
"dot",
|
|
83
|
+
"scatter"
|
|
84
|
+
]);
|
|
85
|
+
function isChartSpec(spec) {
|
|
86
|
+
return CHART_TYPES.has(spec.type);
|
|
87
|
+
}
|
|
88
|
+
function isTableSpec(spec) {
|
|
89
|
+
return spec.type === "table";
|
|
90
|
+
}
|
|
91
|
+
function isGraphSpec(spec) {
|
|
92
|
+
return spec.type === "graph";
|
|
93
|
+
}
|
|
94
|
+
function isTextAnnotation(annotation) {
|
|
95
|
+
return annotation.type === "text";
|
|
96
|
+
}
|
|
97
|
+
function isRangeAnnotation(annotation) {
|
|
98
|
+
return annotation.type === "range";
|
|
99
|
+
}
|
|
100
|
+
function isRefLineAnnotation(annotation) {
|
|
101
|
+
return annotation.type === "refline";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/colors/colorblind.ts
|
|
105
|
+
import { rgb } from "d3-color";
|
|
106
|
+
var PROTAN_MATRIX = [
|
|
107
|
+
[0.567, 0.433, 0],
|
|
108
|
+
[0.558, 0.442, 0],
|
|
109
|
+
[0, 0.242, 0.758]
|
|
110
|
+
];
|
|
111
|
+
var DEUTAN_MATRIX = [
|
|
112
|
+
[0.625, 0.375, 0],
|
|
113
|
+
[0.7, 0.3, 0],
|
|
114
|
+
[0, 0.3, 0.7]
|
|
115
|
+
];
|
|
116
|
+
var TRITAN_MATRIX = [
|
|
117
|
+
[0.95, 0.05, 0],
|
|
118
|
+
[0, 0.433, 0.567],
|
|
119
|
+
[0, 0.475, 0.525]
|
|
120
|
+
];
|
|
121
|
+
var MATRICES = {
|
|
122
|
+
protanopia: PROTAN_MATRIX,
|
|
123
|
+
deuteranopia: DEUTAN_MATRIX,
|
|
124
|
+
tritanopia: TRITAN_MATRIX
|
|
125
|
+
};
|
|
126
|
+
function linearize(v) {
|
|
127
|
+
const s = v / 255;
|
|
128
|
+
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
129
|
+
}
|
|
130
|
+
function delinearize(v) {
|
|
131
|
+
const s = v <= 31308e-7 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055;
|
|
132
|
+
return Math.round(Math.max(0, Math.min(255, s * 255)));
|
|
133
|
+
}
|
|
134
|
+
function simulateColorBlindness(color, type) {
|
|
135
|
+
const c = rgb(color);
|
|
136
|
+
if (c == null) return color;
|
|
137
|
+
const lin = [linearize(c.r), linearize(c.g), linearize(c.b)];
|
|
138
|
+
const m = MATRICES[type];
|
|
139
|
+
const r = m[0][0] * lin[0] + m[0][1] * lin[1] + m[0][2] * lin[2];
|
|
140
|
+
const g = m[1][0] * lin[0] + m[1][1] * lin[1] + m[1][2] * lin[2];
|
|
141
|
+
const b = m[2][0] * lin[0] + m[2][1] * lin[1] + m[2][2] * lin[2];
|
|
142
|
+
return rgb(delinearize(r), delinearize(g), delinearize(b)).formatHex();
|
|
143
|
+
}
|
|
144
|
+
function checkPaletteDistinguishability(colors, type, minDistance = 30) {
|
|
145
|
+
const simulated = colors.map((c) => {
|
|
146
|
+
const s = rgb(simulateColorBlindness(c, type));
|
|
147
|
+
return s ? [s.r, s.g, s.b] : [0, 0, 0];
|
|
148
|
+
});
|
|
149
|
+
for (let i = 0; i < simulated.length; i++) {
|
|
150
|
+
for (let j = i + 1; j < simulated.length; j++) {
|
|
151
|
+
const dr = simulated[i][0] - simulated[j][0];
|
|
152
|
+
const dg = simulated[i][1] - simulated[j][1];
|
|
153
|
+
const db = simulated[i][2] - simulated[j][2];
|
|
154
|
+
const dist = Math.sqrt(dr * dr + dg * dg + db * db);
|
|
155
|
+
if (dist < minDistance) return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/colors/contrast.ts
|
|
162
|
+
import { rgb as rgb2 } from "d3-color";
|
|
163
|
+
function relativeLuminance(color) {
|
|
164
|
+
const c = rgb2(color);
|
|
165
|
+
if (c == null) return 0;
|
|
166
|
+
const srgb = [c.r / 255, c.g / 255, c.b / 255];
|
|
167
|
+
const linear = srgb.map((v) => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
|
|
168
|
+
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
|
|
169
|
+
}
|
|
170
|
+
function contrastRatio(fg, bg) {
|
|
171
|
+
const l1 = relativeLuminance(fg);
|
|
172
|
+
const l2 = relativeLuminance(bg);
|
|
173
|
+
const lighter = Math.max(l1, l2);
|
|
174
|
+
const darker = Math.min(l1, l2);
|
|
175
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
176
|
+
}
|
|
177
|
+
function meetsAA(fg, bg, largeText = false) {
|
|
178
|
+
const ratio = contrastRatio(fg, bg);
|
|
179
|
+
return largeText ? ratio >= 3 : ratio >= 4.5;
|
|
180
|
+
}
|
|
181
|
+
function findAccessibleColor(baseColor, bg, targetRatio = 4.5) {
|
|
182
|
+
if (contrastRatio(baseColor, bg) >= targetRatio) {
|
|
183
|
+
return baseColor;
|
|
184
|
+
}
|
|
185
|
+
const c = rgb2(baseColor);
|
|
186
|
+
if (c == null) return baseColor;
|
|
187
|
+
const bgLum = relativeLuminance(bg);
|
|
188
|
+
const bgIsLight = bgLum > 0.5;
|
|
189
|
+
let lo = 0;
|
|
190
|
+
let hi = 1;
|
|
191
|
+
let best = baseColor;
|
|
192
|
+
for (let i = 0; i < 20; i++) {
|
|
193
|
+
const mid = (lo + hi) / 2;
|
|
194
|
+
const adjusted = bgIsLight ? rgb2(c.r * (1 - mid), c.g * (1 - mid), c.b * (1 - mid)) : rgb2(c.r + (255 - c.r) * mid, c.g + (255 - c.g) * mid, c.b + (255 - c.b) * mid);
|
|
195
|
+
const hex = adjusted.formatHex();
|
|
196
|
+
const ratio = contrastRatio(hex, bg);
|
|
197
|
+
if (ratio >= targetRatio) {
|
|
198
|
+
best = hex;
|
|
199
|
+
hi = mid;
|
|
200
|
+
} else {
|
|
201
|
+
lo = mid;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return best;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/colors/palettes.ts
|
|
208
|
+
var CATEGORICAL_PALETTE = [
|
|
209
|
+
"#1b7fa3",
|
|
210
|
+
// teal-blue (primary)
|
|
211
|
+
"#c44e52",
|
|
212
|
+
// warm red (secondary)
|
|
213
|
+
"#6a9f58",
|
|
214
|
+
// softer green (tertiary)
|
|
215
|
+
"#d47215",
|
|
216
|
+
// orange
|
|
217
|
+
"#507e79",
|
|
218
|
+
// muted teal
|
|
219
|
+
"#9a6a8d",
|
|
220
|
+
// purple
|
|
221
|
+
"#c4636b",
|
|
222
|
+
// rose
|
|
223
|
+
"#9c755f",
|
|
224
|
+
// brown
|
|
225
|
+
"#a88f22",
|
|
226
|
+
// olive gold
|
|
227
|
+
"#858078"
|
|
228
|
+
// warm gray
|
|
229
|
+
];
|
|
230
|
+
var SEQUENTIAL_BLUE = {
|
|
231
|
+
name: "blue",
|
|
232
|
+
stops: ["#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#3182bd", "#08519c"]
|
|
233
|
+
};
|
|
234
|
+
var SEQUENTIAL_GREEN = {
|
|
235
|
+
name: "green",
|
|
236
|
+
stops: ["#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#31a354", "#006d2c"]
|
|
237
|
+
};
|
|
238
|
+
var SEQUENTIAL_ORANGE = {
|
|
239
|
+
name: "orange",
|
|
240
|
+
stops: ["#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#e6550d", "#a63603"]
|
|
241
|
+
};
|
|
242
|
+
var SEQUENTIAL_PURPLE = {
|
|
243
|
+
name: "purple",
|
|
244
|
+
stops: ["#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"]
|
|
245
|
+
};
|
|
246
|
+
var SEQUENTIAL_PALETTES = {
|
|
247
|
+
blue: [...SEQUENTIAL_BLUE.stops],
|
|
248
|
+
green: [...SEQUENTIAL_GREEN.stops],
|
|
249
|
+
orange: [...SEQUENTIAL_ORANGE.stops],
|
|
250
|
+
purple: [...SEQUENTIAL_PURPLE.stops]
|
|
251
|
+
};
|
|
252
|
+
var DIVERGING_RED_BLUE = {
|
|
253
|
+
name: "redBlue",
|
|
254
|
+
stops: [
|
|
255
|
+
"#b2182b",
|
|
256
|
+
// strong red
|
|
257
|
+
"#d6604d",
|
|
258
|
+
// medium red
|
|
259
|
+
"#f4a582",
|
|
260
|
+
// light red
|
|
261
|
+
"#f7f7f7",
|
|
262
|
+
// neutral
|
|
263
|
+
"#92c5de",
|
|
264
|
+
// light blue
|
|
265
|
+
"#4393c3",
|
|
266
|
+
// medium blue
|
|
267
|
+
"#2166ac"
|
|
268
|
+
// strong blue
|
|
269
|
+
]
|
|
270
|
+
};
|
|
271
|
+
var DIVERGING_BROWN_TEAL = {
|
|
272
|
+
name: "brownTeal",
|
|
273
|
+
stops: [
|
|
274
|
+
"#8c510a",
|
|
275
|
+
// strong brown
|
|
276
|
+
"#bf812d",
|
|
277
|
+
// medium brown
|
|
278
|
+
"#dfc27d",
|
|
279
|
+
// light brown
|
|
280
|
+
"#f6e8c3",
|
|
281
|
+
// neutral
|
|
282
|
+
"#80cdc1",
|
|
283
|
+
// light teal
|
|
284
|
+
"#35978f",
|
|
285
|
+
// medium teal
|
|
286
|
+
"#01665e"
|
|
287
|
+
// strong teal
|
|
288
|
+
]
|
|
289
|
+
};
|
|
290
|
+
var DIVERGING_PALETTES = {
|
|
291
|
+
redBlue: [...DIVERGING_RED_BLUE.stops],
|
|
292
|
+
brownTeal: [...DIVERGING_BROWN_TEAL.stops]
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// src/theme/dark-mode.ts
|
|
296
|
+
import { hsl, rgb as rgb3 } from "d3-color";
|
|
297
|
+
var DARK_BG = "#1a1a2e";
|
|
298
|
+
var DARK_TEXT = "#e0e0e0";
|
|
299
|
+
function adaptColorForDarkMode(color, lightBg, darkBg) {
|
|
300
|
+
const originalRatio = contrastRatio(color, lightBg);
|
|
301
|
+
const c = hsl(color);
|
|
302
|
+
if (c == null || Number.isNaN(c.h)) {
|
|
303
|
+
const r = rgb3(color);
|
|
304
|
+
if (r == null) return color;
|
|
305
|
+
const darkBgLum = _luminanceFromHex(darkBg);
|
|
306
|
+
const isLight = darkBgLum < 0.5;
|
|
307
|
+
if (isLight) return color;
|
|
308
|
+
const inverted = hsl(color);
|
|
309
|
+
if (inverted == null) return color;
|
|
310
|
+
inverted.l = 1 - inverted.l;
|
|
311
|
+
return inverted.formatHex();
|
|
312
|
+
}
|
|
313
|
+
let lo = 0;
|
|
314
|
+
let hi = 1;
|
|
315
|
+
let bestColor = color;
|
|
316
|
+
let bestDiff = Infinity;
|
|
317
|
+
for (let i = 0; i < 20; i++) {
|
|
318
|
+
const mid = (lo + hi) / 2;
|
|
319
|
+
const candidate = hsl(c.h, c.s, mid);
|
|
320
|
+
const hex = candidate.formatHex();
|
|
321
|
+
const ratio = contrastRatio(hex, darkBg);
|
|
322
|
+
const diff = Math.abs(ratio - originalRatio);
|
|
323
|
+
if (diff < bestDiff) {
|
|
324
|
+
bestDiff = diff;
|
|
325
|
+
bestColor = hex;
|
|
326
|
+
}
|
|
327
|
+
if (ratio < originalRatio) {
|
|
328
|
+
lo = mid;
|
|
329
|
+
} else {
|
|
330
|
+
hi = mid;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return bestColor;
|
|
334
|
+
}
|
|
335
|
+
function _luminanceFromHex(color) {
|
|
336
|
+
const c = rgb3(color);
|
|
337
|
+
if (c == null) return 0;
|
|
338
|
+
return (0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b) / 255;
|
|
339
|
+
}
|
|
340
|
+
function adaptTheme(theme) {
|
|
341
|
+
const lightBg = theme.colors.background;
|
|
342
|
+
const darkBg = DARK_BG;
|
|
343
|
+
return {
|
|
344
|
+
...theme,
|
|
345
|
+
isDark: true,
|
|
346
|
+
colors: {
|
|
347
|
+
...theme.colors,
|
|
348
|
+
background: darkBg,
|
|
349
|
+
text: DARK_TEXT,
|
|
350
|
+
gridline: "#333344",
|
|
351
|
+
axis: "#888899",
|
|
352
|
+
annotationFill: "rgba(255,255,255,0.08)",
|
|
353
|
+
annotationText: "#bbbbcc",
|
|
354
|
+
categorical: theme.colors.categorical.map((c) => adaptColorForDarkMode(c, lightBg, darkBg))
|
|
355
|
+
// Sequential and diverging palettes are kept as-is since they're
|
|
356
|
+
// typically used for fills where the lightness range still works.
|
|
357
|
+
// If a specific use case needs adaptation, it can be done per-color.
|
|
358
|
+
},
|
|
359
|
+
chrome: {
|
|
360
|
+
title: { ...theme.chrome.title, color: DARK_TEXT },
|
|
361
|
+
subtitle: { ...theme.chrome.subtitle, color: "#aaaaaa" },
|
|
362
|
+
source: { ...theme.chrome.source, color: "#888888" },
|
|
363
|
+
byline: { ...theme.chrome.byline, color: "#888888" },
|
|
364
|
+
footer: { ...theme.chrome.footer, color: "#888888" }
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/theme/defaults.ts
|
|
370
|
+
var DEFAULT_THEME = {
|
|
371
|
+
colors: {
|
|
372
|
+
categorical: [...CATEGORICAL_PALETTE],
|
|
373
|
+
sequential: SEQUENTIAL_PALETTES,
|
|
374
|
+
diverging: DIVERGING_PALETTES,
|
|
375
|
+
background: "#ffffff",
|
|
376
|
+
text: "#1d1d1d",
|
|
377
|
+
gridline: "#e8e8e8",
|
|
378
|
+
axis: "#888888",
|
|
379
|
+
annotationFill: "rgba(0,0,0,0.04)",
|
|
380
|
+
annotationText: "#555555"
|
|
381
|
+
},
|
|
382
|
+
fonts: {
|
|
383
|
+
family: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
384
|
+
mono: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace',
|
|
385
|
+
sizes: {
|
|
386
|
+
title: 22,
|
|
387
|
+
subtitle: 15,
|
|
388
|
+
body: 13,
|
|
389
|
+
small: 11,
|
|
390
|
+
axisTick: 11
|
|
391
|
+
},
|
|
392
|
+
weights: {
|
|
393
|
+
normal: 400,
|
|
394
|
+
medium: 500,
|
|
395
|
+
semibold: 600,
|
|
396
|
+
bold: 700
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
spacing: {
|
|
400
|
+
padding: 12,
|
|
401
|
+
chromeGap: 4,
|
|
402
|
+
chromeToChart: 8,
|
|
403
|
+
chartToFooter: 8,
|
|
404
|
+
axisMargin: 6
|
|
405
|
+
},
|
|
406
|
+
borderRadius: 4,
|
|
407
|
+
chrome: {
|
|
408
|
+
title: {
|
|
409
|
+
fontSize: 22,
|
|
410
|
+
fontWeight: 700,
|
|
411
|
+
color: "#333333",
|
|
412
|
+
lineHeight: 1.3
|
|
413
|
+
},
|
|
414
|
+
subtitle: {
|
|
415
|
+
fontSize: 15,
|
|
416
|
+
fontWeight: 400,
|
|
417
|
+
color: "#666666",
|
|
418
|
+
lineHeight: 1.4
|
|
419
|
+
},
|
|
420
|
+
source: {
|
|
421
|
+
fontSize: 12,
|
|
422
|
+
fontWeight: 400,
|
|
423
|
+
color: "#999999",
|
|
424
|
+
lineHeight: 1.3
|
|
425
|
+
},
|
|
426
|
+
byline: {
|
|
427
|
+
fontSize: 12,
|
|
428
|
+
fontWeight: 400,
|
|
429
|
+
color: "#999999",
|
|
430
|
+
lineHeight: 1.3
|
|
431
|
+
},
|
|
432
|
+
footer: {
|
|
433
|
+
fontSize: 12,
|
|
434
|
+
fontWeight: 400,
|
|
435
|
+
color: "#999999",
|
|
436
|
+
lineHeight: 1.3
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// src/theme/resolve.ts
|
|
442
|
+
function deepMerge(target, source) {
|
|
443
|
+
const result = { ...target };
|
|
444
|
+
for (const key of Object.keys(source)) {
|
|
445
|
+
const sourceVal = source[key];
|
|
446
|
+
const targetVal = target[key];
|
|
447
|
+
if (sourceVal !== void 0 && sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && typeof targetVal === "object" && targetVal !== null && !Array.isArray(targetVal)) {
|
|
448
|
+
result[key] = deepMerge(targetVal, sourceVal);
|
|
449
|
+
} else if (sourceVal !== void 0) {
|
|
450
|
+
result[key] = sourceVal;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
function themeConfigToPartial(config) {
|
|
456
|
+
const partial = {};
|
|
457
|
+
if (config.colors) {
|
|
458
|
+
const colors = {};
|
|
459
|
+
if (config.colors.categorical) colors.categorical = config.colors.categorical;
|
|
460
|
+
if (config.colors.sequential) colors.sequential = config.colors.sequential;
|
|
461
|
+
if (config.colors.diverging) colors.diverging = config.colors.diverging;
|
|
462
|
+
if (config.colors.background) colors.background = config.colors.background;
|
|
463
|
+
if (config.colors.text) colors.text = config.colors.text;
|
|
464
|
+
if (config.colors.gridline) colors.gridline = config.colors.gridline;
|
|
465
|
+
if (config.colors.axis) colors.axis = config.colors.axis;
|
|
466
|
+
partial.colors = colors;
|
|
467
|
+
}
|
|
468
|
+
if (config.fonts) {
|
|
469
|
+
const fonts = {};
|
|
470
|
+
if (config.fonts.family) fonts.family = config.fonts.family;
|
|
471
|
+
if (config.fonts.mono) fonts.mono = config.fonts.mono;
|
|
472
|
+
partial.fonts = fonts;
|
|
473
|
+
}
|
|
474
|
+
if (config.spacing) {
|
|
475
|
+
const spacing = {};
|
|
476
|
+
if (config.spacing.padding !== void 0) spacing.padding = config.spacing.padding;
|
|
477
|
+
if (config.spacing.chromeGap !== void 0) spacing.chromeGap = config.spacing.chromeGap;
|
|
478
|
+
partial.spacing = spacing;
|
|
479
|
+
}
|
|
480
|
+
if (config.borderRadius !== void 0) {
|
|
481
|
+
partial.borderRadius = config.borderRadius;
|
|
482
|
+
}
|
|
483
|
+
return partial;
|
|
484
|
+
}
|
|
485
|
+
function relativeLuminance2(hex) {
|
|
486
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
487
|
+
if (!m) return 0;
|
|
488
|
+
const r = parseInt(m[1].slice(0, 2), 16) / 255;
|
|
489
|
+
const g = parseInt(m[1].slice(2, 4), 16) / 255;
|
|
490
|
+
const b = parseInt(m[1].slice(4, 6), 16) / 255;
|
|
491
|
+
const toLinear = (c) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
492
|
+
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
493
|
+
}
|
|
494
|
+
function isDarkBackground(hex) {
|
|
495
|
+
return relativeLuminance2(hex) < 0.2;
|
|
496
|
+
}
|
|
497
|
+
function adaptChromeForDarkBg(theme, textColor) {
|
|
498
|
+
const light = DEFAULT_THEME.chrome;
|
|
499
|
+
return {
|
|
500
|
+
...theme,
|
|
501
|
+
chrome: {
|
|
502
|
+
title: {
|
|
503
|
+
...theme.chrome.title,
|
|
504
|
+
color: theme.chrome.title.color === light.title.color ? textColor : theme.chrome.title.color
|
|
505
|
+
},
|
|
506
|
+
subtitle: {
|
|
507
|
+
...theme.chrome.subtitle,
|
|
508
|
+
color: theme.chrome.subtitle.color === light.subtitle.color ? adjustOpacity(textColor, 0.7) : theme.chrome.subtitle.color
|
|
509
|
+
},
|
|
510
|
+
source: {
|
|
511
|
+
...theme.chrome.source,
|
|
512
|
+
color: theme.chrome.source.color === light.source.color ? adjustOpacity(textColor, 0.5) : theme.chrome.source.color
|
|
513
|
+
},
|
|
514
|
+
byline: {
|
|
515
|
+
...theme.chrome.byline,
|
|
516
|
+
color: theme.chrome.byline.color === light.byline.color ? adjustOpacity(textColor, 0.5) : theme.chrome.byline.color
|
|
517
|
+
},
|
|
518
|
+
footer: {
|
|
519
|
+
...theme.chrome.footer,
|
|
520
|
+
color: theme.chrome.footer.color === light.footer.color ? adjustOpacity(textColor, 0.5) : theme.chrome.footer.color
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function adjustOpacity(hex, opacity) {
|
|
526
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
527
|
+
if (!m) return hex;
|
|
528
|
+
const r = parseInt(m[1].slice(0, 2), 16);
|
|
529
|
+
const g = parseInt(m[1].slice(2, 4), 16);
|
|
530
|
+
const b = parseInt(m[1].slice(4, 6), 16);
|
|
531
|
+
const mix = (c) => Math.round(c * opacity + 128 * (1 - opacity));
|
|
532
|
+
const toHex = (n) => n.toString(16).padStart(2, "0");
|
|
533
|
+
return `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`;
|
|
534
|
+
}
|
|
535
|
+
function resolveTheme(userTheme, base = DEFAULT_THEME) {
|
|
536
|
+
let merged = userTheme ? deepMerge(base, themeConfigToPartial(userTheme)) : { ...base };
|
|
537
|
+
const dark = isDarkBackground(merged.colors.background);
|
|
538
|
+
if (dark) {
|
|
539
|
+
merged = adaptChromeForDarkBg(merged, merged.colors.text);
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
...merged,
|
|
543
|
+
isDark: dark
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/layout/text-measure.ts
|
|
548
|
+
var AVG_CHAR_WIDTH_RATIO = 0.55;
|
|
549
|
+
var WEIGHT_ADJUSTMENT = {
|
|
550
|
+
100: 0.9,
|
|
551
|
+
200: 0.92,
|
|
552
|
+
300: 0.95,
|
|
553
|
+
400: 1,
|
|
554
|
+
500: 1.02,
|
|
555
|
+
600: 1.05,
|
|
556
|
+
700: 1.08,
|
|
557
|
+
800: 1.1,
|
|
558
|
+
900: 1.12
|
|
559
|
+
};
|
|
560
|
+
function estimateTextWidth(text, fontSize, fontWeight = 400) {
|
|
561
|
+
const weightFactor = WEIGHT_ADJUSTMENT[fontWeight] ?? 1;
|
|
562
|
+
return text.length * fontSize * AVG_CHAR_WIDTH_RATIO * weightFactor;
|
|
563
|
+
}
|
|
564
|
+
function estimateTextHeight(fontSize, lineCount = 1, lineHeight = 1.3) {
|
|
565
|
+
return fontSize * lineHeight * lineCount;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/layout/chrome.ts
|
|
569
|
+
function normalizeChromeText(value) {
|
|
570
|
+
if (value === void 0) return null;
|
|
571
|
+
if (typeof value === "string") return { text: value };
|
|
572
|
+
return { text: value.text, style: value.style, offset: value.offset };
|
|
573
|
+
}
|
|
574
|
+
function buildTextStyle(defaults, fontFamily, textColor, overrides) {
|
|
575
|
+
return {
|
|
576
|
+
fontFamily: overrides?.fontFamily ?? fontFamily,
|
|
577
|
+
fontSize: overrides?.fontSize ?? defaults.fontSize,
|
|
578
|
+
fontWeight: overrides?.fontWeight ?? defaults.fontWeight,
|
|
579
|
+
fill: overrides?.color ?? textColor ?? defaults.color,
|
|
580
|
+
lineHeight: defaults.lineHeight,
|
|
581
|
+
textAnchor: "start",
|
|
582
|
+
dominantBaseline: "hanging"
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function measureWidth(text, style, measureText) {
|
|
586
|
+
if (measureText) {
|
|
587
|
+
return measureText(text, style.fontSize, style.fontWeight).width;
|
|
588
|
+
}
|
|
589
|
+
return estimateTextWidth(text, style.fontSize, style.fontWeight);
|
|
590
|
+
}
|
|
591
|
+
function estimateLineCount(text, style, maxWidth, measureText) {
|
|
592
|
+
const fullWidth = measureWidth(text, style, measureText);
|
|
593
|
+
if (fullWidth <= maxWidth) return 1;
|
|
594
|
+
return Math.ceil(fullWidth / maxWidth);
|
|
595
|
+
}
|
|
596
|
+
function computeChrome(chrome, theme, width, measureText) {
|
|
597
|
+
if (!chrome) {
|
|
598
|
+
return { topHeight: 0, bottomHeight: 0 };
|
|
599
|
+
}
|
|
600
|
+
const padding = theme.spacing.padding;
|
|
601
|
+
const chromeGap = theme.spacing.chromeGap;
|
|
602
|
+
const maxWidth = width - padding * 2;
|
|
603
|
+
const fontFamily = theme.fonts.family;
|
|
604
|
+
let topY = padding;
|
|
605
|
+
const topElements = {};
|
|
606
|
+
const titleNorm = normalizeChromeText(chrome.title);
|
|
607
|
+
if (titleNorm) {
|
|
608
|
+
const style = buildTextStyle(
|
|
609
|
+
theme.chrome.title,
|
|
610
|
+
fontFamily,
|
|
611
|
+
theme.chrome.title.color,
|
|
612
|
+
titleNorm.style
|
|
613
|
+
);
|
|
614
|
+
const lineCount = estimateLineCount(titleNorm.text, style, maxWidth, measureText);
|
|
615
|
+
const element = {
|
|
616
|
+
text: titleNorm.text,
|
|
617
|
+
x: padding + (titleNorm.offset?.dx ?? 0),
|
|
618
|
+
y: topY + (titleNorm.offset?.dy ?? 0),
|
|
619
|
+
maxWidth,
|
|
620
|
+
style
|
|
621
|
+
};
|
|
622
|
+
topElements.title = element;
|
|
623
|
+
topY += estimateTextHeight(style.fontSize, lineCount, style.lineHeight) + chromeGap;
|
|
624
|
+
}
|
|
625
|
+
const subtitleNorm = normalizeChromeText(chrome.subtitle);
|
|
626
|
+
if (subtitleNorm) {
|
|
627
|
+
const style = buildTextStyle(
|
|
628
|
+
theme.chrome.subtitle,
|
|
629
|
+
fontFamily,
|
|
630
|
+
theme.chrome.subtitle.color,
|
|
631
|
+
subtitleNorm.style
|
|
632
|
+
);
|
|
633
|
+
const lineCount = estimateLineCount(subtitleNorm.text, style, maxWidth, measureText);
|
|
634
|
+
const element = {
|
|
635
|
+
text: subtitleNorm.text,
|
|
636
|
+
x: padding + (subtitleNorm.offset?.dx ?? 0),
|
|
637
|
+
y: topY + (subtitleNorm.offset?.dy ?? 0),
|
|
638
|
+
maxWidth,
|
|
639
|
+
style
|
|
640
|
+
};
|
|
641
|
+
topElements.subtitle = element;
|
|
642
|
+
topY += estimateTextHeight(style.fontSize, lineCount, style.lineHeight) + chromeGap;
|
|
643
|
+
}
|
|
644
|
+
const hasTopChrome = titleNorm || subtitleNorm;
|
|
645
|
+
const topHeight = hasTopChrome ? topY - padding + theme.spacing.chromeToChart - chromeGap : 0;
|
|
646
|
+
const bottomElements = {};
|
|
647
|
+
let bottomHeight = 0;
|
|
648
|
+
const bottomItems = [];
|
|
649
|
+
const sourceNorm = normalizeChromeText(chrome.source);
|
|
650
|
+
if (sourceNorm) {
|
|
651
|
+
bottomItems.push({
|
|
652
|
+
key: "source",
|
|
653
|
+
norm: sourceNorm,
|
|
654
|
+
defaults: theme.chrome.source
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
const bylineNorm = normalizeChromeText(chrome.byline);
|
|
658
|
+
if (bylineNorm) {
|
|
659
|
+
bottomItems.push({
|
|
660
|
+
key: "byline",
|
|
661
|
+
norm: bylineNorm,
|
|
662
|
+
defaults: theme.chrome.byline
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
const footerNorm = normalizeChromeText(chrome.footer);
|
|
666
|
+
if (footerNorm) {
|
|
667
|
+
bottomItems.push({
|
|
668
|
+
key: "footer",
|
|
669
|
+
norm: footerNorm,
|
|
670
|
+
defaults: theme.chrome.footer
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
if (bottomItems.length > 0) {
|
|
674
|
+
bottomHeight += theme.spacing.chartToFooter;
|
|
675
|
+
for (const item of bottomItems) {
|
|
676
|
+
const style = buildTextStyle(item.defaults, fontFamily, item.defaults.color, item.norm.style);
|
|
677
|
+
const lineCount = estimateLineCount(item.norm.text, style, maxWidth, measureText);
|
|
678
|
+
const height = estimateTextHeight(style.fontSize, lineCount, style.lineHeight);
|
|
679
|
+
bottomElements[item.key] = {
|
|
680
|
+
text: item.norm.text,
|
|
681
|
+
x: padding + (item.norm.offset?.dx ?? 0),
|
|
682
|
+
y: bottomHeight + (item.norm.offset?.dy ?? 0),
|
|
683
|
+
// offset from where bottom chrome starts
|
|
684
|
+
maxWidth,
|
|
685
|
+
style
|
|
686
|
+
};
|
|
687
|
+
bottomHeight += height + chromeGap;
|
|
688
|
+
}
|
|
689
|
+
bottomHeight -= chromeGap;
|
|
690
|
+
bottomHeight += padding;
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
topHeight,
|
|
694
|
+
bottomHeight,
|
|
695
|
+
...topElements,
|
|
696
|
+
...bottomElements
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/responsive/breakpoints.ts
|
|
701
|
+
var BREAKPOINT_COMPACT_MAX = 400;
|
|
702
|
+
var BREAKPOINT_MEDIUM_MAX = 700;
|
|
703
|
+
function getBreakpoint(width) {
|
|
704
|
+
if (width < BREAKPOINT_COMPACT_MAX) return "compact";
|
|
705
|
+
if (width <= BREAKPOINT_MEDIUM_MAX) return "medium";
|
|
706
|
+
return "full";
|
|
707
|
+
}
|
|
708
|
+
function getLayoutStrategy(breakpoint) {
|
|
709
|
+
switch (breakpoint) {
|
|
710
|
+
case "compact":
|
|
711
|
+
return {
|
|
712
|
+
labelMode: "none",
|
|
713
|
+
legendPosition: "top",
|
|
714
|
+
annotationPosition: "tooltip-only",
|
|
715
|
+
axisLabelDensity: "minimal"
|
|
716
|
+
};
|
|
717
|
+
case "medium":
|
|
718
|
+
return {
|
|
719
|
+
labelMode: "important",
|
|
720
|
+
legendPosition: "top",
|
|
721
|
+
annotationPosition: "inline",
|
|
722
|
+
axisLabelDensity: "reduced"
|
|
723
|
+
};
|
|
724
|
+
case "full":
|
|
725
|
+
return {
|
|
726
|
+
labelMode: "all",
|
|
727
|
+
legendPosition: "right",
|
|
728
|
+
annotationPosition: "inline",
|
|
729
|
+
axisLabelDensity: "full"
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/labels/collision.ts
|
|
735
|
+
var PRIORITY_ORDER = {
|
|
736
|
+
data: 0,
|
|
737
|
+
annotation: 1,
|
|
738
|
+
axis: 2
|
|
739
|
+
};
|
|
740
|
+
function detectCollision(a, b) {
|
|
741
|
+
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
742
|
+
}
|
|
743
|
+
var OFFSET_STRATEGIES = [
|
|
744
|
+
{ dx: 0, dy: 0 },
|
|
745
|
+
// original position
|
|
746
|
+
{ dx: 0, dy: -1.2 },
|
|
747
|
+
// above (factor of height)
|
|
748
|
+
{ dx: 0, dy: 1.2 },
|
|
749
|
+
// below
|
|
750
|
+
{ dx: 1.1, dy: 0 },
|
|
751
|
+
// right
|
|
752
|
+
{ dx: -1.1, dy: 0 },
|
|
753
|
+
// left
|
|
754
|
+
{ dx: 1.1, dy: -1.2 },
|
|
755
|
+
// upper-right
|
|
756
|
+
{ dx: -1.1, dy: -1.2 }
|
|
757
|
+
// upper-left
|
|
758
|
+
];
|
|
759
|
+
function resolveCollisions(labels) {
|
|
760
|
+
const sorted = [...labels].sort(
|
|
761
|
+
(a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]
|
|
762
|
+
);
|
|
763
|
+
const placed = [];
|
|
764
|
+
const results = [];
|
|
765
|
+
for (const label of sorted) {
|
|
766
|
+
let bestRect = null;
|
|
767
|
+
let bestX = label.anchorX;
|
|
768
|
+
let bestY = label.anchorY;
|
|
769
|
+
for (const offset of OFFSET_STRATEGIES) {
|
|
770
|
+
const candidateX = label.anchorX + offset.dx * label.width;
|
|
771
|
+
const candidateY = label.anchorY + offset.dy * label.height;
|
|
772
|
+
const candidateRect = {
|
|
773
|
+
x: candidateX,
|
|
774
|
+
y: candidateY,
|
|
775
|
+
width: label.width,
|
|
776
|
+
height: label.height
|
|
777
|
+
};
|
|
778
|
+
const hasCollision = placed.some((p) => detectCollision(candidateRect, p));
|
|
779
|
+
if (!hasCollision) {
|
|
780
|
+
bestRect = candidateRect;
|
|
781
|
+
bestX = candidateX;
|
|
782
|
+
bestY = candidateY;
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (bestRect) {
|
|
787
|
+
placed.push(bestRect);
|
|
788
|
+
const needsConnector = bestX !== label.anchorX || bestY !== label.anchorY;
|
|
789
|
+
results.push({
|
|
790
|
+
text: label.text,
|
|
791
|
+
x: bestX,
|
|
792
|
+
y: bestY,
|
|
793
|
+
style: label.style,
|
|
794
|
+
visible: true,
|
|
795
|
+
connector: needsConnector ? {
|
|
796
|
+
from: { x: bestX, y: bestY },
|
|
797
|
+
to: { x: label.anchorX, y: label.anchorY },
|
|
798
|
+
stroke: label.style.fill,
|
|
799
|
+
style: "straight"
|
|
800
|
+
} : void 0
|
|
801
|
+
});
|
|
802
|
+
} else {
|
|
803
|
+
results.push({
|
|
804
|
+
text: label.text,
|
|
805
|
+
x: label.anchorX,
|
|
806
|
+
y: label.anchorY,
|
|
807
|
+
style: label.style,
|
|
808
|
+
visible: false
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return results;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/locale/format.ts
|
|
816
|
+
import { format as d3Format } from "d3-format";
|
|
817
|
+
import { timeFormat, utcFormat } from "d3-time-format";
|
|
818
|
+
function formatNumber(value, _locale) {
|
|
819
|
+
if (!Number.isFinite(value)) return String(value);
|
|
820
|
+
if (Number.isInteger(value)) {
|
|
821
|
+
return d3Format(",")(value);
|
|
822
|
+
}
|
|
823
|
+
return d3Format(",.2f")(value);
|
|
824
|
+
}
|
|
825
|
+
var ABBREVIATIONS = [
|
|
826
|
+
{ threshold: 1e12, suffix: "T", divisor: 1e12 },
|
|
827
|
+
{ threshold: 1e9, suffix: "B", divisor: 1e9 },
|
|
828
|
+
{ threshold: 1e6, suffix: "M", divisor: 1e6 },
|
|
829
|
+
{ threshold: 1e3, suffix: "K", divisor: 1e3 }
|
|
830
|
+
];
|
|
831
|
+
function abbreviateNumber(value) {
|
|
832
|
+
if (!Number.isFinite(value)) return String(value);
|
|
833
|
+
const absValue = Math.abs(value);
|
|
834
|
+
const sign = value < 0 ? "-" : "";
|
|
835
|
+
for (const { threshold, suffix, divisor } of ABBREVIATIONS) {
|
|
836
|
+
if (absValue >= threshold) {
|
|
837
|
+
const abbreviated = absValue / divisor;
|
|
838
|
+
const formatted = abbreviated % 1 === 0 ? String(abbreviated) : d3Format(".1f")(abbreviated);
|
|
839
|
+
return `${sign}${formatted}${suffix}`;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return formatNumber(value);
|
|
843
|
+
}
|
|
844
|
+
var GRANULARITY_FORMATS = {
|
|
845
|
+
year: "%Y",
|
|
846
|
+
quarter: "",
|
|
847
|
+
// Quarter is always special-cased in formatDate() below
|
|
848
|
+
month: "%b %Y",
|
|
849
|
+
week: "%b %d",
|
|
850
|
+
day: "%b %d, %Y",
|
|
851
|
+
hour: "%b %d %H:%M",
|
|
852
|
+
minute: "%H:%M"
|
|
853
|
+
};
|
|
854
|
+
function formatDate(value, _locale, granularity) {
|
|
855
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
856
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
857
|
+
const gran = granularity ?? inferGranularity(date);
|
|
858
|
+
if (gran === "quarter") {
|
|
859
|
+
const q = Math.ceil((date.getMonth() + 1) / 3);
|
|
860
|
+
return `Q${q} ${date.getFullYear()}`;
|
|
861
|
+
}
|
|
862
|
+
const formatStr = GRANULARITY_FORMATS[gran];
|
|
863
|
+
if (["year", "month", "day"].includes(gran)) {
|
|
864
|
+
return utcFormat(formatStr)(date);
|
|
865
|
+
}
|
|
866
|
+
return timeFormat(formatStr)(date);
|
|
867
|
+
}
|
|
868
|
+
function inferGranularity(date) {
|
|
869
|
+
if (date.getHours() !== 0 || date.getMinutes() !== 0) {
|
|
870
|
+
return date.getMinutes() !== 0 ? "minute" : "hour";
|
|
871
|
+
}
|
|
872
|
+
if (date.getDate() !== 1) return "day";
|
|
873
|
+
if (date.getMonth() !== 0) return "month";
|
|
874
|
+
return "year";
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/accessibility/alt-text.ts
|
|
878
|
+
var CHART_TYPE_NAMES = {
|
|
879
|
+
line: "Line chart",
|
|
880
|
+
area: "Area chart",
|
|
881
|
+
bar: "Bar chart",
|
|
882
|
+
column: "Column chart",
|
|
883
|
+
pie: "Pie chart",
|
|
884
|
+
donut: "Donut chart",
|
|
885
|
+
dot: "Dot plot",
|
|
886
|
+
scatter: "Scatter plot"
|
|
887
|
+
};
|
|
888
|
+
function generateAltText(spec, data) {
|
|
889
|
+
const chartName = CHART_TYPE_NAMES[spec.type] ?? `${spec.type} chart`;
|
|
890
|
+
const parts = [chartName];
|
|
891
|
+
const title = spec.chrome?.title;
|
|
892
|
+
if (title) {
|
|
893
|
+
const titleText = typeof title === "string" ? title : title.text;
|
|
894
|
+
parts.push(`showing ${titleText}`);
|
|
895
|
+
}
|
|
896
|
+
if (spec.encoding.x && data.length > 0) {
|
|
897
|
+
const field = spec.encoding.x.field;
|
|
898
|
+
const values = data.map((d) => d[field]).filter((v) => v != null);
|
|
899
|
+
if (values.length > 0) {
|
|
900
|
+
if (spec.encoding.x.type === "temporal") {
|
|
901
|
+
const dates = values.map((v) => v instanceof Date ? v : new Date(String(v)));
|
|
902
|
+
const validDates = dates.filter((d) => !Number.isNaN(d.getTime()));
|
|
903
|
+
if (validDates.length >= 2) {
|
|
904
|
+
validDates.sort((a, b) => a.getTime() - b.getTime());
|
|
905
|
+
const first = validDates[0].getUTCFullYear();
|
|
906
|
+
const last = validDates[validDates.length - 1].getUTCFullYear();
|
|
907
|
+
if (first !== last) {
|
|
908
|
+
parts.push(`from ${first} to ${last}`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
} else if (spec.encoding.x.type === "nominal" || spec.encoding.x.type === "ordinal") {
|
|
912
|
+
const uniqueValues = [...new Set(values.map(String))];
|
|
913
|
+
parts.push(`across ${uniqueValues.length} categories`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (spec.encoding.color && data.length > 0) {
|
|
918
|
+
const colorField = spec.encoding.color.field;
|
|
919
|
+
const uniqueSeries = [...new Set(data.map((d) => String(d[colorField])).filter(Boolean))];
|
|
920
|
+
if (uniqueSeries.length > 0) {
|
|
921
|
+
parts.push(`with ${uniqueSeries.length} series (${uniqueSeries.join(", ")})`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
parts.push(`(${data.length} data points)`);
|
|
925
|
+
return parts.join(" ");
|
|
926
|
+
}
|
|
927
|
+
function generateDataTable(spec, data) {
|
|
928
|
+
const fields = [];
|
|
929
|
+
const encoding = spec.encoding;
|
|
930
|
+
if (encoding.x) fields.push(encoding.x.field);
|
|
931
|
+
if (encoding.y) fields.push(encoding.y.field);
|
|
932
|
+
if (encoding.color) fields.push(encoding.color.field);
|
|
933
|
+
if (encoding.size) fields.push(encoding.size.field);
|
|
934
|
+
const uniqueFields = [...new Set(fields)];
|
|
935
|
+
if (uniqueFields.length === 0) return [];
|
|
936
|
+
const headers = uniqueFields;
|
|
937
|
+
const rows = data.map((row) => uniqueFields.map((field) => row[field] ?? ""));
|
|
938
|
+
return [headers, ...rows];
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/accessibility/aria.ts
|
|
942
|
+
function generateAriaLabels(marks) {
|
|
943
|
+
const labels = /* @__PURE__ */ new Map();
|
|
944
|
+
for (let i = 0; i < marks.length; i++) {
|
|
945
|
+
const mark = marks[i];
|
|
946
|
+
const key = `mark-${i}`;
|
|
947
|
+
switch (mark.type) {
|
|
948
|
+
case "line": {
|
|
949
|
+
const series = mark.seriesKey ?? "Series";
|
|
950
|
+
const pointCount = mark.points.length;
|
|
951
|
+
labels.set(key, `Line series: ${series} with ${pointCount} points`);
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
case "area": {
|
|
955
|
+
const series = mark.seriesKey ?? "Area";
|
|
956
|
+
labels.set(key, `Area series: ${series}`);
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
case "rect": {
|
|
960
|
+
const dataEntries = Object.entries(mark.data).filter(([k]) => !k.startsWith("_"));
|
|
961
|
+
const description = dataEntries.map(([k, v]) => `${k}: ${formatValue(v)}`).join(", ");
|
|
962
|
+
labels.set(key, `Data point: ${description}`);
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
965
|
+
case "arc": {
|
|
966
|
+
const dataEntries = Object.entries(mark.data).filter(([k]) => !k.startsWith("_"));
|
|
967
|
+
const description = dataEntries.map(([k, v]) => `${k}: ${formatValue(v)}`).join(", ");
|
|
968
|
+
labels.set(key, `Slice: ${description}`);
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
case "point": {
|
|
972
|
+
const dataEntries = Object.entries(mark.data).filter(([k]) => !k.startsWith("_"));
|
|
973
|
+
const description = dataEntries.map(([k, v]) => `${k}: ${formatValue(v)}`).join(", ");
|
|
974
|
+
labels.set(key, `Data point: ${description}`);
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return labels;
|
|
980
|
+
}
|
|
981
|
+
function formatValue(value) {
|
|
982
|
+
if (value == null) return "N/A";
|
|
983
|
+
if (value instanceof Date) return value.toISOString().slice(0, 10);
|
|
984
|
+
if (typeof value === "number") {
|
|
985
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
986
|
+
}
|
|
987
|
+
return String(value);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/helpers/spec-builders.ts
|
|
991
|
+
var ISO_DATE_RE = /^\d{4}(-\d{2}(-\d{2}(T\d{2}(:\d{2}(:\d{2})?)?)?)?)?$/;
|
|
992
|
+
function inferFieldType(data, field) {
|
|
993
|
+
const sampleSize = Math.min(data.length, 20);
|
|
994
|
+
let hasNumber = false;
|
|
995
|
+
let hasDateString = false;
|
|
996
|
+
let hasOtherString = false;
|
|
997
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
998
|
+
const value = data[i][field];
|
|
999
|
+
if (value == null) continue;
|
|
1000
|
+
if (typeof value === "number") {
|
|
1001
|
+
hasNumber = true;
|
|
1002
|
+
} else if (typeof value === "string") {
|
|
1003
|
+
if (ISO_DATE_RE.test(value) && !Number.isNaN(Date.parse(value))) {
|
|
1004
|
+
hasDateString = true;
|
|
1005
|
+
} else {
|
|
1006
|
+
hasOtherString = true;
|
|
1007
|
+
}
|
|
1008
|
+
} else if (value instanceof Date) {
|
|
1009
|
+
hasDateString = true;
|
|
1010
|
+
} else {
|
|
1011
|
+
hasOtherString = true;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (hasNumber && !hasDateString && !hasOtherString) return "quantitative";
|
|
1015
|
+
if (hasDateString && !hasNumber && !hasOtherString) return "temporal";
|
|
1016
|
+
return "nominal";
|
|
1017
|
+
}
|
|
1018
|
+
function resolveField(ref, data) {
|
|
1019
|
+
if (typeof ref === "string") {
|
|
1020
|
+
return { field: ref, type: inferFieldType(data, ref) };
|
|
1021
|
+
}
|
|
1022
|
+
return ref;
|
|
1023
|
+
}
|
|
1024
|
+
function buildEncoding(channels, options, data) {
|
|
1025
|
+
const encoding = { ...channels };
|
|
1026
|
+
if (options?.color && data) {
|
|
1027
|
+
encoding.color = resolveField(options.color, data);
|
|
1028
|
+
}
|
|
1029
|
+
if (options?.size && data) {
|
|
1030
|
+
encoding.size = resolveField(options.size, data);
|
|
1031
|
+
}
|
|
1032
|
+
return encoding;
|
|
1033
|
+
}
|
|
1034
|
+
function buildChartSpec(type, data, encoding, options) {
|
|
1035
|
+
const spec = { type, data, encoding };
|
|
1036
|
+
if (options?.chrome) spec.chrome = options.chrome;
|
|
1037
|
+
if (options?.annotations) spec.annotations = options.annotations;
|
|
1038
|
+
if (options?.responsive !== void 0) spec.responsive = options.responsive;
|
|
1039
|
+
if (options?.theme) spec.theme = options.theme;
|
|
1040
|
+
if (options?.darkMode) spec.darkMode = options.darkMode;
|
|
1041
|
+
return spec;
|
|
1042
|
+
}
|
|
1043
|
+
function lineChart(data, x, y, options) {
|
|
1044
|
+
const xChannel = resolveField(x, data);
|
|
1045
|
+
const yChannel = resolveField(y, data);
|
|
1046
|
+
const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);
|
|
1047
|
+
return buildChartSpec("line", data, encoding, options);
|
|
1048
|
+
}
|
|
1049
|
+
function barChart(data, category, value, options) {
|
|
1050
|
+
const categoryChannel = resolveField(category, data);
|
|
1051
|
+
const valueChannel = resolveField(value, data);
|
|
1052
|
+
const encoding = buildEncoding({ x: valueChannel, y: categoryChannel }, options, data);
|
|
1053
|
+
return buildChartSpec("bar", data, encoding, options);
|
|
1054
|
+
}
|
|
1055
|
+
function columnChart(data, x, y, options) {
|
|
1056
|
+
const xChannel = resolveField(x, data);
|
|
1057
|
+
const yChannel = resolveField(y, data);
|
|
1058
|
+
const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);
|
|
1059
|
+
return buildChartSpec("column", data, encoding, options);
|
|
1060
|
+
}
|
|
1061
|
+
function pieChart(data, category, value, options) {
|
|
1062
|
+
const categoryChannel = resolveField(category, data);
|
|
1063
|
+
const valueChannel = resolveField(value, data);
|
|
1064
|
+
const encoding = {
|
|
1065
|
+
y: valueChannel,
|
|
1066
|
+
color: categoryChannel
|
|
1067
|
+
};
|
|
1068
|
+
if (options?.size && data) {
|
|
1069
|
+
encoding.size = resolveField(options.size, data);
|
|
1070
|
+
}
|
|
1071
|
+
return buildChartSpec("pie", data, encoding, options);
|
|
1072
|
+
}
|
|
1073
|
+
function areaChart(data, x, y, options) {
|
|
1074
|
+
const xChannel = resolveField(x, data);
|
|
1075
|
+
const yChannel = resolveField(y, data);
|
|
1076
|
+
const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);
|
|
1077
|
+
return buildChartSpec("area", data, encoding, options);
|
|
1078
|
+
}
|
|
1079
|
+
function donutChart(data, category, value, options) {
|
|
1080
|
+
const categoryChannel = resolveField(category, data);
|
|
1081
|
+
const valueChannel = resolveField(value, data);
|
|
1082
|
+
const encoding = {
|
|
1083
|
+
y: valueChannel,
|
|
1084
|
+
color: categoryChannel
|
|
1085
|
+
};
|
|
1086
|
+
if (options?.size && data) {
|
|
1087
|
+
encoding.size = resolveField(options.size, data);
|
|
1088
|
+
}
|
|
1089
|
+
return buildChartSpec("donut", data, encoding, options);
|
|
1090
|
+
}
|
|
1091
|
+
function dotChart(data, x, y, options) {
|
|
1092
|
+
const xChannel = resolveField(x, data);
|
|
1093
|
+
const yChannel = resolveField(y, data);
|
|
1094
|
+
const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);
|
|
1095
|
+
return buildChartSpec("dot", data, encoding, options);
|
|
1096
|
+
}
|
|
1097
|
+
function scatterChart(data, x, y, options) {
|
|
1098
|
+
const xChannel = resolveField(x, data);
|
|
1099
|
+
const yChannel = resolveField(y, data);
|
|
1100
|
+
const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);
|
|
1101
|
+
return buildChartSpec("scatter", data, encoding, options);
|
|
1102
|
+
}
|
|
1103
|
+
function dataTable(data, options) {
|
|
1104
|
+
let columns = options?.columns;
|
|
1105
|
+
if (!columns && data.length > 0) {
|
|
1106
|
+
columns = Object.keys(data[0]).map((key) => {
|
|
1107
|
+
const fieldType = inferFieldType(data, key);
|
|
1108
|
+
const align = fieldType === "quantitative" ? "right" : "left";
|
|
1109
|
+
return {
|
|
1110
|
+
key,
|
|
1111
|
+
label: key,
|
|
1112
|
+
align
|
|
1113
|
+
};
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
const spec = {
|
|
1117
|
+
type: "table",
|
|
1118
|
+
data,
|
|
1119
|
+
columns: columns ?? []
|
|
1120
|
+
};
|
|
1121
|
+
if (options?.rowKey) spec.rowKey = options.rowKey;
|
|
1122
|
+
if (options?.chrome) spec.chrome = options.chrome;
|
|
1123
|
+
if (options?.theme) spec.theme = options.theme;
|
|
1124
|
+
if (options?.darkMode) spec.darkMode = options.darkMode;
|
|
1125
|
+
if (options?.search !== void 0) spec.search = options.search;
|
|
1126
|
+
if (options?.pagination !== void 0) spec.pagination = options.pagination;
|
|
1127
|
+
if (options?.stickyFirstColumn !== void 0) spec.stickyFirstColumn = options.stickyFirstColumn;
|
|
1128
|
+
if (options?.compact !== void 0) spec.compact = options.compact;
|
|
1129
|
+
if (options?.responsive !== void 0) spec.responsive = options.responsive;
|
|
1130
|
+
return spec;
|
|
1131
|
+
}
|
|
1132
|
+
export {
|
|
1133
|
+
CATEGORICAL_PALETTE,
|
|
1134
|
+
CHART_ENCODING_RULES,
|
|
1135
|
+
CHART_TYPES,
|
|
1136
|
+
DEFAULT_THEME,
|
|
1137
|
+
DIVERGING_PALETTES,
|
|
1138
|
+
GRAPH_ENCODING_RULES,
|
|
1139
|
+
SEQUENTIAL_PALETTES,
|
|
1140
|
+
abbreviateNumber,
|
|
1141
|
+
adaptColorForDarkMode,
|
|
1142
|
+
adaptTheme,
|
|
1143
|
+
areaChart,
|
|
1144
|
+
barChart,
|
|
1145
|
+
checkPaletteDistinguishability,
|
|
1146
|
+
columnChart,
|
|
1147
|
+
computeChrome,
|
|
1148
|
+
contrastRatio,
|
|
1149
|
+
dataTable,
|
|
1150
|
+
donutChart,
|
|
1151
|
+
dotChart,
|
|
1152
|
+
estimateTextWidth,
|
|
1153
|
+
findAccessibleColor,
|
|
1154
|
+
formatDate,
|
|
1155
|
+
formatNumber,
|
|
1156
|
+
generateAltText,
|
|
1157
|
+
generateAriaLabels,
|
|
1158
|
+
generateDataTable,
|
|
1159
|
+
getBreakpoint,
|
|
1160
|
+
getLayoutStrategy,
|
|
1161
|
+
inferFieldType,
|
|
1162
|
+
isChartSpec,
|
|
1163
|
+
isGraphSpec,
|
|
1164
|
+
isRangeAnnotation,
|
|
1165
|
+
isRefLineAnnotation,
|
|
1166
|
+
isTableSpec,
|
|
1167
|
+
isTextAnnotation,
|
|
1168
|
+
lineChart,
|
|
1169
|
+
meetsAA,
|
|
1170
|
+
pieChart,
|
|
1171
|
+
resolveCollisions,
|
|
1172
|
+
resolveTheme,
|
|
1173
|
+
scatterChart,
|
|
1174
|
+
simulateColorBlindness
|
|
1175
|
+
};
|
|
1176
|
+
//# sourceMappingURL=index.js.map
|