@k2b/editor 0.6.5 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k2b/editor",
3
- "version": "0.6.5",
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.49.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 ?? "");
@@ -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({ value: z.union([z.number(), ChartDate]), axis: z.enum(["x", "y"]).optional(), label: ChartLabel.optional() }),
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.object({
224
- kind: z.literal("heatmap"),
225
- ...ChartKindCommon,
226
- data: list(ChartCell, 1, limits?.maxMarks),
227
- format: numberFormat.optional(),
228
- showValues: z.boolean().optional(),
229
- min: z.number().optional(),
230
- max: z.number().optional(),
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
- heatmap,
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/stdlib";
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 "bar"
31
- ? BarChartOptions
32
- : Omit<Parameters<(typeof charts)[K]>[0], "width" | "height" | "inspect">);
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 heatmap({ ...options, ...common });
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(columns: ChartColumn[], row: ChartRow): { title?: string; rows: { label: string; value: string }[] } {
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, { style: "percent", maximumFractionDigits: 1 });
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) cell("label", t.category, label, 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 = (input.kind === "line" || input.kind === "scatter" ? input.xAxis?.label : undefined) ?? "X";
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" ? String(value + 1) : xFormat(value),
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 ?? (input.kind === "bar" ? input.yAxis?.label : undefined) ?? t.value,
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
- `${valueFormat(value)}${unit}`,
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(