@k2b/editor 0.6.4 → 0.7.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-locale.ts +19 -0
- package/src/chart-schema.ts +80 -11
- package/src/charts.ts +99 -17
- package/src/markdown/lib/chart-blocks.ts +19 -3
- package/src/markdown/notebook-charts.tsx +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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.50.0",
|
|
74
74
|
"@lezer/common": "1.5.3",
|
|
75
75
|
"@lezer/highlight": "1.2.4",
|
|
76
76
|
"@lezer/markdown": "1.7.2",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** BCP 47 locale strings must be accepted by every Intl formatter used during rendering. */
|
|
4
|
+
export const chartLocaleSchema = () =>
|
|
5
|
+
z
|
|
6
|
+
.string()
|
|
7
|
+
.min(1)
|
|
8
|
+
.refine((value) => {
|
|
9
|
+
try {
|
|
10
|
+
return Intl.getCanonicalLocales(value).length === 1;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}, "Use a valid BCP 47 locale, such as de-DE.");
|
|
15
|
+
|
|
16
|
+
/** The portable renderers which honour an explicit locale. Keep one policy for single charts and groups. */
|
|
17
|
+
const localeKinds = new Set(["sankey", "treemap", "pareto", "bubble", "radar"]);
|
|
18
|
+
export const chartAcceptsLocale = (chart: { kind?: string; layout?: string }): boolean =>
|
|
19
|
+
chart.kind === "heatmap" ? chart.layout === "calendar" : localeKinds.has(chart.kind ?? "");
|
package/src/chart-schema.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { chartLocaleSchema } from "./chart-locale";
|
|
2
|
+
|
|
3
|
+
export { chartAcceptsLocale, chartLocaleSchema } from "./chart-locale";
|
|
4
|
+
|
|
5
|
+
import { CALENDAR_MAX_DAYS, calendarDay } from "@k2b/ui/charts/core";
|
|
1
6
|
import { z } from "zod";
|
|
2
7
|
|
|
3
8
|
/** Optional caller policy. Rendering itself has no AI input budget. */
|
|
@@ -25,12 +30,13 @@ export const chartNumberFormatSchema = () =>
|
|
|
25
30
|
});
|
|
26
31
|
const dateFormatSchema = () => z.strictObject({ date: z.enum(["day", "minute", "second"]) });
|
|
27
32
|
export const chartFormatSchema = () => z.union([chartNumberFormatSchema(), dateFormatSchema()]);
|
|
33
|
+
|
|
28
34
|
export type ChartNumberFormat = z.output<ReturnType<typeof chartNumberFormatSchema>>;
|
|
29
35
|
export type ChartDateFormat = z.output<ReturnType<typeof dateFormatSchema>>;
|
|
30
36
|
export type ChartFormat = ChartNumberFormat | ChartDateFormat;
|
|
31
37
|
|
|
32
38
|
export const chartKindSchema = () =>
|
|
33
|
-
z.enum(["bar", "line", "scatter", "pie", "donut", "histogram", "gauge", "sparkline", "boxplot", "heatmap", "barGauge", "stat"]);
|
|
39
|
+
z.enum(["area", "bar", "line", "scatter", "pie", "donut", "histogram", "gauge", "sparkline", "boxplot", "heatmap", "barGauge", "stat"]);
|
|
34
40
|
|
|
35
41
|
export const chartParts = (limits?: ChartLimits) => {
|
|
36
42
|
const ChartLabel = limits ? z.string().trim().min(1).max(80) : z.union([z.string(), z.number().transform(String)]);
|
|
@@ -60,7 +66,13 @@ export const chartParts = (limits?: ChartLimits) => {
|
|
|
60
66
|
});
|
|
61
67
|
// As many lines as series a reader can tell apart.
|
|
62
68
|
const ChartReferences = list(
|
|
63
|
-
z.object({
|
|
69
|
+
z.object({
|
|
70
|
+
value: z.union([z.number(), ChartDate]),
|
|
71
|
+
axis: z.enum(["x", "y"]).optional(),
|
|
72
|
+
label: ChartLabel.optional(),
|
|
73
|
+
style: z.enum(["solid", "dashed", "dotted"]).optional(),
|
|
74
|
+
tone: z.enum(["neutral", "info", "success", "warning", "danger"]).optional(),
|
|
75
|
+
}),
|
|
64
76
|
0,
|
|
65
77
|
limits?.maxSeries,
|
|
66
78
|
`Use at most ${limits?.maxSeries} references.`,
|
|
@@ -142,6 +154,27 @@ export const chartKinds = (limits?: ChartLimits) => {
|
|
|
142
154
|
const ChartKindCommon = { ...ChartCommon, tooltip: ChartTooltip.optional() };
|
|
143
155
|
const numberFormat = chartNumberFormatSchema();
|
|
144
156
|
return z.discriminatedUnion("kind", [
|
|
157
|
+
z
|
|
158
|
+
.object({
|
|
159
|
+
kind: z.literal("area"),
|
|
160
|
+
...ChartKindCommon,
|
|
161
|
+
layout: z.literal("stacked"),
|
|
162
|
+
normalize: z.boolean().optional(),
|
|
163
|
+
x: list(z.union([z.number(), ChartLabel]), 1, limits?.maxValues),
|
|
164
|
+
series: list(z.object({ label: ChartLabel, data: list(z.number().min(0).nullable(), 0, limits?.maxValues) }), 1, limits?.maxSeries),
|
|
165
|
+
legend: z.boolean().optional(),
|
|
166
|
+
format: numberFormat.optional(),
|
|
167
|
+
})
|
|
168
|
+
.superRefine((spec, ctx) => {
|
|
169
|
+
spec.series.forEach((s, i) => {
|
|
170
|
+
if (s.data.length !== spec.x.length)
|
|
171
|
+
ctx.addIssue({ code: "custom", path: ["series", i, "data"], message: "Area series must match x length." });
|
|
172
|
+
});
|
|
173
|
+
spec.x.forEach((_, i) => {
|
|
174
|
+
const sum = spec.series.reduce((sum, s) => sum + (s.data[i] ?? 0), 0);
|
|
175
|
+
if (!Number.isFinite(sum)) ctx.addIssue({ code: "custom", path: ["x", i], message: "Area totals must be finite." });
|
|
176
|
+
});
|
|
177
|
+
}),
|
|
145
178
|
z.object({
|
|
146
179
|
kind: z.literal("bar"),
|
|
147
180
|
...ChartKindCommon,
|
|
@@ -220,15 +253,51 @@ export const chartKinds = (limits?: ChartLimits) => {
|
|
|
220
253
|
yAxis: ChartAxis.optional(),
|
|
221
254
|
references: ChartReferences.optional(),
|
|
222
255
|
}),
|
|
223
|
-
z.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
256
|
+
z.discriminatedUnion("layout", [
|
|
257
|
+
z
|
|
258
|
+
.object({
|
|
259
|
+
kind: z.literal("heatmap"),
|
|
260
|
+
...ChartKindCommon,
|
|
261
|
+
layout: z.literal("calendar"),
|
|
262
|
+
data: list(
|
|
263
|
+
z.tuple([
|
|
264
|
+
z.string().refine((day) => {
|
|
265
|
+
try {
|
|
266
|
+
calendarDay(day);
|
|
267
|
+
return true;
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}, "Invalid ISO calendar day."),
|
|
272
|
+
ChartNumber.nullable(),
|
|
273
|
+
]),
|
|
274
|
+
1,
|
|
275
|
+
limits?.maxMarks,
|
|
276
|
+
),
|
|
277
|
+
scale: z.enum(["sequential", "diverging"]),
|
|
278
|
+
label: ChartLabel.optional(),
|
|
279
|
+
format: numberFormat.optional(),
|
|
280
|
+
locale: chartLocaleSchema().optional(),
|
|
281
|
+
})
|
|
282
|
+
.superRefine((spec, ctx) => {
|
|
283
|
+
if (ctx.issues.length) return;
|
|
284
|
+
const times = spec.data.map(([day]) => calendarDay(day));
|
|
285
|
+
if (times.length && (Math.max(...times) - Math.min(...times)) / 86400000 >= CALENDAR_MAX_DAYS)
|
|
286
|
+
ctx.addIssue({ code: "custom", message: "Calendar span must not exceed three years." });
|
|
287
|
+
if (new Set(spec.data.map(([day]) => day)).size !== spec.data.length)
|
|
288
|
+
ctx.addIssue({ code: "custom", message: "Calendar dates must be unique." });
|
|
289
|
+
}),
|
|
290
|
+
z.object({
|
|
291
|
+
kind: z.literal("heatmap"),
|
|
292
|
+
...ChartKindCommon,
|
|
293
|
+
layout: z.undefined().optional(),
|
|
294
|
+
data: list(ChartCell, 1, limits?.maxMarks),
|
|
295
|
+
format: numberFormat.optional(),
|
|
296
|
+
showValues: z.boolean().optional(),
|
|
297
|
+
min: z.number().optional(),
|
|
298
|
+
max: z.number().optional(),
|
|
299
|
+
}),
|
|
300
|
+
]),
|
|
232
301
|
z.object({
|
|
233
302
|
kind: z.literal("barGauge"),
|
|
234
303
|
...ChartKindCommon,
|
package/src/charts.ts
CHANGED
|
@@ -1,35 +1,47 @@
|
|
|
1
1
|
// Named renderers, not the `charts` namespace, so bundles leave out the map and state timeline renderers.
|
|
2
|
+
import { i18n } from "@k2b/stdlib";
|
|
2
3
|
import {
|
|
4
|
+
type AreaOptions,
|
|
3
5
|
autoBin,
|
|
6
|
+
type BarChartOptions,
|
|
7
|
+
barChartLayout,
|
|
4
8
|
barGauge,
|
|
5
9
|
boxplot,
|
|
6
10
|
type ChartDatum,
|
|
11
|
+
calendarWords,
|
|
7
12
|
type charts,
|
|
8
13
|
computeDomain,
|
|
9
14
|
donut,
|
|
10
15
|
extendDomainToNice,
|
|
11
16
|
gauge,
|
|
12
|
-
|
|
17
|
+
type HeatmapChartOptions,
|
|
13
18
|
histogram,
|
|
14
|
-
i18n,
|
|
15
19
|
line,
|
|
16
20
|
niceLogTicks,
|
|
17
21
|
niceStep,
|
|
18
22
|
pie,
|
|
23
|
+
renderAreaSvg,
|
|
24
|
+
renderBarChartSvg,
|
|
25
|
+
renderHeatmapSvg,
|
|
19
26
|
scatter,
|
|
20
27
|
sparkline,
|
|
21
28
|
stat,
|
|
22
|
-
} from "@k2b/
|
|
23
|
-
import { type BarChartOptions, barChartLayout, renderBarChartSvg } from "@k2b/ui/charts";
|
|
29
|
+
} from "@k2b/ui/charts/core";
|
|
24
30
|
import { z } from "zod";
|
|
25
31
|
import { type ChartFormat, type ChartInput, type ChartNumberFormat, chartDateTime, chartKinds } from "./chart-schema";
|
|
26
32
|
|
|
27
33
|
export * from "./chart-schema";
|
|
28
34
|
|
|
29
35
|
type RenderOptions = {
|
|
30
|
-
[K in ChartInput["kind"]]: { kind: K } & (K extends "
|
|
31
|
-
?
|
|
32
|
-
:
|
|
36
|
+
[K in ChartInput["kind"]]: { kind: K } & (K extends "area"
|
|
37
|
+
? AreaOptions
|
|
38
|
+
: K extends "heatmap"
|
|
39
|
+
? HeatmapChartOptions
|
|
40
|
+
: K extends "bar"
|
|
41
|
+
? BarChartOptions
|
|
42
|
+
: K extends keyof typeof charts
|
|
43
|
+
? Omit<Parameters<(typeof charts)[K]>[0], "width" | "height" | "inspect">
|
|
44
|
+
: never);
|
|
33
45
|
}[ChartInput["kind"]];
|
|
34
46
|
|
|
35
47
|
/**
|
|
@@ -47,6 +59,7 @@ const labels = /* @__PURE__ */ i18n.define({
|
|
|
47
59
|
series: "Series",
|
|
48
60
|
value: "Value",
|
|
49
61
|
share: "Share",
|
|
62
|
+
position: "Position",
|
|
50
63
|
from: "From",
|
|
51
64
|
to: "To",
|
|
52
65
|
count: "Count",
|
|
@@ -64,6 +77,7 @@ const labels = /* @__PURE__ */ i18n.define({
|
|
|
64
77
|
series: "Datenreihe",
|
|
65
78
|
value: "Wert",
|
|
66
79
|
share: "Anteil",
|
|
80
|
+
position: "Position",
|
|
67
81
|
from: "Von",
|
|
68
82
|
to: "Bis",
|
|
69
83
|
count: "Anzahl",
|
|
@@ -232,6 +246,8 @@ const numberFormat = (format: ChartFormat | undefined) => (format && !("date" in
|
|
|
232
246
|
/** The values a chart writes with its value format, for whole-number currency. */
|
|
233
247
|
const formattedValues = (input: ChartInput): number[] => {
|
|
234
248
|
switch (input.kind) {
|
|
249
|
+
case "area":
|
|
250
|
+
return input.series.flatMap((s) => s.data.filter((v): v is number => v !== null));
|
|
235
251
|
case "line":
|
|
236
252
|
case "scatter":
|
|
237
253
|
return [...input.series.flatMap((series) => series.data.map((point) => point.y)), ...chartReferenceValues(input, "y")];
|
|
@@ -239,8 +255,11 @@ const formattedValues = (input: ChartInput): number[] => {
|
|
|
239
255
|
return [...input.data.map((item) => item.value), ...chartReferenceValues(input, "y")];
|
|
240
256
|
case "pie":
|
|
241
257
|
case "donut":
|
|
242
|
-
case "heatmap":
|
|
243
258
|
return input.data.map((item) => item.value);
|
|
259
|
+
case "heatmap":
|
|
260
|
+
return input.layout === "calendar"
|
|
261
|
+
? input.data.flatMap((item) => (item[1] === null ? [] : [item[1]]))
|
|
262
|
+
: input.data.map((item) => item.value);
|
|
244
263
|
case "barGauge":
|
|
245
264
|
return [...input.data.flatMap((item) => [item.value, item.min, item.max]), input.min, input.max].filter(
|
|
246
265
|
(value) => value !== undefined,
|
|
@@ -306,6 +325,7 @@ export function chartLegend(
|
|
|
306
325
|
): { label: string; color: number; style?: "solid" | "dashed" | "dotted"; reference?: boolean }[] {
|
|
307
326
|
if (input.kind === "bar")
|
|
308
327
|
return input.colorByBar && input.legend ? input.data.map((entry, index) => ({ label: entry.label, color: index % 8 })) : [];
|
|
328
|
+
if (input.kind === "area") return input.legend === false ? [] : input.series.map((s, i) => ({ label: s.label, color: i % 8 }));
|
|
309
329
|
if (input.kind !== "line" && input.kind !== "scatter") return [];
|
|
310
330
|
if (!(input.legend ?? input.series.length > 1)) return [];
|
|
311
331
|
const t = labels.resolve([locale]).t;
|
|
@@ -334,6 +354,7 @@ export function chartRenderOptions(
|
|
|
334
354
|
formats?: { x?: (v: number) => string; y?: (v: number) => string; value?: (v: number) => string };
|
|
335
355
|
} = {},
|
|
336
356
|
): RenderOptions {
|
|
357
|
+
if (input.kind === "heatmap" && input.layout === "calendar") locale = input.locale ?? locale;
|
|
337
358
|
const defaults = formats(input, locale);
|
|
338
359
|
// Function formats must be measured with the same text the renderer will print.
|
|
339
360
|
const format = {
|
|
@@ -346,6 +367,18 @@ export function chartRenderOptions(
|
|
|
346
367
|
const width = options.width ?? WIDTH;
|
|
347
368
|
const references = renderReferences(input);
|
|
348
369
|
switch (input.kind) {
|
|
370
|
+
case "area":
|
|
371
|
+
return {
|
|
372
|
+
kind: "area",
|
|
373
|
+
layout: input.layout,
|
|
374
|
+
normalize: input.normalize,
|
|
375
|
+
x: input.x,
|
|
376
|
+
series: input.series,
|
|
377
|
+
displayWidth: options.width,
|
|
378
|
+
locale,
|
|
379
|
+
format: format.value,
|
|
380
|
+
legend: options.legend === "outside" ? false : input.legend,
|
|
381
|
+
};
|
|
349
382
|
case "bar":
|
|
350
383
|
case "boxplot": {
|
|
351
384
|
const yAxis = referencedAxis(input, "y");
|
|
@@ -458,6 +491,17 @@ export function chartRenderOptions(
|
|
|
458
491
|
thresholds: input.thresholds,
|
|
459
492
|
};
|
|
460
493
|
case "heatmap":
|
|
494
|
+
if (input.layout === "calendar")
|
|
495
|
+
return {
|
|
496
|
+
kind: "heatmap",
|
|
497
|
+
layout: "calendar",
|
|
498
|
+
data: input.data,
|
|
499
|
+
scale: input.scale,
|
|
500
|
+
label: input.label,
|
|
501
|
+
locale: input.locale ?? locale,
|
|
502
|
+
format: format.value,
|
|
503
|
+
displayWidth: options.width,
|
|
504
|
+
};
|
|
461
505
|
return {
|
|
462
506
|
kind: "heatmap",
|
|
463
507
|
data: input.data,
|
|
@@ -501,6 +545,8 @@ const decode = (value: string) =>
|
|
|
501
545
|
export const drawChart = (options: RenderOptions): string => {
|
|
502
546
|
const common = { width: WIDTH, height: HEIGHT, inspect: true };
|
|
503
547
|
switch (options.kind) {
|
|
548
|
+
case "area":
|
|
549
|
+
return renderAreaSvg({ ...options, ...common });
|
|
504
550
|
case "bar":
|
|
505
551
|
return renderBarChartSvg({ ...options, ...common, height: barChartLayout(options).height });
|
|
506
552
|
case "line":
|
|
@@ -522,7 +568,7 @@ export const drawChart = (options: RenderOptions): string => {
|
|
|
522
568
|
case "stat":
|
|
523
569
|
return stat({ ...options, ...common });
|
|
524
570
|
case "heatmap":
|
|
525
|
-
return
|
|
571
|
+
return renderHeatmapSvg({ ...options, ...common });
|
|
526
572
|
case "sparkline":
|
|
527
573
|
return sparkline({ ...options, ...common });
|
|
528
574
|
}
|
|
@@ -552,7 +598,16 @@ export type ChartRow = { key: string; cells: Record<string, string>; values: Rec
|
|
|
552
598
|
export type ChartTable = { columns: ChartColumn[]; rows: ChartRow[] };
|
|
553
599
|
|
|
554
600
|
/** The first text column titles the tooltip; the other columns become rows, matching the table in every host. */
|
|
555
|
-
export function chartTooltip(
|
|
601
|
+
export function chartTooltip(
|
|
602
|
+
columns: ChartColumn[],
|
|
603
|
+
row: ChartRow,
|
|
604
|
+
kind?: ChartInput["kind"],
|
|
605
|
+
): { title?: string; rows: { label: string; value: string }[] } {
|
|
606
|
+
if (kind === "area")
|
|
607
|
+
return {
|
|
608
|
+
title: row.cells.x,
|
|
609
|
+
rows: [{ label: row.cells.series ?? "", value: `${row.cells.percent ? `${row.cells.percent} · ` : ""}${row.cells.value ?? "—"}` }],
|
|
610
|
+
};
|
|
556
611
|
const [first, ...rest] = columns;
|
|
557
612
|
const named = first && !first.numeric;
|
|
558
613
|
return {
|
|
@@ -572,6 +627,7 @@ export function chartRows(
|
|
|
572
627
|
locale: string,
|
|
573
628
|
options: { formats?: { x?: (v: number) => string; y?: (v: number) => string; value?: (v: number) => string } } = {},
|
|
574
629
|
) {
|
|
630
|
+
if (input.kind === "heatmap" && input.layout === "calendar") locale = input.locale ?? locale;
|
|
575
631
|
const t = labels.resolve([locale]).t;
|
|
576
632
|
const format = formats(input, locale);
|
|
577
633
|
const valueFormat = input.tooltip?.format
|
|
@@ -580,7 +636,11 @@ export function chartRows(
|
|
|
580
636
|
const xFormat = options.formats?.x ?? format.x.value;
|
|
581
637
|
const series = chartSeries(input);
|
|
582
638
|
const countFormat = (input.kind === "histogram" ? options.formats?.y : undefined) ?? chartNumberFormatters(undefined, locale).value;
|
|
583
|
-
const percent = new Intl.NumberFormat(locale, {
|
|
639
|
+
const percent = new Intl.NumberFormat(locale, {
|
|
640
|
+
style: "percent",
|
|
641
|
+
minimumFractionDigits: input.kind === "area" ? 1 : 0,
|
|
642
|
+
maximumFractionDigits: 1,
|
|
643
|
+
});
|
|
584
644
|
const unit = "unit" in input && input.unit ? ` ${input.unit}` : "";
|
|
585
645
|
const columns = new Map<string, ChartColumn>();
|
|
586
646
|
const rows: ChartRow[] = [];
|
|
@@ -593,33 +653,45 @@ export function chartRows(
|
|
|
593
653
|
cells[id] = text;
|
|
594
654
|
values[id] = value;
|
|
595
655
|
};
|
|
656
|
+
if (input.kind === "area") cell("series", t.series, datum.label ?? "", datum.label ?? "");
|
|
596
657
|
if (input.kind === "line" || input.kind === "scatter") {
|
|
597
658
|
if (named || series.length > 1) {
|
|
598
659
|
const name = series[datum.seriesIndex ?? 0]?.label ?? `${t.series} ${(datum.seriesIndex ?? 0) + 1}`;
|
|
599
660
|
cell("series", t.series, name, name);
|
|
600
661
|
}
|
|
601
|
-
} else {
|
|
662
|
+
} else if (input.kind !== "area") {
|
|
602
663
|
// Bars may hide some axis labels; the table names every category.
|
|
603
664
|
const label =
|
|
604
665
|
input.kind === "bar" || input.kind === "pie" || input.kind === "donut" || input.kind === "boxplot" || input.kind === "barGauge"
|
|
605
666
|
? input.data[datum.role === "outlier" ? (datum.seriesIndex ?? 0) : datum.index]?.label
|
|
606
667
|
: datum.label;
|
|
607
|
-
if (label)
|
|
668
|
+
if (label)
|
|
669
|
+
cell("label", input.kind === "heatmap" && input.layout === "calendar" ? calendarWords(locale).day : t.category, label, label);
|
|
608
670
|
}
|
|
609
671
|
for (const field of datum.values) {
|
|
610
672
|
const value = field.value;
|
|
673
|
+
if (typeof value === "string" && input.kind === "area" && field.key === "x") {
|
|
674
|
+
cell("x", t.position, value, field.formatted ?? value);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
611
677
|
if (typeof value === "string" && input.kind === "heatmap" && (field.key === "x" || field.key === "y")) {
|
|
612
678
|
cell(field.key, field.key.toUpperCase(), value, value);
|
|
613
679
|
continue;
|
|
614
680
|
}
|
|
615
681
|
if (typeof value !== "number" || field.key === "total") continue;
|
|
616
682
|
if (field.key === "x") {
|
|
617
|
-
const header =
|
|
683
|
+
const header =
|
|
684
|
+
(input.kind === "line" || input.kind === "scatter" ? input.xAxis?.label : undefined) ??
|
|
685
|
+
(input.kind === "area" ? t.position : "X");
|
|
618
686
|
cell(
|
|
619
687
|
"x",
|
|
620
688
|
input.kind === "sparkline" || input.kind === "stat" ? "#" : header,
|
|
621
689
|
value,
|
|
622
|
-
input.kind === "sparkline" || input.kind === "stat"
|
|
690
|
+
input.kind === "sparkline" || input.kind === "stat"
|
|
691
|
+
? String(value + 1)
|
|
692
|
+
: input.kind === "area"
|
|
693
|
+
? (field.formatted ?? String(value))
|
|
694
|
+
: xFormat(value),
|
|
623
695
|
);
|
|
624
696
|
} else if (field.key === "y") {
|
|
625
697
|
const header =
|
|
@@ -630,9 +702,17 @@ export function chartRows(
|
|
|
630
702
|
} else if (field.key === "value")
|
|
631
703
|
cell(
|
|
632
704
|
"value",
|
|
633
|
-
input.tooltip?.label ??
|
|
705
|
+
input.tooltip?.label ??
|
|
706
|
+
(input.kind === "bar"
|
|
707
|
+
? input.yAxis?.label
|
|
708
|
+
: input.kind === "heatmap" && input.layout === "calendar"
|
|
709
|
+
? input.label
|
|
710
|
+
: undefined) ??
|
|
711
|
+
t.value,
|
|
634
712
|
value,
|
|
635
|
-
|
|
713
|
+
input.kind === "heatmap" && input.layout === "calendar"
|
|
714
|
+
? (field.formatted ?? valueFormat(value))
|
|
715
|
+
: `${valueFormat(value)}${unit}`,
|
|
636
716
|
);
|
|
637
717
|
else if (field.key === "delta") cell("delta", t.delta, value, `${value > 0 ? "+" : ""}${valueFormat(value)}`);
|
|
638
718
|
else if (field.key === "q1" || field.key === "q2" || field.key === "q3" || field.key === "whiskerLow" || field.key === "whiskerHigh")
|
|
@@ -665,6 +745,8 @@ export function chartTable(input: ChartInput, locale: string): ChartTable {
|
|
|
665
745
|
/** Input values count against the budget, including observations summarized into boxes or bins. */
|
|
666
746
|
export const chartMarks = (input: ChartInput): number => {
|
|
667
747
|
switch (input.kind) {
|
|
748
|
+
case "area":
|
|
749
|
+
return input.series.reduce((sum, s) => sum + s.data.length, 0);
|
|
668
750
|
case "line":
|
|
669
751
|
case "scatter":
|
|
670
752
|
return input.series.reduce((sum, series) => sum + series.data.length, 0);
|
|
@@ -457,6 +457,8 @@ export const chartBlockFrame = (body: string[], options: Pick<MarkdownChartOptio
|
|
|
457
457
|
*/
|
|
458
458
|
export const notebookChartHeight = (input: ChartInput, locale: string): number | undefined => {
|
|
459
459
|
const options = chartRenderOptions(input, locale, { legend: "outside" });
|
|
460
|
+
if (options.kind === "heatmap" && options.layout === "calendar") return 280;
|
|
461
|
+
if (options.kind === "area") return 280;
|
|
460
462
|
if (options.kind !== "bar") return undefined;
|
|
461
463
|
const layout = barChartLayout(options);
|
|
462
464
|
return layout.layout === "rows" ? layout.height : undefined;
|
|
@@ -487,17 +489,31 @@ export const renderNotebookChartFigure = (input: ChartInput, locale: string, opt
|
|
|
487
489
|
key: ({ datum }) => chartMarkKey(datum),
|
|
488
490
|
tooltip: ({ datum }) => {
|
|
489
491
|
const row = rows.add(datum);
|
|
490
|
-
return chartTooltip(rows.columns(), row);
|
|
492
|
+
return chartTooltip(rows.columns(), row, input.kind);
|
|
491
493
|
},
|
|
492
494
|
reference: ({ datum }) => chartReferenceMark(input, datum),
|
|
493
495
|
});
|
|
494
496
|
if (!snapshot.marks.length) return null;
|
|
495
497
|
const legend = chartLegend(input, locale);
|
|
496
498
|
const t = chartBlockMessages.resolve([locale]).t;
|
|
499
|
+
const valueField = input.kind === "area" ? (input.normalize ? "percent" : "value") : "y";
|
|
500
|
+
let lastX = -Infinity;
|
|
501
|
+
for (const mark of snapshot.marks) lastX = Math.max(lastX, mark.datum.anchor[0]);
|
|
502
|
+
const legendValue = (index: number) => {
|
|
503
|
+
const field = snapshot.marks
|
|
504
|
+
.find((mark) => mark.datum.seriesIndex === index && mark.datum.anchor[0] === lastX)
|
|
505
|
+
?.datum.values.find((field) => field.key === valueField);
|
|
506
|
+
return field
|
|
507
|
+
? (field.formatted ??
|
|
508
|
+
(typeof field.value === "number"
|
|
509
|
+
? new Intl.NumberFormat(locale, { maximumFractionDigits: 6 }).format(field.value)
|
|
510
|
+
: field.value))
|
|
511
|
+
: "—";
|
|
512
|
+
};
|
|
497
513
|
const legendHtml = legend.length
|
|
498
|
-
? `<ul class="k2b-chart-legend" aria-label="${escape(t.chartLegend)}">${legend.map((entry) => `<li><span class="k2b-chart-legend__swatch"${entry.style && entry.style !== "solid" ? ` data-style="${entry.style}"` : ""}${entry.reference ? ' data-reference="true"' : ""} style="--k2b-chart-legend-color:var(--stdlib-chart-c${(entry.color % 8) + 1})" aria-hidden="true"></span>${escape(entry.label)}</li>`).join("")}</ul>`
|
|
514
|
+
? `<ul class="k2b-chart-legend" aria-label="${escape(t.chartLegend)}">${legend.map((entry, index) => `<li><span class="k2b-chart-legend__swatch"${entry.style && entry.style !== "solid" ? ` data-style="${entry.style}"` : ""}${entry.reference ? ' data-reference="true"' : ""} style="--k2b-chart-legend-color:var(--stdlib-chart-c${(entry.color % 8) + 1})" aria-hidden="true"></span>${escape(entry.label)}${input.kind === "area" ? `<b data-chart-legend-value data-series-index="${index}" data-field="${valueField}" data-default="${escape(legendValue(index))}">${escape(legendValue(index))}</b>` : ""}</li>`).join("")}</ul>`
|
|
499
515
|
: "";
|
|
500
|
-
const viewport = `<div class="k2b-chart" data-chart-kind="${snapshot.kind}"><div class="k2b-chart__svg"${snapshot.stretch ? ' data-stretch="true"' : ""} style="--k2b-chart-width:${snapshot.width}px;--k2b-chart-height:${snapshot.height}px">${snapshot.svg}</div></div>${legendHtml}`;
|
|
516
|
+
const viewport = `<div class="k2b-chart" data-chart-kind="${snapshot.kind}"><div class="k2b-chart__svg"${snapshot.stretch ? ' data-stretch="true"' : ""} style="${snapshot.minWidth ? "min-width:0;" : ""}--k2b-chart-width:${snapshot.width}px;--k2b-chart-height:${snapshot.height}px">${snapshot.svg}</div></div>${legendHtml}`;
|
|
501
517
|
const height = notebookChartHeight(input, locale);
|
|
502
518
|
const figure = notebookChartFrameHtml(height ? { ...input, height } : input, viewport, {
|
|
503
519
|
legend: legend.length > 0,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { i18n } from "@k2b/stdlib";
|
|
2
2
|
import { CopyButton, IconButton, LocaleProvider, prepareChartSnapshot, toast } from "@k2b/ui";
|
|
3
3
|
import { type ChartTooltip, inspectChart } from "@k2b/ui/chart-inspection";
|
|
4
|
+
import { calendarInspectionNavigation, mountCalendarScroll } from "@k2b/ui/charts";
|
|
4
5
|
import { createSignal, onCleanup, Show } from "solid-js";
|
|
5
6
|
import { render } from "solid-js/web";
|
|
6
7
|
import { type ChartInput, type ChartTable, chartMarkKey, chartReferenceMark, chartRenderOptions, chartRows, chartTooltip } from "../charts";
|
|
@@ -43,7 +44,7 @@ const draw = (input: ChartInput, locale: string): Drawn => {
|
|
|
43
44
|
key: ({ datum }) => chartMarkKey(datum),
|
|
44
45
|
tooltip: ({ datum }) => {
|
|
45
46
|
const row = rows.add(datum);
|
|
46
|
-
return chartTooltip(rows.columns(), row);
|
|
47
|
+
return chartTooltip(rows.columns(), row, input.kind);
|
|
47
48
|
},
|
|
48
49
|
reference: ({ datum }) => chartReferenceMark(input, datum),
|
|
49
50
|
});
|
|
@@ -121,8 +122,12 @@ export function enhanceNotebookCharts(root: HTMLElement, locale: string, options
|
|
|
121
122
|
chart.setAttribute("aria-label", figure.querySelector(".notebook-book-chart__title")?.textContent ?? "");
|
|
122
123
|
chart.setAttribute("aria-description", t.interactiveChart);
|
|
123
124
|
chart.dataset.interactive = "true";
|
|
125
|
+
if (input.kind === "heatmap" && input.layout === "calendar") cleanups.push(mountCalendarScroll(chart));
|
|
124
126
|
const inspection = inspectChart(chart, {
|
|
125
127
|
kind: input.kind,
|
|
128
|
+
nav: input.kind === "heatmap" && input.layout === "calendar" ? calendarInspectionNavigation : undefined,
|
|
129
|
+
locale,
|
|
130
|
+
legend: figure,
|
|
126
131
|
format: ({ datum }) => lazy().tooltips.get(chartMarkKey(datum)) ?? { rows: [] },
|
|
127
132
|
});
|
|
128
133
|
const dispose = render(
|