@k2b/editor 0.6.5 → 0.8.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 +182 -29
- package/src/charts.ts +240 -24
- package/src/markdown/lib/chart-blocks.ts +21 -4
- 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.8.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.51.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,7 +1,10 @@
|
|
|
1
|
+
import type { ChartInput, ChartLimits } from "@k2b/ui/charts";
|
|
2
|
+
import { CALENDAR_MAX_DAYS, calendarDay } from "@k2b/ui/charts/core";
|
|
1
3
|
import { z } from "zod";
|
|
4
|
+
import { chartLocaleSchema } from "./chart-locale";
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
export
|
|
6
|
+
export type { ChartDateFormat, ChartDescription, ChartFormat, ChartInput, ChartLimits, ChartNumberFormat } from "@k2b/ui/charts";
|
|
7
|
+
export { chartAcceptsLocale, chartLocaleSchema } from "./chart-locale";
|
|
5
8
|
|
|
6
9
|
// Every chart schema is built by a function whose call is marked pure, so a bundle that imports neither chart contract
|
|
7
10
|
// leaves them out. App frames include the app contract through `cloud.chart()`.
|
|
@@ -25,12 +28,25 @@ export const chartNumberFormatSchema = () =>
|
|
|
25
28
|
});
|
|
26
29
|
const dateFormatSchema = () => z.strictObject({ date: z.enum(["day", "minute", "second"]) });
|
|
27
30
|
export const chartFormatSchema = () => z.union([chartNumberFormatSchema(), dateFormatSchema()]);
|
|
28
|
-
export type ChartNumberFormat = z.output<ReturnType<typeof chartNumberFormatSchema>>;
|
|
29
|
-
export type ChartDateFormat = z.output<ReturnType<typeof dateFormatSchema>>;
|
|
30
|
-
export type ChartFormat = ChartNumberFormat | ChartDateFormat;
|
|
31
31
|
|
|
32
32
|
export const chartKindSchema = () =>
|
|
33
|
-
z.enum([
|
|
33
|
+
z.enum([
|
|
34
|
+
"waterfall",
|
|
35
|
+
"area",
|
|
36
|
+
"bar",
|
|
37
|
+
"hbar",
|
|
38
|
+
"line",
|
|
39
|
+
"scatter",
|
|
40
|
+
"pie",
|
|
41
|
+
"donut",
|
|
42
|
+
"histogram",
|
|
43
|
+
"gauge",
|
|
44
|
+
"sparkline",
|
|
45
|
+
"boxplot",
|
|
46
|
+
"heatmap",
|
|
47
|
+
"barGauge",
|
|
48
|
+
"stat",
|
|
49
|
+
]);
|
|
34
50
|
|
|
35
51
|
export const chartParts = (limits?: ChartLimits) => {
|
|
36
52
|
const ChartLabel = limits ? z.string().trim().min(1).max(80) : z.union([z.string(), z.number().transform(String)]);
|
|
@@ -60,7 +76,13 @@ export const chartParts = (limits?: ChartLimits) => {
|
|
|
60
76
|
});
|
|
61
77
|
// As many lines as series a reader can tell apart.
|
|
62
78
|
const ChartReferences = list(
|
|
63
|
-
z.object({
|
|
79
|
+
z.object({
|
|
80
|
+
value: z.union([z.number(), ChartDate]),
|
|
81
|
+
axis: z.enum(["x", "y"]).optional(),
|
|
82
|
+
label: ChartLabel.optional(),
|
|
83
|
+
style: z.enum(["solid", "dashed", "dotted"]).optional(),
|
|
84
|
+
tone: z.enum(["neutral", "info", "success", "warning", "danger"]).optional(),
|
|
85
|
+
}),
|
|
64
86
|
0,
|
|
65
87
|
limits?.maxSeries,
|
|
66
88
|
`Use at most ${limits?.maxSeries} references.`,
|
|
@@ -142,17 +164,107 @@ export const chartKinds = (limits?: ChartLimits) => {
|
|
|
142
164
|
const ChartKindCommon = { ...ChartCommon, tooltip: ChartTooltip.optional() };
|
|
143
165
|
const numberFormat = chartNumberFormatSchema();
|
|
144
166
|
return z.discriminatedUnion("kind", [
|
|
145
|
-
z
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
z
|
|
168
|
+
.object({
|
|
169
|
+
kind: z.literal("waterfall"),
|
|
170
|
+
...ChartKindCommon,
|
|
171
|
+
start: z.object({ label: ChartLabel, value: z.number() }),
|
|
172
|
+
steps: list(z.object({ label: ChartLabel, value: z.number() }), 0, limits?.maxValues),
|
|
173
|
+
end: z.strictObject({ label: ChartLabel }),
|
|
174
|
+
format: numberFormat.optional(),
|
|
175
|
+
})
|
|
176
|
+
.superRefine((spec, ctx) => {
|
|
177
|
+
let balance = spec.start.value;
|
|
178
|
+
let lo = Math.min(0, balance),
|
|
179
|
+
hi = Math.max(0, balance);
|
|
180
|
+
for (const step of spec.steps) {
|
|
181
|
+
balance += step.value;
|
|
182
|
+
lo = Math.min(lo, balance);
|
|
183
|
+
hi = Math.max(hi, balance);
|
|
184
|
+
}
|
|
185
|
+
if (!Number.isFinite(balance) || !Number.isFinite(hi - lo))
|
|
186
|
+
ctx.addIssue({ code: "custom", message: "Waterfall balances must form a finite range." });
|
|
187
|
+
}),
|
|
188
|
+
z
|
|
189
|
+
.object({
|
|
190
|
+
kind: z.literal("area"),
|
|
191
|
+
...ChartKindCommon,
|
|
192
|
+
layout: z.literal("stacked"),
|
|
193
|
+
normalize: z.boolean().optional(),
|
|
194
|
+
x: list(z.union([z.number(), ChartLabel]), 1, limits?.maxValues),
|
|
195
|
+
series: list(z.object({ label: ChartLabel, data: list(z.number().min(0).nullable(), 0, limits?.maxValues) }), 1, limits?.maxSeries),
|
|
196
|
+
legend: z.boolean().optional(),
|
|
197
|
+
format: numberFormat.optional(),
|
|
198
|
+
})
|
|
199
|
+
.superRefine((spec, ctx) => {
|
|
200
|
+
spec.series.forEach((s, i) => {
|
|
201
|
+
if (s.data.length !== spec.x.length)
|
|
202
|
+
ctx.addIssue({ code: "custom", path: ["series", i, "data"], message: "Area series must match x length." });
|
|
203
|
+
});
|
|
204
|
+
spec.x.forEach((_, i) => {
|
|
205
|
+
const sum = spec.series.reduce((sum, s) => sum + (s.data[i] ?? 0), 0);
|
|
206
|
+
if (!Number.isFinite(sum)) ctx.addIssue({ code: "custom", path: ["x", i], message: "Area totals must be finite." });
|
|
207
|
+
});
|
|
208
|
+
}),
|
|
209
|
+
z
|
|
210
|
+
.object({
|
|
211
|
+
kind: z.literal("bar"),
|
|
212
|
+
...ChartKindCommon,
|
|
213
|
+
data: list(ChartCategory, 0, limits?.maxValues).default([]),
|
|
214
|
+
x: list(ChartLabel, 0, limits?.maxValues).optional(),
|
|
215
|
+
series: list(
|
|
216
|
+
z.object({ label: ChartLabel, data: list(ChartNumber.nullable(), 0, limits?.maxValues) }),
|
|
217
|
+
1,
|
|
218
|
+
limits?.maxSeries,
|
|
219
|
+
).optional(),
|
|
220
|
+
layout: z.enum(["columns", "rows", "grouped", "stacked"]).optional(),
|
|
221
|
+
normalize: z.boolean().optional(),
|
|
222
|
+
compare: z.object({ label: ChartLabel, data: list(ChartNumber.nullable(), 0, limits?.maxValues) }).optional(),
|
|
223
|
+
xAxis: ChartAxis.pick({ label: true }).extend({ ordered: z.boolean().optional() }).strict().optional(),
|
|
224
|
+
yAxis: ChartAxis.optional(),
|
|
225
|
+
colorByBar: z.boolean().optional(),
|
|
226
|
+
showValues: z.boolean().optional(),
|
|
227
|
+
legend: z.boolean().optional(),
|
|
228
|
+
references: ChartReferences.optional(),
|
|
229
|
+
})
|
|
230
|
+
.superRefine((bar, ctx) => {
|
|
231
|
+
const issue = (message: string) => ctx.addIssue({ code: "custom", message });
|
|
232
|
+
if (limits && !bar.series && !bar.data.length)
|
|
233
|
+
ctx.addIssue({ code: "custom", path: ["data"], message: "A bar chart needs data or aligned series." });
|
|
234
|
+
if (bar.series) {
|
|
235
|
+
if (!bar.x || bar.series.some((s) => s.data.length !== bar.x?.length)) issue("Bar series must have the same length as x.");
|
|
236
|
+
if (bar.data.length) issue("Use data or aligned series, not both.");
|
|
237
|
+
if (bar.layout === "rows" || bar.layout === "columns") issue("Series bars use grouped or stacked layout.");
|
|
238
|
+
} else if (bar.x || bar.compare || bar.layout === "grouped" || bar.layout === "stacked")
|
|
239
|
+
issue("This layout needs aligned x and series.");
|
|
240
|
+
if (bar.layout === "stacked" && bar.yAxis?.scale === "log") issue("Stacked bars need a linear axis.");
|
|
241
|
+
if (bar.normalize && bar.layout !== "stacked") issue("Normalized bars must be stacked.");
|
|
242
|
+
if (bar.layout === "stacked" && bar.series?.some((s) => s.data.some((v) => v !== null && v < 0)))
|
|
243
|
+
issue("Stacked bars require nonnegative values.");
|
|
244
|
+
if (bar.compare && (bar.series?.length !== 1 || bar.compare.data.length !== bar.x?.length || bar.layout === "stacked"))
|
|
245
|
+
issue("Comparison needs one current series and aligned previous values.");
|
|
246
|
+
}),
|
|
247
|
+
z
|
|
248
|
+
.object({
|
|
249
|
+
kind: z.literal("hbar"),
|
|
250
|
+
...ChartKindCommon,
|
|
251
|
+
series: list(ChartLabel, 1, limits?.maxSeries),
|
|
252
|
+
rows: list(
|
|
253
|
+
z.object({
|
|
254
|
+
group: ChartLabel.optional(),
|
|
255
|
+
label: ChartLabel,
|
|
256
|
+
values: list(z.number().nonnegative().nullable(), 0, limits?.maxSeries),
|
|
257
|
+
}),
|
|
258
|
+
1,
|
|
259
|
+
limits?.maxValues,
|
|
260
|
+
),
|
|
261
|
+
legend: z.boolean().optional(),
|
|
262
|
+
format: numberFormat.optional(),
|
|
263
|
+
})
|
|
264
|
+
.superRefine((bar, ctx) => {
|
|
265
|
+
if (bar.rows.some((r) => r.values.length !== bar.series.length))
|
|
266
|
+
ctx.addIssue({ code: "custom", message: "Horizontal bar values must match series." });
|
|
267
|
+
}),
|
|
156
268
|
z.object({
|
|
157
269
|
kind: z.literal("line"),
|
|
158
270
|
...ChartKindCommon,
|
|
@@ -220,15 +332,51 @@ export const chartKinds = (limits?: ChartLimits) => {
|
|
|
220
332
|
yAxis: ChartAxis.optional(),
|
|
221
333
|
references: ChartReferences.optional(),
|
|
222
334
|
}),
|
|
223
|
-
z.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
335
|
+
z.discriminatedUnion("layout", [
|
|
336
|
+
z
|
|
337
|
+
.object({
|
|
338
|
+
kind: z.literal("heatmap"),
|
|
339
|
+
...ChartKindCommon,
|
|
340
|
+
layout: z.literal("calendar"),
|
|
341
|
+
data: list(
|
|
342
|
+
z.tuple([
|
|
343
|
+
z.string().refine((day) => {
|
|
344
|
+
try {
|
|
345
|
+
calendarDay(day);
|
|
346
|
+
return true;
|
|
347
|
+
} catch {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}, "Invalid ISO calendar day."),
|
|
351
|
+
ChartNumber.nullable(),
|
|
352
|
+
]),
|
|
353
|
+
1,
|
|
354
|
+
limits?.maxMarks,
|
|
355
|
+
),
|
|
356
|
+
scale: z.enum(["sequential", "diverging"]),
|
|
357
|
+
label: ChartLabel.optional(),
|
|
358
|
+
format: numberFormat.optional(),
|
|
359
|
+
locale: chartLocaleSchema().optional(),
|
|
360
|
+
})
|
|
361
|
+
.superRefine((spec, ctx) => {
|
|
362
|
+
if (ctx.issues.length) return;
|
|
363
|
+
const times = spec.data.map(([day]) => calendarDay(day));
|
|
364
|
+
if (times.length && (Math.max(...times) - Math.min(...times)) / 86400000 >= CALENDAR_MAX_DAYS)
|
|
365
|
+
ctx.addIssue({ code: "custom", message: "Calendar span must not exceed three years." });
|
|
366
|
+
if (new Set(spec.data.map(([day]) => day)).size !== spec.data.length)
|
|
367
|
+
ctx.addIssue({ code: "custom", message: "Calendar dates must be unique." });
|
|
368
|
+
}),
|
|
369
|
+
z.object({
|
|
370
|
+
kind: z.literal("heatmap"),
|
|
371
|
+
...ChartKindCommon,
|
|
372
|
+
layout: z.undefined().optional(),
|
|
373
|
+
data: list(ChartCell, 1, limits?.maxMarks),
|
|
374
|
+
format: numberFormat.optional(),
|
|
375
|
+
showValues: z.boolean().optional(),
|
|
376
|
+
min: z.number().optional(),
|
|
377
|
+
max: z.number().optional(),
|
|
378
|
+
}),
|
|
379
|
+
]),
|
|
232
380
|
z.object({
|
|
233
381
|
kind: z.literal("barGauge"),
|
|
234
382
|
...ChartKindCommon,
|
|
@@ -270,5 +418,10 @@ export const chartDateTime = (value: string): number | undefined => {
|
|
|
270
418
|
return date.toISOString().startsWith(`${year}-${month}-${day}T${hour}:${minute}:${second}`) ? date.getTime() : undefined;
|
|
271
419
|
};
|
|
272
420
|
|
|
273
|
-
|
|
274
|
-
|
|
421
|
+
// Both directions are checked: a validator change must update the ui-owned description too.
|
|
422
|
+
type Assert<T extends true> = T;
|
|
423
|
+
type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
|
|
424
|
+
type _ChartSchemaMatchesDescription = Assert<Same<z.output<ReturnType<typeof chartKinds>>, ChartInput>>;
|
|
425
|
+
type _ChartNumberFormatSchemaMatchesDescription = Assert<
|
|
426
|
+
Same<z.output<ReturnType<typeof chartNumberFormatSchema>>, import("@k2b/ui/charts").ChartNumberFormat>
|
|
427
|
+
>;
|
package/src/charts.ts
CHANGED
|
@@ -1,35 +1,56 @@
|
|
|
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,
|
|
18
|
+
type HorizontalBarOptions,
|
|
19
|
+
hbarLayout,
|
|
13
20
|
histogram,
|
|
14
|
-
i18n,
|
|
15
21
|
line,
|
|
16
22
|
niceLogTicks,
|
|
17
23
|
niceStep,
|
|
18
24
|
pie,
|
|
25
|
+
renderAreaSvg,
|
|
26
|
+
renderBarChartSvg,
|
|
27
|
+
renderHbarSvg,
|
|
28
|
+
renderHeatmapSvg,
|
|
29
|
+
renderWaterfallSvg,
|
|
19
30
|
scatter,
|
|
20
31
|
sparkline,
|
|
21
32
|
stat,
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
type WaterfallOptions,
|
|
34
|
+
} from "@k2b/ui/charts/core";
|
|
24
35
|
import { z } from "zod";
|
|
25
36
|
import { type ChartFormat, type ChartInput, type ChartNumberFormat, chartDateTime, chartKinds } from "./chart-schema";
|
|
26
37
|
|
|
27
38
|
export * from "./chart-schema";
|
|
28
39
|
|
|
29
40
|
type RenderOptions = {
|
|
30
|
-
[K in ChartInput["kind"]]: { kind: K } & (K extends "
|
|
31
|
-
?
|
|
32
|
-
:
|
|
41
|
+
[K in ChartInput["kind"]]: { kind: K } & (K extends "hbar"
|
|
42
|
+
? HorizontalBarOptions
|
|
43
|
+
: K extends "waterfall"
|
|
44
|
+
? WaterfallOptions
|
|
45
|
+
: K extends "area"
|
|
46
|
+
? AreaOptions
|
|
47
|
+
: K extends "heatmap"
|
|
48
|
+
? HeatmapChartOptions
|
|
49
|
+
: K extends "bar"
|
|
50
|
+
? BarChartOptions & { data: NonNullable<BarChartOptions["data"]> }
|
|
51
|
+
: K extends keyof typeof charts
|
|
52
|
+
? Omit<Parameters<(typeof charts)[K]>[0], "width" | "height" | "inspect">
|
|
53
|
+
: never);
|
|
33
54
|
}[ChartInput["kind"]];
|
|
34
55
|
|
|
35
56
|
/**
|
|
@@ -45,8 +66,15 @@ const labels = /* @__PURE__ */ i18n.define({
|
|
|
45
66
|
en: {
|
|
46
67
|
category: "Category",
|
|
47
68
|
series: "Series",
|
|
69
|
+
balance: "Balance",
|
|
70
|
+
before: "Balance before",
|
|
71
|
+
inflow: "In",
|
|
72
|
+
outflow: "Out",
|
|
73
|
+
after: "Balance after",
|
|
48
74
|
value: "Value",
|
|
75
|
+
total: "Total",
|
|
49
76
|
share: "Share",
|
|
77
|
+
position: "Position",
|
|
50
78
|
from: "From",
|
|
51
79
|
to: "To",
|
|
52
80
|
count: "Count",
|
|
@@ -62,8 +90,15 @@ const labels = /* @__PURE__ */ i18n.define({
|
|
|
62
90
|
de: {
|
|
63
91
|
category: "Kategorie",
|
|
64
92
|
series: "Datenreihe",
|
|
93
|
+
balance: "Stand",
|
|
94
|
+
before: "Stand davor",
|
|
95
|
+
inflow: "Rein",
|
|
96
|
+
outflow: "Raus",
|
|
97
|
+
after: "Stand danach",
|
|
65
98
|
value: "Wert",
|
|
99
|
+
total: "Gesamt",
|
|
66
100
|
share: "Anteil",
|
|
101
|
+
position: "Position",
|
|
67
102
|
from: "Von",
|
|
68
103
|
to: "Bis",
|
|
69
104
|
count: "Anzahl",
|
|
@@ -123,7 +158,19 @@ export const chartReferenceValues = (input: ChartInput, name: "x" | "y") =>
|
|
|
123
158
|
|
|
124
159
|
/** The value axes of a chart with what they show: the data checks and the label room read them alike. */
|
|
125
160
|
export const chartValueAxes = (input: ChartInput): { values: number[]; axis?: ChartAxisInput; zero?: boolean; name: "x" | "y" }[] => {
|
|
126
|
-
if (input.kind === "bar")
|
|
161
|
+
if (input.kind === "bar")
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
values: [
|
|
165
|
+
...input.data.map((item) => item.value),
|
|
166
|
+
...(input.series?.flatMap((s) => s.data.filter((v): v is number => v !== null)) ?? []),
|
|
167
|
+
...(input.compare?.data.filter((v): v is number => v !== null) ?? []),
|
|
168
|
+
],
|
|
169
|
+
axis: input.yAxis,
|
|
170
|
+
zero: true,
|
|
171
|
+
name: "y",
|
|
172
|
+
},
|
|
173
|
+
];
|
|
127
174
|
if (input.kind === "boxplot") return [{ values: input.data.flatMap((item) => item.values), axis: input.yAxis, name: "y" }];
|
|
128
175
|
if (input.kind === "histogram") {
|
|
129
176
|
if (!input.references?.length) return [{ values: input.data, name: "x" }];
|
|
@@ -232,15 +279,29 @@ const numberFormat = (format: ChartFormat | undefined) => (format && !("date" in
|
|
|
232
279
|
/** The values a chart writes with its value format, for whole-number currency. */
|
|
233
280
|
const formattedValues = (input: ChartInput): number[] => {
|
|
234
281
|
switch (input.kind) {
|
|
282
|
+
case "waterfall":
|
|
283
|
+
return [input.start.value, ...input.steps.map((step) => step.value)];
|
|
284
|
+
case "area":
|
|
285
|
+
return input.series.flatMap((s) => s.data.filter((v): v is number => v !== null));
|
|
235
286
|
case "line":
|
|
236
287
|
case "scatter":
|
|
237
288
|
return [...input.series.flatMap((series) => series.data.map((point) => point.y)), ...chartReferenceValues(input, "y")];
|
|
238
289
|
case "bar":
|
|
239
|
-
return [
|
|
290
|
+
return [
|
|
291
|
+
...input.data.map((item) => item.value),
|
|
292
|
+
...(input.series?.flatMap((s) => s.data.filter((v): v is number => v !== null)) ?? []),
|
|
293
|
+
...(input.compare?.data.filter((v): v is number => v !== null) ?? []),
|
|
294
|
+
...chartReferenceValues(input, "y"),
|
|
295
|
+
];
|
|
296
|
+
case "hbar":
|
|
297
|
+
return input.rows.flatMap((r) => r.values.filter((v): v is number => v !== null));
|
|
240
298
|
case "pie":
|
|
241
299
|
case "donut":
|
|
242
|
-
case "heatmap":
|
|
243
300
|
return input.data.map((item) => item.value);
|
|
301
|
+
case "heatmap":
|
|
302
|
+
return input.layout === "calendar"
|
|
303
|
+
? input.data.flatMap((item) => (item[1] === null ? [] : [item[1]]))
|
|
304
|
+
: input.data.map((item) => item.value);
|
|
244
305
|
case "barGauge":
|
|
245
306
|
return [...input.data.flatMap((item) => [item.value, item.min, item.max]), input.min, input.max].filter(
|
|
246
307
|
(value) => value !== undefined,
|
|
@@ -304,8 +365,11 @@ export function chartLegend(
|
|
|
304
365
|
input: ChartInput,
|
|
305
366
|
locale: string,
|
|
306
367
|
): { label: string; color: number; style?: "solid" | "dashed" | "dotted"; reference?: boolean }[] {
|
|
368
|
+
// These category renderers own their live legends, including prepared snapshots.
|
|
369
|
+
if (input.kind === "hbar" || (input.kind === "bar" && input.series)) return [];
|
|
307
370
|
if (input.kind === "bar")
|
|
308
371
|
return input.colorByBar && input.legend ? input.data.map((entry, index) => ({ label: entry.label, color: index % 8 })) : [];
|
|
372
|
+
if (input.kind === "area") return input.legend === false ? [] : input.series.map((s, i) => ({ label: s.label, color: i % 8 }));
|
|
309
373
|
if (input.kind !== "line" && input.kind !== "scatter") return [];
|
|
310
374
|
if (!(input.legend ?? input.series.length > 1)) return [];
|
|
311
375
|
const t = labels.resolve([locale]).t;
|
|
@@ -334,6 +398,7 @@ export function chartRenderOptions(
|
|
|
334
398
|
formats?: { x?: (v: number) => string; y?: (v: number) => string; value?: (v: number) => string };
|
|
335
399
|
} = {},
|
|
336
400
|
): RenderOptions {
|
|
401
|
+
if (input.kind === "heatmap" && input.layout === "calendar") locale = input.locale ?? locale;
|
|
337
402
|
const defaults = formats(input, locale);
|
|
338
403
|
// Function formats must be measured with the same text the renderer will print.
|
|
339
404
|
const format = {
|
|
@@ -346,6 +411,37 @@ export function chartRenderOptions(
|
|
|
346
411
|
const width = options.width ?? WIDTH;
|
|
347
412
|
const references = renderReferences(input);
|
|
348
413
|
switch (input.kind) {
|
|
414
|
+
case "hbar":
|
|
415
|
+
return {
|
|
416
|
+
kind: "hbar",
|
|
417
|
+
locale,
|
|
418
|
+
series: input.series,
|
|
419
|
+
rows: input.rows,
|
|
420
|
+
legend: input.legend,
|
|
421
|
+
valueFormat: format.value,
|
|
422
|
+
};
|
|
423
|
+
case "waterfall":
|
|
424
|
+
return {
|
|
425
|
+
kind: "waterfall",
|
|
426
|
+
start: input.start,
|
|
427
|
+
steps: input.steps,
|
|
428
|
+
end: input.end,
|
|
429
|
+
displayWidth: options.width ?? 296,
|
|
430
|
+
locale,
|
|
431
|
+
format: format.value,
|
|
432
|
+
};
|
|
433
|
+
case "area":
|
|
434
|
+
return {
|
|
435
|
+
kind: "area",
|
|
436
|
+
layout: input.layout,
|
|
437
|
+
normalize: input.normalize,
|
|
438
|
+
x: input.x,
|
|
439
|
+
series: input.series,
|
|
440
|
+
displayWidth: options.width,
|
|
441
|
+
locale,
|
|
442
|
+
format: format.value,
|
|
443
|
+
legend: options.legend === "outside" ? false : input.legend,
|
|
444
|
+
};
|
|
349
445
|
case "bar":
|
|
350
446
|
case "boxplot": {
|
|
351
447
|
const yAxis = referencedAxis(input, "y");
|
|
@@ -353,14 +449,20 @@ export function chartRenderOptions(
|
|
|
353
449
|
if (input.kind === "bar")
|
|
354
450
|
return {
|
|
355
451
|
kind: "bar",
|
|
452
|
+
locale,
|
|
356
453
|
data: input.data,
|
|
454
|
+
x: input.x,
|
|
455
|
+
series: input.series,
|
|
456
|
+
layout: input.layout,
|
|
457
|
+
normalize: input.normalize,
|
|
458
|
+
compare: input.compare,
|
|
357
459
|
xAxis: input.xAxis,
|
|
358
460
|
yAxis: axis(yAxis, options.formats?.y ?? format.tick),
|
|
359
461
|
// A host's own function format writes printed values like its ticks and table cells.
|
|
360
462
|
valueFormat: options.formats?.value ?? options.formats?.y ?? format.value,
|
|
361
463
|
colorByBar: input.colorByBar,
|
|
362
464
|
showValues: input.showValues,
|
|
363
|
-
legend: options.legend === "outside" ? false : input.legend,
|
|
465
|
+
legend: input.series ? input.legend : options.legend === "outside" ? false : input.legend,
|
|
364
466
|
references,
|
|
365
467
|
...(options.width === undefined ? {} : { displayWidth: options.width }),
|
|
366
468
|
};
|
|
@@ -458,6 +560,17 @@ export function chartRenderOptions(
|
|
|
458
560
|
thresholds: input.thresholds,
|
|
459
561
|
};
|
|
460
562
|
case "heatmap":
|
|
563
|
+
if (input.layout === "calendar")
|
|
564
|
+
return {
|
|
565
|
+
kind: "heatmap",
|
|
566
|
+
layout: "calendar",
|
|
567
|
+
data: input.data,
|
|
568
|
+
scale: input.scale,
|
|
569
|
+
label: input.label,
|
|
570
|
+
locale: input.locale ?? locale,
|
|
571
|
+
format: format.value,
|
|
572
|
+
displayWidth: options.width,
|
|
573
|
+
};
|
|
461
574
|
return {
|
|
462
575
|
kind: "heatmap",
|
|
463
576
|
data: input.data,
|
|
@@ -501,6 +614,12 @@ const decode = (value: string) =>
|
|
|
501
614
|
export const drawChart = (options: RenderOptions): string => {
|
|
502
615
|
const common = { width: WIDTH, height: HEIGHT, inspect: true };
|
|
503
616
|
switch (options.kind) {
|
|
617
|
+
case "hbar":
|
|
618
|
+
return renderHbarSvg({ ...options, ...common, height: hbarLayout(options).height });
|
|
619
|
+
case "waterfall":
|
|
620
|
+
return renderWaterfallSvg({ ...options, ...common });
|
|
621
|
+
case "area":
|
|
622
|
+
return renderAreaSvg({ ...options, ...common });
|
|
504
623
|
case "bar":
|
|
505
624
|
return renderBarChartSvg({ ...options, ...common, height: barChartLayout(options).height });
|
|
506
625
|
case "line":
|
|
@@ -522,7 +641,7 @@ export const drawChart = (options: RenderOptions): string => {
|
|
|
522
641
|
case "stat":
|
|
523
642
|
return stat({ ...options, ...common });
|
|
524
643
|
case "heatmap":
|
|
525
|
-
return
|
|
644
|
+
return renderHeatmapSvg({ ...options, ...common });
|
|
526
645
|
case "sparkline":
|
|
527
646
|
return sparkline({ ...options, ...common });
|
|
528
647
|
}
|
|
@@ -548,11 +667,26 @@ export const chartMarkKey = (datum: Pick<ChartDatum, "index" | "seriesIndex"> &
|
|
|
548
667
|
`${datum.role === "outlier" ? "outlier:" : datum.role === "value" ? "value:" : ""}${datum.seriesIndex ?? 0}:${datum.index}`;
|
|
549
668
|
|
|
550
669
|
export type ChartColumn = { id: string; label: string; numeric: boolean };
|
|
551
|
-
export type ChartRow = {
|
|
670
|
+
export type ChartRow = {
|
|
671
|
+
key: string;
|
|
672
|
+
cells: Record<string, string>;
|
|
673
|
+
values: Record<string, string | number>;
|
|
674
|
+
tooltip?: { title?: string; rows: { label: string; value: string }[] };
|
|
675
|
+
};
|
|
552
676
|
export type ChartTable = { columns: ChartColumn[]; rows: ChartRow[] };
|
|
553
677
|
|
|
554
678
|
/** The first text column titles the tooltip; the other columns become rows, matching the table in every host. */
|
|
555
|
-
export function chartTooltip(
|
|
679
|
+
export function chartTooltip(
|
|
680
|
+
columns: ChartColumn[],
|
|
681
|
+
row: ChartRow,
|
|
682
|
+
kind?: ChartInput["kind"],
|
|
683
|
+
): { title?: string; rows: { label: string; value: string }[] } {
|
|
684
|
+
if (row.tooltip) return row.tooltip;
|
|
685
|
+
if (kind === "area")
|
|
686
|
+
return {
|
|
687
|
+
title: row.cells.x,
|
|
688
|
+
rows: [{ label: row.cells.series ?? "", value: `${row.cells.percent ? `${row.cells.percent} · ` : ""}${row.cells.value ?? "—"}` }],
|
|
689
|
+
};
|
|
556
690
|
const [first, ...rest] = columns;
|
|
557
691
|
const named = first && !first.numeric;
|
|
558
692
|
return {
|
|
@@ -572,6 +706,7 @@ export function chartRows(
|
|
|
572
706
|
locale: string,
|
|
573
707
|
options: { formats?: { x?: (v: number) => string; y?: (v: number) => string; value?: (v: number) => string } } = {},
|
|
574
708
|
) {
|
|
709
|
+
if (input.kind === "heatmap" && input.layout === "calendar") locale = input.locale ?? locale;
|
|
575
710
|
const t = labels.resolve([locale]).t;
|
|
576
711
|
const format = formats(input, locale);
|
|
577
712
|
const valueFormat = input.tooltip?.format
|
|
@@ -580,12 +715,27 @@ export function chartRows(
|
|
|
580
715
|
const xFormat = options.formats?.x ?? format.x.value;
|
|
581
716
|
const series = chartSeries(input);
|
|
582
717
|
const countFormat = (input.kind === "histogram" ? options.formats?.y : undefined) ?? chartNumberFormatters(undefined, locale).value;
|
|
583
|
-
const percent = new Intl.NumberFormat(locale, {
|
|
718
|
+
const percent = new Intl.NumberFormat(locale, {
|
|
719
|
+
style: "percent",
|
|
720
|
+
minimumFractionDigits: input.kind === "area" ? 1 : 0,
|
|
721
|
+
maximumFractionDigits: 1,
|
|
722
|
+
});
|
|
584
723
|
const unit = "unit" in input && input.unit ? ` ${input.unit}` : "";
|
|
585
724
|
const columns = new Map<string, ChartColumn>();
|
|
586
725
|
const rows: ChartRow[] = [];
|
|
726
|
+
if (input.kind === "waterfall") {
|
|
727
|
+
for (const [id, label, numeric] of [
|
|
728
|
+
["label", t.category, false],
|
|
729
|
+
["before", t.before, true],
|
|
730
|
+
["inflow", t.inflow, true],
|
|
731
|
+
["outflow", t.outflow, true],
|
|
732
|
+
["after", t.after, true],
|
|
733
|
+
] as const)
|
|
734
|
+
columns.set(id, { id, label, numeric });
|
|
735
|
+
}
|
|
587
736
|
const named = series.some((entry) => entry.label);
|
|
588
737
|
const add = (datum: ChartDatum): ChartRow => {
|
|
738
|
+
const tooltip = input.kind === "waterfall" ? { title: datum.label, rows: [] as { label: string; value: string }[] } : undefined;
|
|
589
739
|
const cells: Record<string, string> = {};
|
|
590
740
|
const values: Record<string, string | number> = {};
|
|
591
741
|
const cell = (id: string, label: string, value: string | number, text: string) => {
|
|
@@ -593,33 +743,83 @@ export function chartRows(
|
|
|
593
743
|
cells[id] = text;
|
|
594
744
|
values[id] = value;
|
|
595
745
|
};
|
|
746
|
+
if (input.kind === "area") cell("series", t.series, datum.label ?? "", datum.label ?? "");
|
|
596
747
|
if (input.kind === "line" || input.kind === "scatter") {
|
|
597
748
|
if (named || series.length > 1) {
|
|
598
749
|
const name = series[datum.seriesIndex ?? 0]?.label ?? `${t.series} ${(datum.seriesIndex ?? 0) + 1}`;
|
|
599
750
|
cell("series", t.series, name, name);
|
|
600
751
|
}
|
|
601
|
-
} else {
|
|
752
|
+
} else if (input.kind !== "area") {
|
|
602
753
|
// Bars may hide some axis labels; the table names every category.
|
|
603
754
|
const label =
|
|
604
755
|
input.kind === "bar" || input.kind === "pie" || input.kind === "donut" || input.kind === "boxplot" || input.kind === "barGauge"
|
|
605
|
-
? input.
|
|
756
|
+
? ((input.kind === "bar" && input.series
|
|
757
|
+
? undefined
|
|
758
|
+
: input.data[datum.role === "outlier" ? (datum.seriesIndex ?? 0) : datum.index]?.label) ?? datum.label)
|
|
606
759
|
: datum.label;
|
|
607
|
-
if (label)
|
|
760
|
+
if (label)
|
|
761
|
+
cell("label", input.kind === "heatmap" && input.layout === "calendar" ? calendarWords(locale).day : t.category, label, label);
|
|
608
762
|
}
|
|
609
763
|
for (const field of datum.values) {
|
|
610
764
|
const value = field.value;
|
|
765
|
+
if (typeof value === "string" && input.kind === "area" && field.key === "x") {
|
|
766
|
+
cell("x", t.position, value, field.formatted ?? value);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
611
769
|
if (typeof value === "string" && input.kind === "heatmap" && (field.key === "x" || field.key === "y")) {
|
|
612
770
|
cell(field.key, field.key.toUpperCase(), value, value);
|
|
613
771
|
continue;
|
|
614
772
|
}
|
|
773
|
+
if (input.kind === "hbar" || (input.kind === "bar" && input.series)) {
|
|
774
|
+
const seriesIndex = field.key.startsWith("series:") ? Number(field.key.slice(7)) : undefined;
|
|
775
|
+
const label =
|
|
776
|
+
seriesIndex === undefined
|
|
777
|
+
? field.key === "previous" && input.kind === "bar"
|
|
778
|
+
? (input.compare?.label ?? field.key)
|
|
779
|
+
: field.key === "total"
|
|
780
|
+
? t.total
|
|
781
|
+
: field.key === "delta"
|
|
782
|
+
? t.delta
|
|
783
|
+
: field.key
|
|
784
|
+
: input.kind === "hbar"
|
|
785
|
+
? input.series[seriesIndex]!
|
|
786
|
+
: input.series![seriesIndex]!.label;
|
|
787
|
+
cell(field.key, label, value, field.formatted ?? String(value));
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
615
790
|
if (typeof value !== "number" || field.key === "total") continue;
|
|
616
|
-
if (
|
|
617
|
-
const
|
|
791
|
+
if (input.kind === "waterfall" && ["balance", "before", "inflow", "outflow", "change", "after"].includes(field.key)) {
|
|
792
|
+
const label =
|
|
793
|
+
field.key === "balance"
|
|
794
|
+
? t.balance
|
|
795
|
+
: field.key === "before"
|
|
796
|
+
? t.before
|
|
797
|
+
: field.key === "inflow"
|
|
798
|
+
? t.inflow
|
|
799
|
+
: field.key === "outflow"
|
|
800
|
+
? t.outflow
|
|
801
|
+
: field.key === "after"
|
|
802
|
+
? t.after
|
|
803
|
+
: t.delta;
|
|
804
|
+
const arrow = field.key === "inflow" ? "↙ " : field.key === "outflow" ? "↗ " : field.key === "change" ? "= " : "";
|
|
805
|
+
const text = `${arrow}${valueFormat(field.key === "inflow" || field.key === "outflow" ? Math.abs(value) : value)}`;
|
|
806
|
+
// Totals occupy the ending balance column; zero changes are neutral inflows.
|
|
807
|
+
const id = field.key === "balance" ? "after" : field.key === "change" ? "inflow" : field.key;
|
|
808
|
+
cell(id, label, value, text);
|
|
809
|
+
if (field.key !== "before") tooltip?.rows.push({ label, value: text });
|
|
810
|
+
} else if (field.key === "x") {
|
|
811
|
+
const header =
|
|
812
|
+
(input.kind === "line" || input.kind === "scatter" ? input.xAxis?.label : undefined) ??
|
|
813
|
+
(input.kind === "area" ? t.position : "X");
|
|
618
814
|
cell(
|
|
619
815
|
"x",
|
|
620
816
|
input.kind === "sparkline" || input.kind === "stat" ? "#" : header,
|
|
621
817
|
value,
|
|
622
|
-
input.kind === "sparkline" || input.kind === "stat"
|
|
818
|
+
input.kind === "sparkline" || input.kind === "stat"
|
|
819
|
+
? String(value + 1)
|
|
820
|
+
: input.kind === "area"
|
|
821
|
+
? (field.formatted ?? String(value))
|
|
822
|
+
: xFormat(value),
|
|
623
823
|
);
|
|
624
824
|
} else if (field.key === "y") {
|
|
625
825
|
const header =
|
|
@@ -630,9 +830,17 @@ export function chartRows(
|
|
|
630
830
|
} else if (field.key === "value")
|
|
631
831
|
cell(
|
|
632
832
|
"value",
|
|
633
|
-
input.tooltip?.label ??
|
|
833
|
+
input.tooltip?.label ??
|
|
834
|
+
(input.kind === "bar"
|
|
835
|
+
? input.yAxis?.label
|
|
836
|
+
: input.kind === "heatmap" && input.layout === "calendar"
|
|
837
|
+
? input.label
|
|
838
|
+
: undefined) ??
|
|
839
|
+
t.value,
|
|
634
840
|
value,
|
|
635
|
-
|
|
841
|
+
input.kind === "heatmap" && input.layout === "calendar"
|
|
842
|
+
? (field.formatted ?? valueFormat(value))
|
|
843
|
+
: `${valueFormat(value)}${unit}`,
|
|
636
844
|
);
|
|
637
845
|
else if (field.key === "delta") cell("delta", t.delta, value, `${value > 0 ? "+" : ""}${valueFormat(value)}`);
|
|
638
846
|
else if (field.key === "q1" || field.key === "q2" || field.key === "q3" || field.key === "whiskerLow" || field.key === "whiskerHigh")
|
|
@@ -648,7 +856,7 @@ export function chartRows(
|
|
|
648
856
|
cell(field.key, t[field.key], value, `${fieldFormat(value)}${field.key === "min" || field.key === "max" ? unit : ""}`);
|
|
649
857
|
}
|
|
650
858
|
}
|
|
651
|
-
const row = { key: chartMarkKey(datum), cells, values };
|
|
859
|
+
const row = { key: chartMarkKey(datum), cells, values, ...(tooltip ? { tooltip } : {}) };
|
|
652
860
|
rows.push(row);
|
|
653
861
|
return row;
|
|
654
862
|
};
|
|
@@ -665,9 +873,17 @@ export function chartTable(input: ChartInput, locale: string): ChartTable {
|
|
|
665
873
|
/** Input values count against the budget, including observations summarized into boxes or bins. */
|
|
666
874
|
export const chartMarks = (input: ChartInput): number => {
|
|
667
875
|
switch (input.kind) {
|
|
876
|
+
case "waterfall":
|
|
877
|
+
return input.steps.length + 2;
|
|
878
|
+
case "area":
|
|
879
|
+
return input.series.reduce((sum, s) => sum + s.data.length, 0);
|
|
668
880
|
case "line":
|
|
669
881
|
case "scatter":
|
|
670
882
|
return input.series.reduce((sum, series) => sum + series.data.length, 0);
|
|
883
|
+
case "bar":
|
|
884
|
+
return input.series ? (input.x?.length ?? 0) * (input.series.length + (input.compare ? 1 : 0)) : input.data.length;
|
|
885
|
+
case "hbar":
|
|
886
|
+
return input.rows.length * input.series.length;
|
|
671
887
|
case "boxplot":
|
|
672
888
|
return input.data.reduce((sum, box) => sum + box.values.length, 0);
|
|
673
889
|
case "gauge":
|
|
@@ -1,6 +1,6 @@
|
|
|
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
4
|
import { Lexer, type Tokens } from "marked";
|
|
5
5
|
import type { z } from "zod";
|
|
6
6
|
import {
|
|
@@ -457,6 +457,9 @@ 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 === "hbar") return hbarLayout(options).height;
|
|
461
|
+
if (options.kind === "heatmap" && options.layout === "calendar") return 280;
|
|
462
|
+
if (options.kind === "waterfall" || options.kind === "area") return 280;
|
|
460
463
|
if (options.kind !== "bar") return undefined;
|
|
461
464
|
const layout = barChartLayout(options);
|
|
462
465
|
return layout.layout === "rows" ? layout.height : undefined;
|
|
@@ -487,17 +490,31 @@ export const renderNotebookChartFigure = (input: ChartInput, locale: string, opt
|
|
|
487
490
|
key: ({ datum }) => chartMarkKey(datum),
|
|
488
491
|
tooltip: ({ datum }) => {
|
|
489
492
|
const row = rows.add(datum);
|
|
490
|
-
return chartTooltip(rows.columns(), row);
|
|
493
|
+
return chartTooltip(rows.columns(), row, input.kind);
|
|
491
494
|
},
|
|
492
495
|
reference: ({ datum }) => chartReferenceMark(input, datum),
|
|
493
496
|
});
|
|
494
497
|
if (!snapshot.marks.length) return null;
|
|
495
498
|
const legend = chartLegend(input, locale);
|
|
496
499
|
const t = chartBlockMessages.resolve([locale]).t;
|
|
500
|
+
const valueField = input.kind === "area" ? (input.normalize ? "percent" : "value") : "y";
|
|
501
|
+
let lastX = -Infinity;
|
|
502
|
+
for (const mark of snapshot.marks) lastX = Math.max(lastX, mark.datum.anchor[0]);
|
|
503
|
+
const legendValue = (index: number) => {
|
|
504
|
+
const field = snapshot.marks
|
|
505
|
+
.find((mark) => mark.datum.seriesIndex === index && mark.datum.anchor[0] === lastX)
|
|
506
|
+
?.datum.values.find((field) => field.key === valueField);
|
|
507
|
+
return field
|
|
508
|
+
? (field.formatted ??
|
|
509
|
+
(typeof field.value === "number"
|
|
510
|
+
? new Intl.NumberFormat(locale, { maximumFractionDigits: 6 }).format(field.value)
|
|
511
|
+
: field.value))
|
|
512
|
+
: "—";
|
|
513
|
+
};
|
|
497
514
|
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>`
|
|
515
|
+
? `<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
516
|
: "";
|
|
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}`;
|
|
517
|
+
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
518
|
const height = notebookChartHeight(input, locale);
|
|
502
519
|
const figure = notebookChartFrameHtml(height ? { ...input, height } : input, viewport, {
|
|
503
520
|
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(
|