@k2b/editor 0.7.0 → 0.9.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/package.json +2 -2
- package/src/chart-schema.ts +199 -21
- package/src/charts.ts +367 -28
- package/src/markdown/lib/chart-blocks.ts +8 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Configurable Markdown editor for SolidJS: editor, toolbar, bubble menu, and slash menu on one shared state.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@codemirror/state": "6.7.1",
|
|
71
71
|
"@codemirror/view": "6.43.7",
|
|
72
72
|
"@k2b/stdlib": "0.30.0",
|
|
73
|
-
"@k2b/ui": "0.
|
|
73
|
+
"@k2b/ui": "0.52.0",
|
|
74
74
|
"@lezer/common": "1.5.3",
|
|
75
75
|
"@lezer/highlight": "1.2.4",
|
|
76
76
|
"@lezer/markdown": "1.7.2",
|
package/src/chart-schema.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export { chartAcceptsLocale, chartLocaleSchema } from "./chart-locale";
|
|
4
|
-
|
|
1
|
+
import type { ChartInput, ChartLimits } from "@k2b/ui/charts";
|
|
5
2
|
import { CALENDAR_MAX_DAYS, calendarDay } from "@k2b/ui/charts/core";
|
|
6
3
|
import { z } from "zod";
|
|
4
|
+
import { chartLocaleSchema } from "./chart-locale";
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
export
|
|
6
|
+
export type { ChartDateFormat, ChartDescription, ChartFormat, ChartInput, ChartLimits, ChartNumberFormat } from "@k2b/ui/charts";
|
|
7
|
+
export { chartAcceptsLocale, chartLocaleSchema } from "./chart-locale";
|
|
10
8
|
|
|
11
9
|
// Every chart schema is built by a function whose call is marked pure, so a bundle that imports neither chart contract
|
|
12
10
|
// leaves them out. App frames include the app contract through `cloud.chart()`.
|
|
@@ -31,12 +29,29 @@ export const chartNumberFormatSchema = () =>
|
|
|
31
29
|
const dateFormatSchema = () => z.strictObject({ date: z.enum(["day", "minute", "second"]) });
|
|
32
30
|
export const chartFormatSchema = () => z.union([chartNumberFormatSchema(), dateFormatSchema()]);
|
|
33
31
|
|
|
34
|
-
export type ChartNumberFormat = z.output<ReturnType<typeof chartNumberFormatSchema>>;
|
|
35
|
-
export type ChartDateFormat = z.output<ReturnType<typeof dateFormatSchema>>;
|
|
36
|
-
export type ChartFormat = ChartNumberFormat | ChartDateFormat;
|
|
37
|
-
|
|
38
32
|
export const chartKindSchema = () =>
|
|
39
|
-
z.enum([
|
|
33
|
+
z.enum([
|
|
34
|
+
"waterfall",
|
|
35
|
+
"area",
|
|
36
|
+
"bar",
|
|
37
|
+
"hbar",
|
|
38
|
+
"funnel",
|
|
39
|
+
"line",
|
|
40
|
+
"scatter",
|
|
41
|
+
"pie",
|
|
42
|
+
"donut",
|
|
43
|
+
"histogram",
|
|
44
|
+
"gauge",
|
|
45
|
+
"sparkline",
|
|
46
|
+
"boxplot",
|
|
47
|
+
"heatmap",
|
|
48
|
+
"barGauge",
|
|
49
|
+
"stat",
|
|
50
|
+
"treemap",
|
|
51
|
+
"pareto",
|
|
52
|
+
"bubble",
|
|
53
|
+
"radar",
|
|
54
|
+
]);
|
|
40
55
|
|
|
41
56
|
export const chartParts = (limits?: ChartLimits) => {
|
|
42
57
|
const ChartLabel = limits ? z.string().trim().min(1).max(80) : z.union([z.string(), z.number().transform(String)]);
|
|
@@ -154,6 +169,27 @@ export const chartKinds = (limits?: ChartLimits) => {
|
|
|
154
169
|
const ChartKindCommon = { ...ChartCommon, tooltip: ChartTooltip.optional() };
|
|
155
170
|
const numberFormat = chartNumberFormatSchema();
|
|
156
171
|
return z.discriminatedUnion("kind", [
|
|
172
|
+
z
|
|
173
|
+
.object({
|
|
174
|
+
kind: z.literal("waterfall"),
|
|
175
|
+
...ChartKindCommon,
|
|
176
|
+
start: z.object({ label: ChartLabel, value: z.number() }),
|
|
177
|
+
steps: list(z.object({ label: ChartLabel, value: z.number() }), 0, limits?.maxValues),
|
|
178
|
+
end: z.strictObject({ label: ChartLabel }),
|
|
179
|
+
format: numberFormat.optional(),
|
|
180
|
+
})
|
|
181
|
+
.superRefine((spec, ctx) => {
|
|
182
|
+
let balance = spec.start.value;
|
|
183
|
+
let lo = Math.min(0, balance),
|
|
184
|
+
hi = Math.max(0, balance);
|
|
185
|
+
for (const step of spec.steps) {
|
|
186
|
+
balance += step.value;
|
|
187
|
+
lo = Math.min(lo, balance);
|
|
188
|
+
hi = Math.max(hi, balance);
|
|
189
|
+
}
|
|
190
|
+
if (!Number.isFinite(balance) || !Number.isFinite(hi - lo))
|
|
191
|
+
ctx.addIssue({ code: "custom", message: "Waterfall balances must form a finite range." });
|
|
192
|
+
}),
|
|
157
193
|
z
|
|
158
194
|
.object({
|
|
159
195
|
kind: z.literal("area"),
|
|
@@ -175,17 +211,79 @@ export const chartKinds = (limits?: ChartLimits) => {
|
|
|
175
211
|
if (!Number.isFinite(sum)) ctx.addIssue({ code: "custom", path: ["x", i], message: "Area totals must be finite." });
|
|
176
212
|
});
|
|
177
213
|
}),
|
|
214
|
+
z
|
|
215
|
+
.object({
|
|
216
|
+
kind: z.literal("bar"),
|
|
217
|
+
...ChartKindCommon,
|
|
218
|
+
data: list(ChartCategory, 0, limits?.maxValues).default([]),
|
|
219
|
+
x: list(ChartLabel, 0, limits?.maxValues).optional(),
|
|
220
|
+
series: list(
|
|
221
|
+
z.object({ label: ChartLabel, data: list(ChartNumber.nullable(), 0, limits?.maxValues) }),
|
|
222
|
+
1,
|
|
223
|
+
limits?.maxSeries,
|
|
224
|
+
).optional(),
|
|
225
|
+
layout: z.enum(["columns", "rows", "grouped", "stacked"]).optional(),
|
|
226
|
+
normalize: z.boolean().optional(),
|
|
227
|
+
share: z.boolean().optional(),
|
|
228
|
+
limit: z.number().int().positive().optional(),
|
|
229
|
+
tone: z.enum(["in", "out"]).optional(),
|
|
230
|
+
format: numberFormat.optional(),
|
|
231
|
+
compare: z.object({ label: ChartLabel, data: list(ChartNumber.nullable(), 0, limits?.maxValues) }).optional(),
|
|
232
|
+
xAxis: ChartAxis.pick({ label: true }).extend({ ordered: z.boolean().optional() }).strict().optional(),
|
|
233
|
+
yAxis: ChartAxis.optional(),
|
|
234
|
+
colorByBar: z.boolean().optional(),
|
|
235
|
+
showValues: z.boolean().optional(),
|
|
236
|
+
legend: z.boolean().optional(),
|
|
237
|
+
references: ChartReferences.optional(),
|
|
238
|
+
})
|
|
239
|
+
.superRefine((bar, ctx) => {
|
|
240
|
+
const issue = (message: string) => ctx.addIssue({ code: "custom", message });
|
|
241
|
+
if (limits && !bar.series && !bar.data.length)
|
|
242
|
+
ctx.addIssue({ code: "custom", path: ["data"], message: "A bar chart needs data or aligned series." });
|
|
243
|
+
if ((bar.share !== undefined || bar.limit !== undefined || bar.tone !== undefined) && bar.layout !== "rows")
|
|
244
|
+
issue("Share, limit and tone require rows layout.");
|
|
245
|
+
if (bar.layout === "rows" && bar.data.some((d) => Number.isFinite(d.value) && d.value < 0))
|
|
246
|
+
issue("Ranked bar lists require nonnegative values.");
|
|
247
|
+
if (bar.series) {
|
|
248
|
+
if (!bar.x || bar.series.some((s) => s.data.length !== bar.x?.length)) issue("Bar series must have the same length as x.");
|
|
249
|
+
if (bar.data.length) issue("Use data or aligned series, not both.");
|
|
250
|
+
if (bar.layout === "rows" || bar.layout === "columns") issue("Series bars use grouped or stacked layout.");
|
|
251
|
+
} else if (bar.x || bar.compare || bar.layout === "grouped" || bar.layout === "stacked")
|
|
252
|
+
issue("This layout needs aligned x and series.");
|
|
253
|
+
if (bar.layout === "stacked" && bar.yAxis?.scale === "log") issue("Stacked bars need a linear axis.");
|
|
254
|
+
if (bar.normalize && bar.layout !== "stacked") issue("Normalized bars must be stacked.");
|
|
255
|
+
if (bar.layout === "stacked" && bar.series?.some((s) => s.data.some((v) => v !== null && v < 0)))
|
|
256
|
+
issue("Stacked bars require nonnegative values.");
|
|
257
|
+
if (bar.compare && (bar.series?.length !== 1 || bar.compare.data.length !== bar.x?.length || bar.layout === "stacked"))
|
|
258
|
+
issue("Comparison needs one current series and aligned previous values.");
|
|
259
|
+
}),
|
|
178
260
|
z.object({
|
|
179
|
-
kind: z.literal("
|
|
261
|
+
kind: z.literal("funnel"),
|
|
180
262
|
...ChartKindCommon,
|
|
181
|
-
data: list(
|
|
182
|
-
|
|
183
|
-
yAxis: ChartAxis.optional(),
|
|
184
|
-
colorByBar: z.boolean().optional(),
|
|
185
|
-
showValues: z.boolean().optional(),
|
|
186
|
-
legend: z.boolean().optional(),
|
|
187
|
-
references: ChartReferences.optional(),
|
|
263
|
+
data: list(z.object({ label: ChartLabel, value: z.number().nonnegative().nullable() }), 1, limits?.maxValues),
|
|
264
|
+
format: numberFormat.optional(),
|
|
188
265
|
}),
|
|
266
|
+
z
|
|
267
|
+
.object({
|
|
268
|
+
kind: z.literal("hbar"),
|
|
269
|
+
...ChartKindCommon,
|
|
270
|
+
series: list(ChartLabel, 1, limits?.maxSeries),
|
|
271
|
+
rows: list(
|
|
272
|
+
z.object({
|
|
273
|
+
group: ChartLabel.optional(),
|
|
274
|
+
label: ChartLabel,
|
|
275
|
+
values: list(z.number().nonnegative().nullable(), 0, limits?.maxSeries),
|
|
276
|
+
}),
|
|
277
|
+
1,
|
|
278
|
+
limits?.maxValues,
|
|
279
|
+
),
|
|
280
|
+
legend: z.boolean().optional(),
|
|
281
|
+
format: numberFormat.optional(),
|
|
282
|
+
})
|
|
283
|
+
.superRefine((bar, ctx) => {
|
|
284
|
+
if (bar.rows.some((r) => r.values.length !== bar.series.length))
|
|
285
|
+
ctx.addIssue({ code: "custom", message: "Horizontal bar values must match series." });
|
|
286
|
+
}),
|
|
189
287
|
z.object({
|
|
190
288
|
kind: z.literal("line"),
|
|
191
289
|
...ChartKindCommon,
|
|
@@ -319,6 +417,81 @@ export const chartKinds = (limits?: ChartLimits) => {
|
|
|
319
417
|
better: z.enum(["up", "down"]).optional(),
|
|
320
418
|
sparkline: list(ChartNumber, 2, limits?.maxValues).optional(),
|
|
321
419
|
}),
|
|
420
|
+
z
|
|
421
|
+
.object({
|
|
422
|
+
kind: z.literal("treemap"),
|
|
423
|
+
...ChartKindCommon,
|
|
424
|
+
groups: list(ChartLabel, 1, limits?.maxSeries),
|
|
425
|
+
data: list(z.object({ label: ChartLabel, group: ChartLabel, value: z.number().nonnegative() }), 1, limits?.maxMarks),
|
|
426
|
+
legend: z.boolean().optional(),
|
|
427
|
+
format: numberFormat.optional(),
|
|
428
|
+
locale: chartLocaleSchema().optional(),
|
|
429
|
+
})
|
|
430
|
+
.superRefine((spec, ctx) => {
|
|
431
|
+
if (!Number.isFinite(spec.data.reduce((sum, d) => sum + d.value, 0)))
|
|
432
|
+
ctx.addIssue({ code: "custom", message: "Treemap total must be finite." });
|
|
433
|
+
if (spec.data.some((d) => !spec.groups.includes(d.group)))
|
|
434
|
+
ctx.addIssue({ code: "custom", message: "Treemap groups must contain every item group." });
|
|
435
|
+
}),
|
|
436
|
+
z
|
|
437
|
+
.object({
|
|
438
|
+
kind: z.literal("pareto"),
|
|
439
|
+
...ChartKindCommon,
|
|
440
|
+
labels: z.tuple([ChartLabel, ChartLabel]),
|
|
441
|
+
data: list(z.object({ label: ChartLabel, value: z.number().nonnegative(), prev: z.number().nonnegative() }), 1, limits?.maxValues),
|
|
442
|
+
format: numberFormat.optional(),
|
|
443
|
+
locale: chartLocaleSchema().optional(),
|
|
444
|
+
})
|
|
445
|
+
.superRefine((spec, ctx) => {
|
|
446
|
+
if (!Number.isFinite(spec.data.reduce((sum, d) => sum + d.value, 0)))
|
|
447
|
+
ctx.addIssue({ code: "custom", message: "Pareto total must be finite." });
|
|
448
|
+
}),
|
|
449
|
+
z
|
|
450
|
+
.object({
|
|
451
|
+
kind: z.literal("bubble"),
|
|
452
|
+
...ChartKindCommon,
|
|
453
|
+
groups: list(ChartLabel, 1, limits?.maxSeries),
|
|
454
|
+
data: list(
|
|
455
|
+
z.object({ label: ChartLabel, group: ChartLabel, x: z.number(), y: z.number(), size: z.number().nonnegative().optional() }),
|
|
456
|
+
1,
|
|
457
|
+
limits?.maxMarks,
|
|
458
|
+
),
|
|
459
|
+
xLabel: ChartLabel,
|
|
460
|
+
yLabel: ChartLabel,
|
|
461
|
+
sizeLabel: ChartLabel.optional(),
|
|
462
|
+
averages: z.boolean().optional(),
|
|
463
|
+
legend: z.boolean().optional(),
|
|
464
|
+
xFormat: numberFormat.optional(),
|
|
465
|
+
yFormat: numberFormat.optional(),
|
|
466
|
+
format: numberFormat.optional(),
|
|
467
|
+
locale: chartLocaleSchema().optional(),
|
|
468
|
+
})
|
|
469
|
+
.superRefine((spec, ctx) => {
|
|
470
|
+
if (spec.data.some((p) => !spec.groups.includes(p.group)))
|
|
471
|
+
ctx.addIssue({ code: "custom", message: "Bubble groups must contain every point group." });
|
|
472
|
+
for (const key of ["x", "y"] as const)
|
|
473
|
+
if (!Number.isFinite(Math.max(0, ...spec.data.map((p) => p[key])) - Math.min(0, ...spec.data.map((p) => p[key]))))
|
|
474
|
+
ctx.addIssue({ code: "custom", message: "Bubble coordinate ranges must be finite." });
|
|
475
|
+
}),
|
|
476
|
+
z
|
|
477
|
+
.object({
|
|
478
|
+
kind: z.literal("radar"),
|
|
479
|
+
...ChartKindCommon,
|
|
480
|
+
axes: z.array(ChartLabel).min(3).max(8),
|
|
481
|
+
max: z.number().positive(),
|
|
482
|
+
unit: ChartUnit.optional(),
|
|
483
|
+
series: z
|
|
484
|
+
.array(z.object({ label: ChartLabel, data: z.array(z.number().nonnegative()).min(3).max(8) }))
|
|
485
|
+
.min(1)
|
|
486
|
+
.max(4),
|
|
487
|
+
format: numberFormat.optional(),
|
|
488
|
+
legend: z.boolean().optional(),
|
|
489
|
+
locale: chartLocaleSchema().optional(),
|
|
490
|
+
})
|
|
491
|
+
.superRefine((spec, ctx) => {
|
|
492
|
+
if (spec.series.some((s) => s.data.length !== spec.axes.length || s.data.some((v) => v > spec.max)))
|
|
493
|
+
ctx.addIssue({ code: "custom", message: "Radar series need one value per axis between zero and the common maximum." });
|
|
494
|
+
}),
|
|
322
495
|
]);
|
|
323
496
|
};
|
|
324
497
|
|
|
@@ -339,5 +512,10 @@ export const chartDateTime = (value: string): number | undefined => {
|
|
|
339
512
|
return date.toISOString().startsWith(`${year}-${month}-${day}T${hour}:${minute}:${second}`) ? date.getTime() : undefined;
|
|
340
513
|
};
|
|
341
514
|
|
|
342
|
-
|
|
343
|
-
|
|
515
|
+
// Both directions are checked: a validator change must update the ui-owned description too.
|
|
516
|
+
type Assert<T extends true> = T;
|
|
517
|
+
type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
|
|
518
|
+
type _ChartSchemaMatchesDescription = Assert<Same<z.output<ReturnType<typeof chartKinds>>, ChartInput>>;
|
|
519
|
+
type _ChartNumberFormatSchemaMatchesDescription = Assert<
|
|
520
|
+
Same<z.output<ReturnType<typeof chartNumberFormatSchema>>, import("@k2b/ui/charts").ChartNumberFormat>
|
|
521
|
+
>;
|
package/src/charts.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AreaOptions,
|
|
5
5
|
autoBin,
|
|
6
6
|
type BarChartOptions,
|
|
7
|
+
type BubbleOptions,
|
|
7
8
|
barChartLayout,
|
|
8
9
|
barGauge,
|
|
9
10
|
boxplot,
|
|
@@ -13,19 +14,34 @@ import {
|
|
|
13
14
|
computeDomain,
|
|
14
15
|
donut,
|
|
15
16
|
extendDomainToNice,
|
|
17
|
+
type FunnelOptions,
|
|
16
18
|
gauge,
|
|
17
19
|
type HeatmapChartOptions,
|
|
20
|
+
type HorizontalBarOptions,
|
|
21
|
+
hbarLayout,
|
|
18
22
|
histogram,
|
|
19
23
|
line,
|
|
20
24
|
niceLogTicks,
|
|
21
25
|
niceStep,
|
|
26
|
+
type ParetoOptions,
|
|
22
27
|
pie,
|
|
28
|
+
type RadarOptions,
|
|
29
|
+
rankedBarLayout,
|
|
23
30
|
renderAreaSvg,
|
|
24
31
|
renderBarChartSvg,
|
|
32
|
+
renderBubbleSvg,
|
|
33
|
+
renderFunnelSvg,
|
|
34
|
+
renderHbarSvg,
|
|
25
35
|
renderHeatmapSvg,
|
|
36
|
+
renderParetoSvg,
|
|
37
|
+
renderRadarSvg,
|
|
38
|
+
renderTreemapSvg,
|
|
39
|
+
renderWaterfallSvg,
|
|
26
40
|
scatter,
|
|
27
41
|
sparkline,
|
|
28
42
|
stat,
|
|
43
|
+
type TreemapOptions,
|
|
44
|
+
type WaterfallOptions,
|
|
29
45
|
} from "@k2b/ui/charts/core";
|
|
30
46
|
import { z } from "zod";
|
|
31
47
|
import { type ChartFormat, type ChartInput, type ChartNumberFormat, chartDateTime, chartKinds } from "./chart-schema";
|
|
@@ -33,15 +49,29 @@ import { type ChartFormat, type ChartInput, type ChartNumberFormat, chartDateTim
|
|
|
33
49
|
export * from "./chart-schema";
|
|
34
50
|
|
|
35
51
|
type RenderOptions = {
|
|
36
|
-
[K in ChartInput["kind"]]: { kind: K } & (K extends "
|
|
37
|
-
?
|
|
38
|
-
: K extends "
|
|
39
|
-
?
|
|
40
|
-
: K extends "
|
|
41
|
-
?
|
|
42
|
-
: K extends
|
|
43
|
-
?
|
|
44
|
-
:
|
|
52
|
+
[K in ChartInput["kind"]]: { kind: K } & (K extends "funnel"
|
|
53
|
+
? FunnelOptions
|
|
54
|
+
: K extends "hbar"
|
|
55
|
+
? HorizontalBarOptions
|
|
56
|
+
: K extends "waterfall"
|
|
57
|
+
? WaterfallOptions
|
|
58
|
+
: K extends "area"
|
|
59
|
+
? AreaOptions
|
|
60
|
+
: K extends "treemap"
|
|
61
|
+
? TreemapOptions
|
|
62
|
+
: K extends "pareto"
|
|
63
|
+
? ParetoOptions
|
|
64
|
+
: K extends "bubble"
|
|
65
|
+
? BubbleOptions
|
|
66
|
+
: K extends "radar"
|
|
67
|
+
? RadarOptions
|
|
68
|
+
: K extends "heatmap"
|
|
69
|
+
? HeatmapChartOptions
|
|
70
|
+
: K extends "bar"
|
|
71
|
+
? BarChartOptions & { data: NonNullable<BarChartOptions["data"]> }
|
|
72
|
+
: K extends keyof typeof charts
|
|
73
|
+
? Omit<Parameters<(typeof charts)[K]>[0], "width" | "height" | "inspect">
|
|
74
|
+
: never);
|
|
45
75
|
}[ChartInput["kind"]];
|
|
46
76
|
|
|
47
77
|
/**
|
|
@@ -56,8 +86,20 @@ const labels = /* @__PURE__ */ i18n.define({
|
|
|
56
86
|
messages: {
|
|
57
87
|
en: {
|
|
58
88
|
category: "Category",
|
|
89
|
+
size: "Size",
|
|
90
|
+
axis: "Axis",
|
|
59
91
|
series: "Series",
|
|
92
|
+
group: "Group",
|
|
93
|
+
previous: "Previous",
|
|
94
|
+
change: "Change",
|
|
95
|
+
cumulative: "Cumulative",
|
|
96
|
+
balance: "Balance",
|
|
97
|
+
before: "Balance before",
|
|
98
|
+
inflow: "In",
|
|
99
|
+
outflow: "Out",
|
|
100
|
+
after: "Balance after",
|
|
60
101
|
value: "Value",
|
|
102
|
+
total: "Total",
|
|
61
103
|
share: "Share",
|
|
62
104
|
position: "Position",
|
|
63
105
|
from: "From",
|
|
@@ -71,11 +113,26 @@ const labels = /* @__PURE__ */ i18n.define({
|
|
|
71
113
|
whiskerLow: "Whisker low",
|
|
72
114
|
whiskerHigh: "Whisker high",
|
|
73
115
|
delta: "Change",
|
|
116
|
+
startShare: "Share of start",
|
|
117
|
+
previousShare: "Share of previous step",
|
|
118
|
+
rank: "Rank",
|
|
74
119
|
},
|
|
75
120
|
de: {
|
|
76
121
|
category: "Kategorie",
|
|
122
|
+
size: "Größe",
|
|
123
|
+
axis: "Achse",
|
|
77
124
|
series: "Datenreihe",
|
|
125
|
+
group: "Bereich",
|
|
126
|
+
previous: "Vorher",
|
|
127
|
+
change: "Änderung",
|
|
128
|
+
cumulative: "Kumuliert",
|
|
129
|
+
balance: "Stand",
|
|
130
|
+
before: "Stand davor",
|
|
131
|
+
inflow: "Rein",
|
|
132
|
+
outflow: "Raus",
|
|
133
|
+
after: "Stand danach",
|
|
78
134
|
value: "Wert",
|
|
135
|
+
total: "Gesamt",
|
|
79
136
|
share: "Anteil",
|
|
80
137
|
position: "Position",
|
|
81
138
|
from: "Von",
|
|
@@ -89,6 +146,9 @@ const labels = /* @__PURE__ */ i18n.define({
|
|
|
89
146
|
whiskerLow: "Unterer Whisker",
|
|
90
147
|
whiskerHigh: "Oberer Whisker",
|
|
91
148
|
delta: "Änderung",
|
|
149
|
+
startShare: "Anteil am Start",
|
|
150
|
+
previousShare: "Anteil am vorigen Schritt",
|
|
151
|
+
rank: "Rang",
|
|
92
152
|
},
|
|
93
153
|
},
|
|
94
154
|
});
|
|
@@ -137,7 +197,19 @@ export const chartReferenceValues = (input: ChartInput, name: "x" | "y") =>
|
|
|
137
197
|
|
|
138
198
|
/** The value axes of a chart with what they show: the data checks and the label room read them alike. */
|
|
139
199
|
export const chartValueAxes = (input: ChartInput): { values: number[]; axis?: ChartAxisInput; zero?: boolean; name: "x" | "y" }[] => {
|
|
140
|
-
if (input.kind === "bar")
|
|
200
|
+
if (input.kind === "bar")
|
|
201
|
+
return [
|
|
202
|
+
{
|
|
203
|
+
values: [
|
|
204
|
+
...input.data.map((item) => item.value),
|
|
205
|
+
...(input.series?.flatMap((s) => s.data.filter((v): v is number => v !== null)) ?? []),
|
|
206
|
+
...(input.compare?.data.filter((v): v is number => v !== null) ?? []),
|
|
207
|
+
],
|
|
208
|
+
axis: input.yAxis,
|
|
209
|
+
zero: true,
|
|
210
|
+
name: "y",
|
|
211
|
+
},
|
|
212
|
+
];
|
|
141
213
|
if (input.kind === "boxplot") return [{ values: input.data.flatMap((item) => item.values), axis: input.yAxis, name: "y" }];
|
|
142
214
|
if (input.kind === "histogram") {
|
|
143
215
|
if (!input.references?.length) return [{ values: input.data, name: "x" }];
|
|
@@ -246,13 +318,30 @@ const numberFormat = (format: ChartFormat | undefined) => (format && !("date" in
|
|
|
246
318
|
/** The values a chart writes with its value format, for whole-number currency. */
|
|
247
319
|
const formattedValues = (input: ChartInput): number[] => {
|
|
248
320
|
switch (input.kind) {
|
|
321
|
+
case "bubble":
|
|
322
|
+
return input.data.flatMap((p) => (p.size === undefined ? [] : [p.size]));
|
|
323
|
+
case "radar":
|
|
324
|
+
return input.series.flatMap((s) => s.data);
|
|
325
|
+
case "waterfall":
|
|
326
|
+
return [input.start.value, ...input.steps.map((step) => step.value)];
|
|
249
327
|
case "area":
|
|
250
328
|
return input.series.flatMap((s) => s.data.filter((v): v is number => v !== null));
|
|
251
329
|
case "line":
|
|
252
330
|
case "scatter":
|
|
253
331
|
return [...input.series.flatMap((series) => series.data.map((point) => point.y)), ...chartReferenceValues(input, "y")];
|
|
254
332
|
case "bar":
|
|
255
|
-
return [
|
|
333
|
+
return [
|
|
334
|
+
...input.data.map((item) => item.value),
|
|
335
|
+
...(input.series?.flatMap((s) => s.data.filter((v): v is number => v !== null)) ?? []),
|
|
336
|
+
...(input.compare?.data.filter((v): v is number => v !== null) ?? []),
|
|
337
|
+
...chartReferenceValues(input, "y"),
|
|
338
|
+
];
|
|
339
|
+
case "hbar":
|
|
340
|
+
return input.rows.flatMap((r) => r.values.filter((v): v is number => v !== null));
|
|
341
|
+
case "funnel":
|
|
342
|
+
return input.data.flatMap((d) => (d.value === null ? [] : [d.value]));
|
|
343
|
+
case "treemap":
|
|
344
|
+
case "pareto":
|
|
256
345
|
case "pie":
|
|
257
346
|
case "donut":
|
|
258
347
|
return input.data.map((item) => item.value);
|
|
@@ -276,7 +365,7 @@ const formattedValues = (input: ChartInput): number[] => {
|
|
|
276
365
|
}
|
|
277
366
|
};
|
|
278
367
|
const formats = (input: ChartInput, locale: string): Formats => {
|
|
279
|
-
const yFormat = "yAxis" in input ? numberFormat(input.yAxis?.format) : "format" in input ? input.format : undefined;
|
|
368
|
+
const yFormat = ("yAxis" in input ? numberFormat(input.yAxis?.format) : undefined) ?? ("format" in input ? input.format : undefined);
|
|
280
369
|
const y = chartNumberFormatters(yFormat, locale, formattedValues(input));
|
|
281
370
|
const xFormat = "xAxis" in input && input.kind !== "bar" ? input.xAxis?.format : undefined;
|
|
282
371
|
const x = chartNumberFormatters(numberFormat(xFormat), locale);
|
|
@@ -323,6 +412,8 @@ export function chartLegend(
|
|
|
323
412
|
input: ChartInput,
|
|
324
413
|
locale: string,
|
|
325
414
|
): { label: string; color: number; style?: "solid" | "dashed" | "dotted"; reference?: boolean }[] {
|
|
415
|
+
// These category renderers own their live legends, including prepared snapshots.
|
|
416
|
+
if (input.kind === "hbar" || (input.kind === "bar" && input.series)) return [];
|
|
326
417
|
if (input.kind === "bar")
|
|
327
418
|
return input.colorByBar && input.legend ? input.data.map((entry, index) => ({ label: entry.label, color: index % 8 })) : [];
|
|
328
419
|
if (input.kind === "area") return input.legend === false ? [] : input.series.map((s, i) => ({ label: s.label, color: i % 8 }));
|
|
@@ -354,7 +445,7 @@ export function chartRenderOptions(
|
|
|
354
445
|
formats?: { x?: (v: number) => string; y?: (v: number) => string; value?: (v: number) => string };
|
|
355
446
|
} = {},
|
|
356
447
|
): RenderOptions {
|
|
357
|
-
if (
|
|
448
|
+
if ("locale" in input && input.locale) locale = input.locale;
|
|
358
449
|
const defaults = formats(input, locale);
|
|
359
450
|
// Function formats must be measured with the same text the renderer will print.
|
|
360
451
|
const format = {
|
|
@@ -367,6 +458,74 @@ export function chartRenderOptions(
|
|
|
367
458
|
const width = options.width ?? WIDTH;
|
|
368
459
|
const references = renderReferences(input);
|
|
369
460
|
switch (input.kind) {
|
|
461
|
+
case "treemap":
|
|
462
|
+
return {
|
|
463
|
+
kind: "treemap",
|
|
464
|
+
legend: input.legend,
|
|
465
|
+
groups: input.groups,
|
|
466
|
+
data: input.data,
|
|
467
|
+
format: format.value,
|
|
468
|
+
locale: input.locale ?? locale,
|
|
469
|
+
displayWidth: options.width,
|
|
470
|
+
};
|
|
471
|
+
case "pareto":
|
|
472
|
+
return {
|
|
473
|
+
kind: "pareto",
|
|
474
|
+
labels: input.labels,
|
|
475
|
+
data: input.data,
|
|
476
|
+
format: format.value,
|
|
477
|
+
locale: input.locale ?? locale,
|
|
478
|
+
displayWidth: options.width,
|
|
479
|
+
};
|
|
480
|
+
case "bubble":
|
|
481
|
+
return {
|
|
482
|
+
kind: "bubble",
|
|
483
|
+
groups: input.groups,
|
|
484
|
+
data: input.data,
|
|
485
|
+
xLabel: input.xLabel,
|
|
486
|
+
yLabel: input.yLabel,
|
|
487
|
+
sizeLabel: input.sizeLabel,
|
|
488
|
+
averages: input.averages,
|
|
489
|
+
legend: input.legend,
|
|
490
|
+
locale,
|
|
491
|
+
displayWidth: options.width,
|
|
492
|
+
xFormat: options.formats?.x ?? (input.xFormat ? chartNumberFormatters(input.xFormat, locale).value : undefined),
|
|
493
|
+
yFormat: options.formats?.y ?? (input.yFormat ? chartNumberFormatters(input.yFormat, locale).value : undefined),
|
|
494
|
+
sizeFormat: format.value,
|
|
495
|
+
};
|
|
496
|
+
case "radar":
|
|
497
|
+
return {
|
|
498
|
+
kind: "radar",
|
|
499
|
+
axes: input.axes,
|
|
500
|
+
max: input.max,
|
|
501
|
+
unit: input.unit,
|
|
502
|
+
series: input.series,
|
|
503
|
+
legend: input.legend,
|
|
504
|
+
locale,
|
|
505
|
+
displayWidth: options.width,
|
|
506
|
+
format: format.value,
|
|
507
|
+
};
|
|
508
|
+
case "funnel":
|
|
509
|
+
return { kind: "funnel", data: input.data, valueFormat: format.value, locale };
|
|
510
|
+
case "hbar":
|
|
511
|
+
return {
|
|
512
|
+
kind: "hbar",
|
|
513
|
+
locale,
|
|
514
|
+
series: input.series,
|
|
515
|
+
rows: input.rows,
|
|
516
|
+
legend: input.legend,
|
|
517
|
+
valueFormat: format.value,
|
|
518
|
+
};
|
|
519
|
+
case "waterfall":
|
|
520
|
+
return {
|
|
521
|
+
kind: "waterfall",
|
|
522
|
+
start: input.start,
|
|
523
|
+
steps: input.steps,
|
|
524
|
+
end: input.end,
|
|
525
|
+
displayWidth: options.width ?? 296,
|
|
526
|
+
locale,
|
|
527
|
+
format: format.value,
|
|
528
|
+
};
|
|
370
529
|
case "area":
|
|
371
530
|
return {
|
|
372
531
|
kind: "area",
|
|
@@ -386,14 +545,23 @@ export function chartRenderOptions(
|
|
|
386
545
|
if (input.kind === "bar")
|
|
387
546
|
return {
|
|
388
547
|
kind: "bar",
|
|
548
|
+
locale,
|
|
389
549
|
data: input.data,
|
|
550
|
+
x: input.x,
|
|
551
|
+
series: input.series,
|
|
552
|
+
layout: input.layout,
|
|
553
|
+
normalize: input.normalize,
|
|
554
|
+
share: input.share,
|
|
555
|
+
limit: input.limit,
|
|
556
|
+
tone: input.tone,
|
|
557
|
+
compare: input.compare,
|
|
390
558
|
xAxis: input.xAxis,
|
|
391
559
|
yAxis: axis(yAxis, options.formats?.y ?? format.tick),
|
|
392
560
|
// A host's own function format writes printed values like its ticks and table cells.
|
|
393
561
|
valueFormat: options.formats?.value ?? options.formats?.y ?? format.value,
|
|
394
562
|
colorByBar: input.colorByBar,
|
|
395
563
|
showValues: input.showValues,
|
|
396
|
-
legend: options.legend === "outside" ? false : input.legend,
|
|
564
|
+
legend: input.series ? input.legend : options.legend === "outside" ? false : input.legend,
|
|
397
565
|
references,
|
|
398
566
|
...(options.width === undefined ? {} : { displayWidth: options.width }),
|
|
399
567
|
};
|
|
@@ -545,6 +713,20 @@ const decode = (value: string) =>
|
|
|
545
713
|
export const drawChart = (options: RenderOptions): string => {
|
|
546
714
|
const common = { width: WIDTH, height: HEIGHT, inspect: true };
|
|
547
715
|
switch (options.kind) {
|
|
716
|
+
case "treemap":
|
|
717
|
+
return renderTreemapSvg({ ...options, ...common });
|
|
718
|
+
case "pareto":
|
|
719
|
+
return renderParetoSvg({ ...options, ...common });
|
|
720
|
+
case "bubble":
|
|
721
|
+
return renderBubbleSvg({ ...options, ...common });
|
|
722
|
+
case "radar":
|
|
723
|
+
return renderRadarSvg({ ...options, ...common });
|
|
724
|
+
case "funnel":
|
|
725
|
+
return renderFunnelSvg({ ...options, ...common });
|
|
726
|
+
case "hbar":
|
|
727
|
+
return renderHbarSvg({ ...options, ...common, height: hbarLayout(options).height });
|
|
728
|
+
case "waterfall":
|
|
729
|
+
return renderWaterfallSvg({ ...options, ...common });
|
|
548
730
|
case "area":
|
|
549
731
|
return renderAreaSvg({ ...options, ...common });
|
|
550
732
|
case "bar":
|
|
@@ -594,7 +776,12 @@ export const chartMarkKey = (datum: Pick<ChartDatum, "index" | "seriesIndex"> &
|
|
|
594
776
|
`${datum.role === "outlier" ? "outlier:" : datum.role === "value" ? "value:" : ""}${datum.seriesIndex ?? 0}:${datum.index}`;
|
|
595
777
|
|
|
596
778
|
export type ChartColumn = { id: string; label: string; numeric: boolean };
|
|
597
|
-
export type ChartRow = {
|
|
779
|
+
export type ChartRow = {
|
|
780
|
+
key: string;
|
|
781
|
+
cells: Record<string, string>;
|
|
782
|
+
values: Record<string, string | number>;
|
|
783
|
+
tooltip?: { title?: string; rows: { label: string; value: string }[] };
|
|
784
|
+
};
|
|
598
785
|
export type ChartTable = { columns: ChartColumn[]; rows: ChartRow[] };
|
|
599
786
|
|
|
600
787
|
/** The first text column titles the tooltip; the other columns become rows, matching the table in every host. */
|
|
@@ -603,6 +790,7 @@ export function chartTooltip(
|
|
|
603
790
|
row: ChartRow,
|
|
604
791
|
kind?: ChartInput["kind"],
|
|
605
792
|
): { title?: string; rows: { label: string; value: string }[] } {
|
|
793
|
+
if (row.tooltip) return row.tooltip;
|
|
606
794
|
if (kind === "area")
|
|
607
795
|
return {
|
|
608
796
|
title: row.cells.x,
|
|
@@ -627,7 +815,7 @@ export function chartRows(
|
|
|
627
815
|
locale: string,
|
|
628
816
|
options: { formats?: { x?: (v: number) => string; y?: (v: number) => string; value?: (v: number) => string } } = {},
|
|
629
817
|
) {
|
|
630
|
-
if (
|
|
818
|
+
if ("locale" in input && input.locale) locale = input.locale;
|
|
631
819
|
const t = labels.resolve([locale]).t;
|
|
632
820
|
const format = formats(input, locale);
|
|
633
821
|
const valueFormat = input.tooltip?.format
|
|
@@ -644,12 +832,23 @@ export function chartRows(
|
|
|
644
832
|
const unit = "unit" in input && input.unit ? ` ${input.unit}` : "";
|
|
645
833
|
const columns = new Map<string, ChartColumn>();
|
|
646
834
|
const rows: ChartRow[] = [];
|
|
835
|
+
if (input.kind === "waterfall") {
|
|
836
|
+
for (const [id, label, numeric] of [
|
|
837
|
+
["label", t.category, false],
|
|
838
|
+
["before", t.before, true],
|
|
839
|
+
["inflow", t.inflow, true],
|
|
840
|
+
["outflow", t.outflow, true],
|
|
841
|
+
["after", t.after, true],
|
|
842
|
+
] as const)
|
|
843
|
+
columns.set(id, { id, label, numeric });
|
|
844
|
+
}
|
|
647
845
|
const named = series.some((entry) => entry.label);
|
|
648
846
|
const add = (datum: ChartDatum): ChartRow => {
|
|
847
|
+
const tooltip = input.kind === "waterfall" ? { title: datum.label, rows: [] as { label: string; value: string }[] } : undefined;
|
|
649
848
|
const cells: Record<string, string> = {};
|
|
650
849
|
const values: Record<string, string | number> = {};
|
|
651
|
-
const cell = (id: string, label: string, value: string | number, text: string) => {
|
|
652
|
-
if (!columns.has(id)) columns.set(id, { id, label, numeric
|
|
850
|
+
const cell = (id: string, label: string, value: string | number, text: string, numeric = typeof value === "number") => {
|
|
851
|
+
if (!columns.has(id)) columns.set(id, { id, label, numeric });
|
|
653
852
|
cells[id] = text;
|
|
654
853
|
values[id] = value;
|
|
655
854
|
};
|
|
@@ -663,13 +862,40 @@ export function chartRows(
|
|
|
663
862
|
// Bars may hide some axis labels; the table names every category.
|
|
664
863
|
const label =
|
|
665
864
|
input.kind === "bar" || input.kind === "pie" || input.kind === "donut" || input.kind === "boxplot" || input.kind === "barGauge"
|
|
666
|
-
? input.
|
|
865
|
+
? ((input.kind === "bar" && input.series
|
|
866
|
+
? undefined
|
|
867
|
+
: input.data[datum.role === "outlier" ? (datum.seriesIndex ?? 0) : datum.index]?.label) ?? datum.label)
|
|
667
868
|
: datum.label;
|
|
668
869
|
if (label)
|
|
669
|
-
cell(
|
|
870
|
+
cell(
|
|
871
|
+
"label",
|
|
872
|
+
input.kind === "heatmap" && input.layout === "calendar"
|
|
873
|
+
? calendarWords(locale).day
|
|
874
|
+
: input.kind === "radar"
|
|
875
|
+
? t.axis
|
|
876
|
+
: t.category,
|
|
877
|
+
label,
|
|
878
|
+
label,
|
|
879
|
+
);
|
|
670
880
|
}
|
|
671
881
|
for (const field of datum.values) {
|
|
672
882
|
const value = field.value;
|
|
883
|
+
if (input.kind === "bubble") {
|
|
884
|
+
const label =
|
|
885
|
+
field.key === "x"
|
|
886
|
+
? input.xLabel
|
|
887
|
+
: field.key === "y"
|
|
888
|
+
? input.yLabel
|
|
889
|
+
: field.key === "size"
|
|
890
|
+
? (input.sizeLabel ?? t.size)
|
|
891
|
+
: t.group;
|
|
892
|
+
cell(field.key, label, value, field.formatted ?? String(value));
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
if (input.kind === "radar" && field.key.startsWith("series:")) {
|
|
896
|
+
cell(field.key, input.series[Number(field.key.slice(7))]!.label, value, field.formatted ?? String(value));
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
673
899
|
if (typeof value === "string" && input.kind === "area" && field.key === "x") {
|
|
674
900
|
cell("x", t.position, value, field.formatted ?? value);
|
|
675
901
|
continue;
|
|
@@ -678,8 +904,79 @@ export function chartRows(
|
|
|
678
904
|
cell(field.key, field.key.toUpperCase(), value, value);
|
|
679
905
|
continue;
|
|
680
906
|
}
|
|
907
|
+
if (input.kind === "treemap" && field.key === "group" && typeof value === "string") {
|
|
908
|
+
cell("group", t.group, value, value);
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (
|
|
912
|
+
input.kind === "pareto" &&
|
|
913
|
+
(field.key === "previous" || field.key === "change" || field.key === "cumulative") &&
|
|
914
|
+
typeof value === "number"
|
|
915
|
+
) {
|
|
916
|
+
cell(field.key, field.key === "previous" ? input.labels[1] : t[field.key], value, field.formatted ?? valueFormat(value));
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
if (input.kind === "hbar" || (input.kind === "bar" && input.series)) {
|
|
920
|
+
const seriesIndex = field.key.startsWith("series:") ? Number(field.key.slice(7)) : undefined;
|
|
921
|
+
const label =
|
|
922
|
+
seriesIndex === undefined
|
|
923
|
+
? field.key === "previous" && input.kind === "bar"
|
|
924
|
+
? (input.compare?.label ?? field.key)
|
|
925
|
+
: field.key === "total"
|
|
926
|
+
? t.total
|
|
927
|
+
: field.key === "delta"
|
|
928
|
+
? t.delta
|
|
929
|
+
: field.key
|
|
930
|
+
: input.kind === "hbar"
|
|
931
|
+
? input.series[seriesIndex]!
|
|
932
|
+
: input.series![seriesIndex]!.label;
|
|
933
|
+
cell(field.key, label, value, field.formatted ?? String(value), true);
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
if (
|
|
937
|
+
field.key === "startPercent" ||
|
|
938
|
+
field.key === "previousPercent" ||
|
|
939
|
+
field.key === "rank" ||
|
|
940
|
+
((input.kind === "bar" || input.kind === "funnel") && typeof value === "string")
|
|
941
|
+
) {
|
|
942
|
+
cell(
|
|
943
|
+
field.key,
|
|
944
|
+
field.key === "startPercent"
|
|
945
|
+
? t.startShare
|
|
946
|
+
: field.key === "previousPercent"
|
|
947
|
+
? t.previousShare
|
|
948
|
+
: field.key === "rank"
|
|
949
|
+
? t.rank
|
|
950
|
+
: field.key === "percent"
|
|
951
|
+
? t.share
|
|
952
|
+
: t.value,
|
|
953
|
+
value,
|
|
954
|
+
field.formatted ?? String(value),
|
|
955
|
+
true,
|
|
956
|
+
);
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
681
959
|
if (typeof value !== "number" || field.key === "total") continue;
|
|
682
|
-
if (
|
|
960
|
+
if (input.kind === "waterfall" && ["balance", "before", "inflow", "outflow", "change", "after"].includes(field.key)) {
|
|
961
|
+
const label =
|
|
962
|
+
field.key === "balance"
|
|
963
|
+
? t.balance
|
|
964
|
+
: field.key === "before"
|
|
965
|
+
? t.before
|
|
966
|
+
: field.key === "inflow"
|
|
967
|
+
? t.inflow
|
|
968
|
+
: field.key === "outflow"
|
|
969
|
+
? t.outflow
|
|
970
|
+
: field.key === "after"
|
|
971
|
+
? t.after
|
|
972
|
+
: t.delta;
|
|
973
|
+
const arrow = field.key === "inflow" ? "↙ " : field.key === "outflow" ? "↗ " : field.key === "change" ? "= " : "";
|
|
974
|
+
const text = `${arrow}${valueFormat(field.key === "inflow" || field.key === "outflow" ? Math.abs(value) : value)}`;
|
|
975
|
+
// Totals occupy the ending balance column; zero changes are neutral inflows.
|
|
976
|
+
const id = field.key === "balance" ? "after" : field.key === "change" ? "inflow" : field.key;
|
|
977
|
+
cell(id, label, value, text);
|
|
978
|
+
if (field.key !== "before") tooltip?.rows.push({ label, value: text });
|
|
979
|
+
} else if (field.key === "x") {
|
|
683
980
|
const header =
|
|
684
981
|
(input.kind === "line" || input.kind === "scatter" ? input.xAxis?.label : undefined) ??
|
|
685
982
|
(input.kind === "area" ? t.position : "X");
|
|
@@ -703,11 +1000,13 @@ export function chartRows(
|
|
|
703
1000
|
cell(
|
|
704
1001
|
"value",
|
|
705
1002
|
input.tooltip?.label ??
|
|
706
|
-
(input.kind === "
|
|
707
|
-
? input.
|
|
708
|
-
: input.kind === "
|
|
709
|
-
? input.label
|
|
710
|
-
:
|
|
1003
|
+
(input.kind === "pareto"
|
|
1004
|
+
? input.labels[0]
|
|
1005
|
+
: input.kind === "bar"
|
|
1006
|
+
? input.yAxis?.label
|
|
1007
|
+
: input.kind === "heatmap" && input.layout === "calendar"
|
|
1008
|
+
? input.label
|
|
1009
|
+
: undefined) ??
|
|
711
1010
|
t.value,
|
|
712
1011
|
value,
|
|
713
1012
|
input.kind === "heatmap" && input.layout === "calendar"
|
|
@@ -728,11 +1027,41 @@ export function chartRows(
|
|
|
728
1027
|
cell(field.key, t[field.key], value, `${fieldFormat(value)}${field.key === "min" || field.key === "max" ? unit : ""}`);
|
|
729
1028
|
}
|
|
730
1029
|
}
|
|
731
|
-
const row = { key: chartMarkKey(datum), cells, values };
|
|
1030
|
+
const row = { key: chartMarkKey(datum), cells, values, ...(tooltip ? { tooltip } : {}) };
|
|
732
1031
|
rows.push(row);
|
|
733
1032
|
return row;
|
|
734
1033
|
};
|
|
735
|
-
return {
|
|
1034
|
+
return {
|
|
1035
|
+
add,
|
|
1036
|
+
columns: () => [...columns.values()],
|
|
1037
|
+
table: (): ChartTable => {
|
|
1038
|
+
if (input.kind === "bar" && input.layout === "rows" && input.limit !== undefined) {
|
|
1039
|
+
const source = { ...input, limit: undefined };
|
|
1040
|
+
const all = chartRows(source, locale, options);
|
|
1041
|
+
const layout = rankedBarLayout({ data: source.data, title: source.title, subtitle: source.subtitle, valueFormat, locale });
|
|
1042
|
+
for (const row of layout.rows) {
|
|
1043
|
+
const value = Number.isFinite(row.value) ? row.value : null;
|
|
1044
|
+
all.add({
|
|
1045
|
+
role: "item",
|
|
1046
|
+
index: row.index,
|
|
1047
|
+
label: row.label,
|
|
1048
|
+
anchor: [0, row.y],
|
|
1049
|
+
values: [
|
|
1050
|
+
{ key: "value", value: value ?? "–", formatted: value === null ? "–" : valueFormat(value) },
|
|
1051
|
+
{
|
|
1052
|
+
key: "percent",
|
|
1053
|
+
value: row.share === null ? "–" : row.share * 100,
|
|
1054
|
+
formatted: row.share === null ? "–" : percent.format(row.share),
|
|
1055
|
+
},
|
|
1056
|
+
{ key: "rank", value: row.rank },
|
|
1057
|
+
],
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
return all.table();
|
|
1061
|
+
}
|
|
1062
|
+
return { columns: [...columns.values()], rows };
|
|
1063
|
+
},
|
|
1064
|
+
};
|
|
736
1065
|
}
|
|
737
1066
|
|
|
738
1067
|
/** The data behind every mark of the chart as a table: the accessible alternative and what Copy data copies. */
|
|
@@ -745,11 +1074,21 @@ export function chartTable(input: ChartInput, locale: string): ChartTable {
|
|
|
745
1074
|
/** Input values count against the budget, including observations summarized into boxes or bins. */
|
|
746
1075
|
export const chartMarks = (input: ChartInput): number => {
|
|
747
1076
|
switch (input.kind) {
|
|
1077
|
+
case "pareto":
|
|
1078
|
+
return input.data.length * 3;
|
|
1079
|
+
case "radar":
|
|
1080
|
+
return input.axes.length * input.series.length;
|
|
1081
|
+
case "waterfall":
|
|
1082
|
+
return input.steps.length + 2;
|
|
748
1083
|
case "area":
|
|
749
1084
|
return input.series.reduce((sum, s) => sum + s.data.length, 0);
|
|
750
1085
|
case "line":
|
|
751
1086
|
case "scatter":
|
|
752
1087
|
return input.series.reduce((sum, series) => sum + series.data.length, 0);
|
|
1088
|
+
case "bar":
|
|
1089
|
+
return input.series ? (input.x?.length ?? 0) * (input.series.length + (input.compare ? 1 : 0)) : input.data.length;
|
|
1090
|
+
case "hbar":
|
|
1091
|
+
return input.rows.length * input.series.length;
|
|
753
1092
|
case "boxplot":
|
|
754
1093
|
return input.data.reduce((sum, box) => sum + box.values.length, 0);
|
|
755
1094
|
case "gauge":
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { highlight, text } from "@k2b/stdlib";
|
|
2
2
|
import { prepareChartSnapshot } from "@k2b/ui";
|
|
3
|
-
import { barChartLayout } from "@k2b/ui/charts";
|
|
3
|
+
import { barChartLayout, hbarLayout } from "@k2b/ui/charts";
|
|
4
|
+
import { paretoLayout } from "@k2b/ui/charts/core";
|
|
4
5
|
import { Lexer, type Tokens } from "marked";
|
|
5
6
|
import type { z } from "zod";
|
|
6
7
|
import {
|
|
@@ -457,8 +458,13 @@ export const chartBlockFrame = (body: string[], options: Pick<MarkdownChartOptio
|
|
|
457
458
|
*/
|
|
458
459
|
export const notebookChartHeight = (input: ChartInput, locale: string): number | undefined => {
|
|
459
460
|
const options = chartRenderOptions(input, locale, { legend: "outside" });
|
|
461
|
+
if (options.kind === "treemap") return 280;
|
|
462
|
+
if (options.kind === "pareto") return paretoLayout(options).height;
|
|
463
|
+
|
|
464
|
+
if (options.kind === "bubble" || options.kind === "radar") return 280;
|
|
465
|
+
if (options.kind === "hbar") return hbarLayout(options).height;
|
|
460
466
|
if (options.kind === "heatmap" && options.layout === "calendar") return 280;
|
|
461
|
-
if (options.kind === "area") return 280;
|
|
467
|
+
if (options.kind === "waterfall" || options.kind === "area") return 280;
|
|
462
468
|
if (options.kind !== "bar") return undefined;
|
|
463
469
|
const layout = barChartLayout(options);
|
|
464
470
|
return layout.layout === "rows" ? layout.height : undefined;
|