@k2b/editor 0.8.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 +94 -0
- package/src/charts.ts +231 -26
- package/src/markdown/lib/chart-blocks.ts +5 -0
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
|
@@ -35,6 +35,7 @@ export const chartKindSchema = () =>
|
|
|
35
35
|
"area",
|
|
36
36
|
"bar",
|
|
37
37
|
"hbar",
|
|
38
|
+
"funnel",
|
|
38
39
|
"line",
|
|
39
40
|
"scatter",
|
|
40
41
|
"pie",
|
|
@@ -46,6 +47,10 @@ export const chartKindSchema = () =>
|
|
|
46
47
|
"heatmap",
|
|
47
48
|
"barGauge",
|
|
48
49
|
"stat",
|
|
50
|
+
"treemap",
|
|
51
|
+
"pareto",
|
|
52
|
+
"bubble",
|
|
53
|
+
"radar",
|
|
49
54
|
]);
|
|
50
55
|
|
|
51
56
|
export const chartParts = (limits?: ChartLimits) => {
|
|
@@ -219,6 +224,10 @@ export const chartKinds = (limits?: ChartLimits) => {
|
|
|
219
224
|
).optional(),
|
|
220
225
|
layout: z.enum(["columns", "rows", "grouped", "stacked"]).optional(),
|
|
221
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(),
|
|
222
231
|
compare: z.object({ label: ChartLabel, data: list(ChartNumber.nullable(), 0, limits?.maxValues) }).optional(),
|
|
223
232
|
xAxis: ChartAxis.pick({ label: true }).extend({ ordered: z.boolean().optional() }).strict().optional(),
|
|
224
233
|
yAxis: ChartAxis.optional(),
|
|
@@ -231,6 +240,10 @@ export const chartKinds = (limits?: ChartLimits) => {
|
|
|
231
240
|
const issue = (message: string) => ctx.addIssue({ code: "custom", message });
|
|
232
241
|
if (limits && !bar.series && !bar.data.length)
|
|
233
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.");
|
|
234
247
|
if (bar.series) {
|
|
235
248
|
if (!bar.x || bar.series.some((s) => s.data.length !== bar.x?.length)) issue("Bar series must have the same length as x.");
|
|
236
249
|
if (bar.data.length) issue("Use data or aligned series, not both.");
|
|
@@ -244,6 +257,12 @@ export const chartKinds = (limits?: ChartLimits) => {
|
|
|
244
257
|
if (bar.compare && (bar.series?.length !== 1 || bar.compare.data.length !== bar.x?.length || bar.layout === "stacked"))
|
|
245
258
|
issue("Comparison needs one current series and aligned previous values.");
|
|
246
259
|
}),
|
|
260
|
+
z.object({
|
|
261
|
+
kind: z.literal("funnel"),
|
|
262
|
+
...ChartKindCommon,
|
|
263
|
+
data: list(z.object({ label: ChartLabel, value: z.number().nonnegative().nullable() }), 1, limits?.maxValues),
|
|
264
|
+
format: numberFormat.optional(),
|
|
265
|
+
}),
|
|
247
266
|
z
|
|
248
267
|
.object({
|
|
249
268
|
kind: z.literal("hbar"),
|
|
@@ -398,6 +417,81 @@ export const chartKinds = (limits?: ChartLimits) => {
|
|
|
398
417
|
better: z.enum(["up", "down"]).optional(),
|
|
399
418
|
sparkline: list(ChartNumber, 2, limits?.maxValues).optional(),
|
|
400
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
|
+
}),
|
|
401
495
|
]);
|
|
402
496
|
};
|
|
403
497
|
|
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,6 +14,7 @@ import {
|
|
|
13
14
|
computeDomain,
|
|
14
15
|
donut,
|
|
15
16
|
extendDomainToNice,
|
|
17
|
+
type FunnelOptions,
|
|
16
18
|
gauge,
|
|
17
19
|
type HeatmapChartOptions,
|
|
18
20
|
type HorizontalBarOptions,
|
|
@@ -21,15 +23,24 @@ import {
|
|
|
21
23
|
line,
|
|
22
24
|
niceLogTicks,
|
|
23
25
|
niceStep,
|
|
26
|
+
type ParetoOptions,
|
|
24
27
|
pie,
|
|
28
|
+
type RadarOptions,
|
|
29
|
+
rankedBarLayout,
|
|
25
30
|
renderAreaSvg,
|
|
26
31
|
renderBarChartSvg,
|
|
32
|
+
renderBubbleSvg,
|
|
33
|
+
renderFunnelSvg,
|
|
27
34
|
renderHbarSvg,
|
|
28
35
|
renderHeatmapSvg,
|
|
36
|
+
renderParetoSvg,
|
|
37
|
+
renderRadarSvg,
|
|
38
|
+
renderTreemapSvg,
|
|
29
39
|
renderWaterfallSvg,
|
|
30
40
|
scatter,
|
|
31
41
|
sparkline,
|
|
32
42
|
stat,
|
|
43
|
+
type TreemapOptions,
|
|
33
44
|
type WaterfallOptions,
|
|
34
45
|
} from "@k2b/ui/charts/core";
|
|
35
46
|
import { z } from "zod";
|
|
@@ -38,19 +49,29 @@ import { type ChartFormat, type ChartInput, type ChartNumberFormat, chartDateTim
|
|
|
38
49
|
export * from "./chart-schema";
|
|
39
50
|
|
|
40
51
|
type RenderOptions = {
|
|
41
|
-
[K in ChartInput["kind"]]: { kind: K } & (K extends "
|
|
42
|
-
?
|
|
43
|
-
: K extends "
|
|
44
|
-
?
|
|
45
|
-
: K extends "
|
|
46
|
-
?
|
|
47
|
-
: K extends "
|
|
48
|
-
?
|
|
49
|
-
: K extends "
|
|
50
|
-
?
|
|
51
|
-
: K extends
|
|
52
|
-
?
|
|
53
|
-
:
|
|
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);
|
|
54
75
|
}[ChartInput["kind"]];
|
|
55
76
|
|
|
56
77
|
/**
|
|
@@ -65,7 +86,13 @@ const labels = /* @__PURE__ */ i18n.define({
|
|
|
65
86
|
messages: {
|
|
66
87
|
en: {
|
|
67
88
|
category: "Category",
|
|
89
|
+
size: "Size",
|
|
90
|
+
axis: "Axis",
|
|
68
91
|
series: "Series",
|
|
92
|
+
group: "Group",
|
|
93
|
+
previous: "Previous",
|
|
94
|
+
change: "Change",
|
|
95
|
+
cumulative: "Cumulative",
|
|
69
96
|
balance: "Balance",
|
|
70
97
|
before: "Balance before",
|
|
71
98
|
inflow: "In",
|
|
@@ -86,10 +113,19 @@ const labels = /* @__PURE__ */ i18n.define({
|
|
|
86
113
|
whiskerLow: "Whisker low",
|
|
87
114
|
whiskerHigh: "Whisker high",
|
|
88
115
|
delta: "Change",
|
|
116
|
+
startShare: "Share of start",
|
|
117
|
+
previousShare: "Share of previous step",
|
|
118
|
+
rank: "Rank",
|
|
89
119
|
},
|
|
90
120
|
de: {
|
|
91
121
|
category: "Kategorie",
|
|
122
|
+
size: "Größe",
|
|
123
|
+
axis: "Achse",
|
|
92
124
|
series: "Datenreihe",
|
|
125
|
+
group: "Bereich",
|
|
126
|
+
previous: "Vorher",
|
|
127
|
+
change: "Änderung",
|
|
128
|
+
cumulative: "Kumuliert",
|
|
93
129
|
balance: "Stand",
|
|
94
130
|
before: "Stand davor",
|
|
95
131
|
inflow: "Rein",
|
|
@@ -110,6 +146,9 @@ const labels = /* @__PURE__ */ i18n.define({
|
|
|
110
146
|
whiskerLow: "Unterer Whisker",
|
|
111
147
|
whiskerHigh: "Oberer Whisker",
|
|
112
148
|
delta: "Änderung",
|
|
149
|
+
startShare: "Anteil am Start",
|
|
150
|
+
previousShare: "Anteil am vorigen Schritt",
|
|
151
|
+
rank: "Rang",
|
|
113
152
|
},
|
|
114
153
|
},
|
|
115
154
|
});
|
|
@@ -279,6 +318,10 @@ const numberFormat = (format: ChartFormat | undefined) => (format && !("date" in
|
|
|
279
318
|
/** The values a chart writes with its value format, for whole-number currency. */
|
|
280
319
|
const formattedValues = (input: ChartInput): number[] => {
|
|
281
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);
|
|
282
325
|
case "waterfall":
|
|
283
326
|
return [input.start.value, ...input.steps.map((step) => step.value)];
|
|
284
327
|
case "area":
|
|
@@ -295,6 +338,10 @@ const formattedValues = (input: ChartInput): number[] => {
|
|
|
295
338
|
];
|
|
296
339
|
case "hbar":
|
|
297
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":
|
|
298
345
|
case "pie":
|
|
299
346
|
case "donut":
|
|
300
347
|
return input.data.map((item) => item.value);
|
|
@@ -318,7 +365,7 @@ const formattedValues = (input: ChartInput): number[] => {
|
|
|
318
365
|
}
|
|
319
366
|
};
|
|
320
367
|
const formats = (input: ChartInput, locale: string): Formats => {
|
|
321
|
-
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);
|
|
322
369
|
const y = chartNumberFormatters(yFormat, locale, formattedValues(input));
|
|
323
370
|
const xFormat = "xAxis" in input && input.kind !== "bar" ? input.xAxis?.format : undefined;
|
|
324
371
|
const x = chartNumberFormatters(numberFormat(xFormat), locale);
|
|
@@ -398,7 +445,7 @@ export function chartRenderOptions(
|
|
|
398
445
|
formats?: { x?: (v: number) => string; y?: (v: number) => string; value?: (v: number) => string };
|
|
399
446
|
} = {},
|
|
400
447
|
): RenderOptions {
|
|
401
|
-
if (
|
|
448
|
+
if ("locale" in input && input.locale) locale = input.locale;
|
|
402
449
|
const defaults = formats(input, locale);
|
|
403
450
|
// Function formats must be measured with the same text the renderer will print.
|
|
404
451
|
const format = {
|
|
@@ -411,6 +458,55 @@ export function chartRenderOptions(
|
|
|
411
458
|
const width = options.width ?? WIDTH;
|
|
412
459
|
const references = renderReferences(input);
|
|
413
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 };
|
|
414
510
|
case "hbar":
|
|
415
511
|
return {
|
|
416
512
|
kind: "hbar",
|
|
@@ -455,6 +551,9 @@ export function chartRenderOptions(
|
|
|
455
551
|
series: input.series,
|
|
456
552
|
layout: input.layout,
|
|
457
553
|
normalize: input.normalize,
|
|
554
|
+
share: input.share,
|
|
555
|
+
limit: input.limit,
|
|
556
|
+
tone: input.tone,
|
|
458
557
|
compare: input.compare,
|
|
459
558
|
xAxis: input.xAxis,
|
|
460
559
|
yAxis: axis(yAxis, options.formats?.y ?? format.tick),
|
|
@@ -614,6 +713,16 @@ const decode = (value: string) =>
|
|
|
614
713
|
export const drawChart = (options: RenderOptions): string => {
|
|
615
714
|
const common = { width: WIDTH, height: HEIGHT, inspect: true };
|
|
616
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 });
|
|
617
726
|
case "hbar":
|
|
618
727
|
return renderHbarSvg({ ...options, ...common, height: hbarLayout(options).height });
|
|
619
728
|
case "waterfall":
|
|
@@ -706,7 +815,7 @@ export function chartRows(
|
|
|
706
815
|
locale: string,
|
|
707
816
|
options: { formats?: { x?: (v: number) => string; y?: (v: number) => string; value?: (v: number) => string } } = {},
|
|
708
817
|
) {
|
|
709
|
-
if (
|
|
818
|
+
if ("locale" in input && input.locale) locale = input.locale;
|
|
710
819
|
const t = labels.resolve([locale]).t;
|
|
711
820
|
const format = formats(input, locale);
|
|
712
821
|
const valueFormat = input.tooltip?.format
|
|
@@ -738,8 +847,8 @@ export function chartRows(
|
|
|
738
847
|
const tooltip = input.kind === "waterfall" ? { title: datum.label, rows: [] as { label: string; value: string }[] } : undefined;
|
|
739
848
|
const cells: Record<string, string> = {};
|
|
740
849
|
const values: Record<string, string | number> = {};
|
|
741
|
-
const cell = (id: string, label: string, value: string | number, text: string) => {
|
|
742
|
-
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 });
|
|
743
852
|
cells[id] = text;
|
|
744
853
|
values[id] = value;
|
|
745
854
|
};
|
|
@@ -758,10 +867,35 @@ export function chartRows(
|
|
|
758
867
|
: input.data[datum.role === "outlier" ? (datum.seriesIndex ?? 0) : datum.index]?.label) ?? datum.label)
|
|
759
868
|
: datum.label;
|
|
760
869
|
if (label)
|
|
761
|
-
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
|
+
);
|
|
762
880
|
}
|
|
763
881
|
for (const field of datum.values) {
|
|
764
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
|
+
}
|
|
765
899
|
if (typeof value === "string" && input.kind === "area" && field.key === "x") {
|
|
766
900
|
cell("x", t.position, value, field.formatted ?? value);
|
|
767
901
|
continue;
|
|
@@ -770,6 +904,18 @@ export function chartRows(
|
|
|
770
904
|
cell(field.key, field.key.toUpperCase(), value, value);
|
|
771
905
|
continue;
|
|
772
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
|
+
}
|
|
773
919
|
if (input.kind === "hbar" || (input.kind === "bar" && input.series)) {
|
|
774
920
|
const seriesIndex = field.key.startsWith("series:") ? Number(field.key.slice(7)) : undefined;
|
|
775
921
|
const label =
|
|
@@ -784,7 +930,30 @@ export function chartRows(
|
|
|
784
930
|
: input.kind === "hbar"
|
|
785
931
|
? input.series[seriesIndex]!
|
|
786
932
|
: input.series![seriesIndex]!.label;
|
|
787
|
-
cell(field.key, label, value, field.formatted ?? String(value));
|
|
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
|
+
);
|
|
788
957
|
continue;
|
|
789
958
|
}
|
|
790
959
|
if (typeof value !== "number" || field.key === "total") continue;
|
|
@@ -831,11 +1000,13 @@ export function chartRows(
|
|
|
831
1000
|
cell(
|
|
832
1001
|
"value",
|
|
833
1002
|
input.tooltip?.label ??
|
|
834
|
-
(input.kind === "
|
|
835
|
-
? input.
|
|
836
|
-
: input.kind === "
|
|
837
|
-
? input.label
|
|
838
|
-
:
|
|
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) ??
|
|
839
1010
|
t.value,
|
|
840
1011
|
value,
|
|
841
1012
|
input.kind === "heatmap" && input.layout === "calendar"
|
|
@@ -860,7 +1031,37 @@ export function chartRows(
|
|
|
860
1031
|
rows.push(row);
|
|
861
1032
|
return row;
|
|
862
1033
|
};
|
|
863
|
-
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
|
+
};
|
|
864
1065
|
}
|
|
865
1066
|
|
|
866
1067
|
/** The data behind every mark of the chart as a table: the accessible alternative and what Copy data copies. */
|
|
@@ -873,6 +1074,10 @@ export function chartTable(input: ChartInput, locale: string): ChartTable {
|
|
|
873
1074
|
/** Input values count against the budget, including observations summarized into boxes or bins. */
|
|
874
1075
|
export const chartMarks = (input: ChartInput): number => {
|
|
875
1076
|
switch (input.kind) {
|
|
1077
|
+
case "pareto":
|
|
1078
|
+
return input.data.length * 3;
|
|
1079
|
+
case "radar":
|
|
1080
|
+
return input.axes.length * input.series.length;
|
|
876
1081
|
case "waterfall":
|
|
877
1082
|
return input.steps.length + 2;
|
|
878
1083
|
case "area":
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { highlight, text } from "@k2b/stdlib";
|
|
2
2
|
import { prepareChartSnapshot } from "@k2b/ui";
|
|
3
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,6 +458,10 @@ 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;
|
|
460
465
|
if (options.kind === "hbar") return hbarLayout(options).height;
|
|
461
466
|
if (options.kind === "heatmap" && options.layout === "calendar") return 280;
|
|
462
467
|
if (options.kind === "waterfall" || options.kind === "area") return 280;
|