@k2b/editor 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k2b/editor",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Configurable Markdown editor for SolidJS: editor, toolbar, bubble menu, and slash menu on one shared state.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -70,7 +70,7 @@
70
70
  "@codemirror/state": "6.7.1",
71
71
  "@codemirror/view": "6.43.7",
72
72
  "@k2b/stdlib": "0.30.0",
73
- "@k2b/ui": "0.51.0",
73
+ "@k2b/ui": "0.52.0",
74
74
  "@lezer/common": "1.5.3",
75
75
  "@lezer/highlight": "1.2.4",
76
76
  "@lezer/markdown": "1.7.2",
@@ -35,6 +35,7 @@ export const chartKindSchema = () =>
35
35
  "area",
36
36
  "bar",
37
37
  "hbar",
38
+ "funnel",
38
39
  "line",
39
40
  "scatter",
40
41
  "pie",
@@ -46,6 +47,10 @@ export const chartKindSchema = () =>
46
47
  "heatmap",
47
48
  "barGauge",
48
49
  "stat",
50
+ "treemap",
51
+ "pareto",
52
+ "bubble",
53
+ "radar",
49
54
  ]);
50
55
 
51
56
  export const chartParts = (limits?: ChartLimits) => {
@@ -219,6 +224,10 @@ export const chartKinds = (limits?: ChartLimits) => {
219
224
  ).optional(),
220
225
  layout: z.enum(["columns", "rows", "grouped", "stacked"]).optional(),
221
226
  normalize: z.boolean().optional(),
227
+ share: z.boolean().optional(),
228
+ limit: z.number().int().positive().optional(),
229
+ tone: z.enum(["in", "out"]).optional(),
230
+ format: numberFormat.optional(),
222
231
  compare: z.object({ label: ChartLabel, data: list(ChartNumber.nullable(), 0, limits?.maxValues) }).optional(),
223
232
  xAxis: ChartAxis.pick({ label: true }).extend({ ordered: z.boolean().optional() }).strict().optional(),
224
233
  yAxis: ChartAxis.optional(),
@@ -231,6 +240,10 @@ export const chartKinds = (limits?: ChartLimits) => {
231
240
  const issue = (message: string) => ctx.addIssue({ code: "custom", message });
232
241
  if (limits && !bar.series && !bar.data.length)
233
242
  ctx.addIssue({ code: "custom", path: ["data"], message: "A bar chart needs data or aligned series." });
243
+ if ((bar.share !== undefined || bar.limit !== undefined || bar.tone !== undefined) && bar.layout !== "rows")
244
+ issue("Share, limit and tone require rows layout.");
245
+ if (bar.layout === "rows" && bar.data.some((d) => Number.isFinite(d.value) && d.value < 0))
246
+ issue("Ranked bar lists require nonnegative values.");
234
247
  if (bar.series) {
235
248
  if (!bar.x || bar.series.some((s) => s.data.length !== bar.x?.length)) issue("Bar series must have the same length as x.");
236
249
  if (bar.data.length) issue("Use data or aligned series, not both.");
@@ -244,6 +257,12 @@ export const chartKinds = (limits?: ChartLimits) => {
244
257
  if (bar.compare && (bar.series?.length !== 1 || bar.compare.data.length !== bar.x?.length || bar.layout === "stacked"))
245
258
  issue("Comparison needs one current series and aligned previous values.");
246
259
  }),
260
+ z.object({
261
+ kind: z.literal("funnel"),
262
+ ...ChartKindCommon,
263
+ data: list(z.object({ label: ChartLabel, value: z.number().nonnegative().nullable() }), 1, limits?.maxValues),
264
+ format: numberFormat.optional(),
265
+ }),
247
266
  z
248
267
  .object({
249
268
  kind: z.literal("hbar"),
@@ -398,6 +417,81 @@ export const chartKinds = (limits?: ChartLimits) => {
398
417
  better: z.enum(["up", "down"]).optional(),
399
418
  sparkline: list(ChartNumber, 2, limits?.maxValues).optional(),
400
419
  }),
420
+ z
421
+ .object({
422
+ kind: z.literal("treemap"),
423
+ ...ChartKindCommon,
424
+ groups: list(ChartLabel, 1, limits?.maxSeries),
425
+ data: list(z.object({ label: ChartLabel, group: ChartLabel, value: z.number().nonnegative() }), 1, limits?.maxMarks),
426
+ legend: z.boolean().optional(),
427
+ format: numberFormat.optional(),
428
+ locale: chartLocaleSchema().optional(),
429
+ })
430
+ .superRefine((spec, ctx) => {
431
+ if (!Number.isFinite(spec.data.reduce((sum, d) => sum + d.value, 0)))
432
+ ctx.addIssue({ code: "custom", message: "Treemap total must be finite." });
433
+ if (spec.data.some((d) => !spec.groups.includes(d.group)))
434
+ ctx.addIssue({ code: "custom", message: "Treemap groups must contain every item group." });
435
+ }),
436
+ z
437
+ .object({
438
+ kind: z.literal("pareto"),
439
+ ...ChartKindCommon,
440
+ labels: z.tuple([ChartLabel, ChartLabel]),
441
+ data: list(z.object({ label: ChartLabel, value: z.number().nonnegative(), prev: z.number().nonnegative() }), 1, limits?.maxValues),
442
+ format: numberFormat.optional(),
443
+ locale: chartLocaleSchema().optional(),
444
+ })
445
+ .superRefine((spec, ctx) => {
446
+ if (!Number.isFinite(spec.data.reduce((sum, d) => sum + d.value, 0)))
447
+ ctx.addIssue({ code: "custom", message: "Pareto total must be finite." });
448
+ }),
449
+ z
450
+ .object({
451
+ kind: z.literal("bubble"),
452
+ ...ChartKindCommon,
453
+ groups: list(ChartLabel, 1, limits?.maxSeries),
454
+ data: list(
455
+ z.object({ label: ChartLabel, group: ChartLabel, x: z.number(), y: z.number(), size: z.number().nonnegative().optional() }),
456
+ 1,
457
+ limits?.maxMarks,
458
+ ),
459
+ xLabel: ChartLabel,
460
+ yLabel: ChartLabel,
461
+ sizeLabel: ChartLabel.optional(),
462
+ averages: z.boolean().optional(),
463
+ legend: z.boolean().optional(),
464
+ xFormat: numberFormat.optional(),
465
+ yFormat: numberFormat.optional(),
466
+ format: numberFormat.optional(),
467
+ locale: chartLocaleSchema().optional(),
468
+ })
469
+ .superRefine((spec, ctx) => {
470
+ if (spec.data.some((p) => !spec.groups.includes(p.group)))
471
+ ctx.addIssue({ code: "custom", message: "Bubble groups must contain every point group." });
472
+ for (const key of ["x", "y"] as const)
473
+ if (!Number.isFinite(Math.max(0, ...spec.data.map((p) => p[key])) - Math.min(0, ...spec.data.map((p) => p[key]))))
474
+ ctx.addIssue({ code: "custom", message: "Bubble coordinate ranges must be finite." });
475
+ }),
476
+ z
477
+ .object({
478
+ kind: z.literal("radar"),
479
+ ...ChartKindCommon,
480
+ axes: z.array(ChartLabel).min(3).max(8),
481
+ max: z.number().positive(),
482
+ unit: ChartUnit.optional(),
483
+ series: z
484
+ .array(z.object({ label: ChartLabel, data: z.array(z.number().nonnegative()).min(3).max(8) }))
485
+ .min(1)
486
+ .max(4),
487
+ format: numberFormat.optional(),
488
+ legend: z.boolean().optional(),
489
+ locale: chartLocaleSchema().optional(),
490
+ })
491
+ .superRefine((spec, ctx) => {
492
+ if (spec.series.some((s) => s.data.length !== spec.axes.length || s.data.some((v) => v > spec.max)))
493
+ ctx.addIssue({ code: "custom", message: "Radar series need one value per axis between zero and the common maximum." });
494
+ }),
401
495
  ]);
402
496
  };
403
497
 
package/src/charts.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  type AreaOptions,
5
5
  autoBin,
6
6
  type BarChartOptions,
7
+ type BubbleOptions,
7
8
  barChartLayout,
8
9
  barGauge,
9
10
  boxplot,
@@ -13,6 +14,7 @@ import {
13
14
  computeDomain,
14
15
  donut,
15
16
  extendDomainToNice,
17
+ type FunnelOptions,
16
18
  gauge,
17
19
  type HeatmapChartOptions,
18
20
  type HorizontalBarOptions,
@@ -21,15 +23,24 @@ import {
21
23
  line,
22
24
  niceLogTicks,
23
25
  niceStep,
26
+ type ParetoOptions,
24
27
  pie,
28
+ type RadarOptions,
29
+ rankedBarLayout,
25
30
  renderAreaSvg,
26
31
  renderBarChartSvg,
32
+ renderBubbleSvg,
33
+ renderFunnelSvg,
27
34
  renderHbarSvg,
28
35
  renderHeatmapSvg,
36
+ renderParetoSvg,
37
+ renderRadarSvg,
38
+ renderTreemapSvg,
29
39
  renderWaterfallSvg,
30
40
  scatter,
31
41
  sparkline,
32
42
  stat,
43
+ type TreemapOptions,
33
44
  type WaterfallOptions,
34
45
  } from "@k2b/ui/charts/core";
35
46
  import { z } from "zod";
@@ -38,19 +49,29 @@ import { type ChartFormat, type ChartInput, type ChartNumberFormat, chartDateTim
38
49
  export * from "./chart-schema";
39
50
 
40
51
  type RenderOptions = {
41
- [K in ChartInput["kind"]]: { kind: K } & (K extends "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);
52
+ [K in ChartInput["kind"]]: { kind: K } & (K extends "funnel"
53
+ ? FunnelOptions
54
+ : K extends "hbar"
55
+ ? HorizontalBarOptions
56
+ : K extends "waterfall"
57
+ ? WaterfallOptions
58
+ : K extends "area"
59
+ ? AreaOptions
60
+ : K extends "treemap"
61
+ ? TreemapOptions
62
+ : K extends "pareto"
63
+ ? ParetoOptions
64
+ : K extends "bubble"
65
+ ? BubbleOptions
66
+ : K extends "radar"
67
+ ? RadarOptions
68
+ : K extends "heatmap"
69
+ ? HeatmapChartOptions
70
+ : K extends "bar"
71
+ ? BarChartOptions & { data: NonNullable<BarChartOptions["data"]> }
72
+ : K extends keyof typeof charts
73
+ ? Omit<Parameters<(typeof charts)[K]>[0], "width" | "height" | "inspect">
74
+ : never);
54
75
  }[ChartInput["kind"]];
55
76
 
56
77
  /**
@@ -65,7 +86,13 @@ const labels = /* @__PURE__ */ i18n.define({
65
86
  messages: {
66
87
  en: {
67
88
  category: "Category",
89
+ size: "Size",
90
+ axis: "Axis",
68
91
  series: "Series",
92
+ group: "Group",
93
+ previous: "Previous",
94
+ change: "Change",
95
+ cumulative: "Cumulative",
69
96
  balance: "Balance",
70
97
  before: "Balance before",
71
98
  inflow: "In",
@@ -86,10 +113,19 @@ const labels = /* @__PURE__ */ i18n.define({
86
113
  whiskerLow: "Whisker low",
87
114
  whiskerHigh: "Whisker high",
88
115
  delta: "Change",
116
+ startShare: "Share of start",
117
+ previousShare: "Share of previous step",
118
+ rank: "Rank",
89
119
  },
90
120
  de: {
91
121
  category: "Kategorie",
122
+ size: "Größe",
123
+ axis: "Achse",
92
124
  series: "Datenreihe",
125
+ group: "Bereich",
126
+ previous: "Vorher",
127
+ change: "Änderung",
128
+ cumulative: "Kumuliert",
93
129
  balance: "Stand",
94
130
  before: "Stand davor",
95
131
  inflow: "Rein",
@@ -110,6 +146,9 @@ const labels = /* @__PURE__ */ i18n.define({
110
146
  whiskerLow: "Unterer Whisker",
111
147
  whiskerHigh: "Oberer Whisker",
112
148
  delta: "Änderung",
149
+ startShare: "Anteil am Start",
150
+ previousShare: "Anteil am vorigen Schritt",
151
+ rank: "Rang",
113
152
  },
114
153
  },
115
154
  });
@@ -279,6 +318,10 @@ const numberFormat = (format: ChartFormat | undefined) => (format && !("date" in
279
318
  /** The values a chart writes with its value format, for whole-number currency. */
280
319
  const formattedValues = (input: ChartInput): number[] => {
281
320
  switch (input.kind) {
321
+ case "bubble":
322
+ return input.data.flatMap((p) => (p.size === undefined ? [] : [p.size]));
323
+ case "radar":
324
+ return input.series.flatMap((s) => s.data);
282
325
  case "waterfall":
283
326
  return [input.start.value, ...input.steps.map((step) => step.value)];
284
327
  case "area":
@@ -295,6 +338,10 @@ const formattedValues = (input: ChartInput): number[] => {
295
338
  ];
296
339
  case "hbar":
297
340
  return input.rows.flatMap((r) => r.values.filter((v): v is number => v !== null));
341
+ case "funnel":
342
+ return input.data.flatMap((d) => (d.value === null ? [] : [d.value]));
343
+ case "treemap":
344
+ case "pareto":
298
345
  case "pie":
299
346
  case "donut":
300
347
  return input.data.map((item) => item.value);
@@ -318,7 +365,7 @@ const formattedValues = (input: ChartInput): number[] => {
318
365
  }
319
366
  };
320
367
  const formats = (input: ChartInput, locale: string): Formats => {
321
- const yFormat = "yAxis" in input ? numberFormat(input.yAxis?.format) : "format" in input ? input.format : undefined;
368
+ const yFormat = ("yAxis" in input ? numberFormat(input.yAxis?.format) : undefined) ?? ("format" in input ? input.format : undefined);
322
369
  const y = chartNumberFormatters(yFormat, locale, formattedValues(input));
323
370
  const xFormat = "xAxis" in input && input.kind !== "bar" ? input.xAxis?.format : undefined;
324
371
  const x = chartNumberFormatters(numberFormat(xFormat), locale);
@@ -398,7 +445,7 @@ export function chartRenderOptions(
398
445
  formats?: { x?: (v: number) => string; y?: (v: number) => string; value?: (v: number) => string };
399
446
  } = {},
400
447
  ): RenderOptions {
401
- if (input.kind === "heatmap" && input.layout === "calendar") locale = input.locale ?? locale;
448
+ if ("locale" in input && input.locale) locale = input.locale;
402
449
  const defaults = formats(input, locale);
403
450
  // Function formats must be measured with the same text the renderer will print.
404
451
  const format = {
@@ -411,6 +458,55 @@ export function chartRenderOptions(
411
458
  const width = options.width ?? WIDTH;
412
459
  const references = renderReferences(input);
413
460
  switch (input.kind) {
461
+ case "treemap":
462
+ return {
463
+ kind: "treemap",
464
+ legend: input.legend,
465
+ groups: input.groups,
466
+ data: input.data,
467
+ format: format.value,
468
+ locale: input.locale ?? locale,
469
+ displayWidth: options.width,
470
+ };
471
+ case "pareto":
472
+ return {
473
+ kind: "pareto",
474
+ labels: input.labels,
475
+ data: input.data,
476
+ format: format.value,
477
+ locale: input.locale ?? locale,
478
+ displayWidth: options.width,
479
+ };
480
+ case "bubble":
481
+ return {
482
+ kind: "bubble",
483
+ groups: input.groups,
484
+ data: input.data,
485
+ xLabel: input.xLabel,
486
+ yLabel: input.yLabel,
487
+ sizeLabel: input.sizeLabel,
488
+ averages: input.averages,
489
+ legend: input.legend,
490
+ locale,
491
+ displayWidth: options.width,
492
+ xFormat: options.formats?.x ?? (input.xFormat ? chartNumberFormatters(input.xFormat, locale).value : undefined),
493
+ yFormat: options.formats?.y ?? (input.yFormat ? chartNumberFormatters(input.yFormat, locale).value : undefined),
494
+ sizeFormat: format.value,
495
+ };
496
+ case "radar":
497
+ return {
498
+ kind: "radar",
499
+ axes: input.axes,
500
+ max: input.max,
501
+ unit: input.unit,
502
+ series: input.series,
503
+ legend: input.legend,
504
+ locale,
505
+ displayWidth: options.width,
506
+ format: format.value,
507
+ };
508
+ case "funnel":
509
+ return { kind: "funnel", data: input.data, valueFormat: format.value, locale };
414
510
  case "hbar":
415
511
  return {
416
512
  kind: "hbar",
@@ -455,6 +551,9 @@ export function chartRenderOptions(
455
551
  series: input.series,
456
552
  layout: input.layout,
457
553
  normalize: input.normalize,
554
+ share: input.share,
555
+ limit: input.limit,
556
+ tone: input.tone,
458
557
  compare: input.compare,
459
558
  xAxis: input.xAxis,
460
559
  yAxis: axis(yAxis, options.formats?.y ?? format.tick),
@@ -614,6 +713,16 @@ const decode = (value: string) =>
614
713
  export const drawChart = (options: RenderOptions): string => {
615
714
  const common = { width: WIDTH, height: HEIGHT, inspect: true };
616
715
  switch (options.kind) {
716
+ case "treemap":
717
+ return renderTreemapSvg({ ...options, ...common });
718
+ case "pareto":
719
+ return renderParetoSvg({ ...options, ...common });
720
+ case "bubble":
721
+ return renderBubbleSvg({ ...options, ...common });
722
+ case "radar":
723
+ return renderRadarSvg({ ...options, ...common });
724
+ case "funnel":
725
+ return renderFunnelSvg({ ...options, ...common });
617
726
  case "hbar":
618
727
  return renderHbarSvg({ ...options, ...common, height: hbarLayout(options).height });
619
728
  case "waterfall":
@@ -706,7 +815,7 @@ export function chartRows(
706
815
  locale: string,
707
816
  options: { formats?: { x?: (v: number) => string; y?: (v: number) => string; value?: (v: number) => string } } = {},
708
817
  ) {
709
- if (input.kind === "heatmap" && input.layout === "calendar") locale = input.locale ?? locale;
818
+ if ("locale" in input && input.locale) locale = input.locale;
710
819
  const t = labels.resolve([locale]).t;
711
820
  const format = formats(input, locale);
712
821
  const valueFormat = input.tooltip?.format
@@ -738,8 +847,8 @@ export function chartRows(
738
847
  const tooltip = input.kind === "waterfall" ? { title: datum.label, rows: [] as { label: string; value: string }[] } : undefined;
739
848
  const cells: Record<string, string> = {};
740
849
  const values: Record<string, string | number> = {};
741
- const cell = (id: string, label: string, value: string | number, text: string) => {
742
- if (!columns.has(id)) columns.set(id, { id, label, numeric: typeof value === "number" });
850
+ const cell = (id: string, label: string, value: string | number, text: string, numeric = typeof value === "number") => {
851
+ if (!columns.has(id)) columns.set(id, { id, label, numeric });
743
852
  cells[id] = text;
744
853
  values[id] = value;
745
854
  };
@@ -758,10 +867,35 @@ export function chartRows(
758
867
  : input.data[datum.role === "outlier" ? (datum.seriesIndex ?? 0) : datum.index]?.label) ?? datum.label)
759
868
  : datum.label;
760
869
  if (label)
761
- cell("label", input.kind === "heatmap" && input.layout === "calendar" ? calendarWords(locale).day : t.category, label, label);
870
+ cell(
871
+ "label",
872
+ input.kind === "heatmap" && input.layout === "calendar"
873
+ ? calendarWords(locale).day
874
+ : input.kind === "radar"
875
+ ? t.axis
876
+ : t.category,
877
+ label,
878
+ label,
879
+ );
762
880
  }
763
881
  for (const field of datum.values) {
764
882
  const value = field.value;
883
+ if (input.kind === "bubble") {
884
+ const label =
885
+ field.key === "x"
886
+ ? input.xLabel
887
+ : field.key === "y"
888
+ ? input.yLabel
889
+ : field.key === "size"
890
+ ? (input.sizeLabel ?? t.size)
891
+ : t.group;
892
+ cell(field.key, label, value, field.formatted ?? String(value));
893
+ continue;
894
+ }
895
+ if (input.kind === "radar" && field.key.startsWith("series:")) {
896
+ cell(field.key, input.series[Number(field.key.slice(7))]!.label, value, field.formatted ?? String(value));
897
+ continue;
898
+ }
765
899
  if (typeof value === "string" && input.kind === "area" && field.key === "x") {
766
900
  cell("x", t.position, value, field.formatted ?? value);
767
901
  continue;
@@ -770,6 +904,18 @@ export function chartRows(
770
904
  cell(field.key, field.key.toUpperCase(), value, value);
771
905
  continue;
772
906
  }
907
+ if (input.kind === "treemap" && field.key === "group" && typeof value === "string") {
908
+ cell("group", t.group, value, value);
909
+ continue;
910
+ }
911
+ if (
912
+ input.kind === "pareto" &&
913
+ (field.key === "previous" || field.key === "change" || field.key === "cumulative") &&
914
+ typeof value === "number"
915
+ ) {
916
+ cell(field.key, field.key === "previous" ? input.labels[1] : t[field.key], value, field.formatted ?? valueFormat(value));
917
+ continue;
918
+ }
773
919
  if (input.kind === "hbar" || (input.kind === "bar" && input.series)) {
774
920
  const seriesIndex = field.key.startsWith("series:") ? Number(field.key.slice(7)) : undefined;
775
921
  const label =
@@ -784,7 +930,30 @@ export function chartRows(
784
930
  : input.kind === "hbar"
785
931
  ? input.series[seriesIndex]!
786
932
  : input.series![seriesIndex]!.label;
787
- cell(field.key, label, value, field.formatted ?? String(value));
933
+ cell(field.key, label, value, field.formatted ?? String(value), true);
934
+ continue;
935
+ }
936
+ if (
937
+ field.key === "startPercent" ||
938
+ field.key === "previousPercent" ||
939
+ field.key === "rank" ||
940
+ ((input.kind === "bar" || input.kind === "funnel") && typeof value === "string")
941
+ ) {
942
+ cell(
943
+ field.key,
944
+ field.key === "startPercent"
945
+ ? t.startShare
946
+ : field.key === "previousPercent"
947
+ ? t.previousShare
948
+ : field.key === "rank"
949
+ ? t.rank
950
+ : field.key === "percent"
951
+ ? t.share
952
+ : t.value,
953
+ value,
954
+ field.formatted ?? String(value),
955
+ true,
956
+ );
788
957
  continue;
789
958
  }
790
959
  if (typeof value !== "number" || field.key === "total") continue;
@@ -831,11 +1000,13 @@ export function chartRows(
831
1000
  cell(
832
1001
  "value",
833
1002
  input.tooltip?.label ??
834
- (input.kind === "bar"
835
- ? input.yAxis?.label
836
- : input.kind === "heatmap" && input.layout === "calendar"
837
- ? input.label
838
- : undefined) ??
1003
+ (input.kind === "pareto"
1004
+ ? input.labels[0]
1005
+ : input.kind === "bar"
1006
+ ? input.yAxis?.label
1007
+ : input.kind === "heatmap" && input.layout === "calendar"
1008
+ ? input.label
1009
+ : undefined) ??
839
1010
  t.value,
840
1011
  value,
841
1012
  input.kind === "heatmap" && input.layout === "calendar"
@@ -860,7 +1031,37 @@ export function chartRows(
860
1031
  rows.push(row);
861
1032
  return row;
862
1033
  };
863
- return { add, columns: () => [...columns.values()], table: (): ChartTable => ({ columns: [...columns.values()], rows }) };
1034
+ return {
1035
+ add,
1036
+ columns: () => [...columns.values()],
1037
+ table: (): ChartTable => {
1038
+ if (input.kind === "bar" && input.layout === "rows" && input.limit !== undefined) {
1039
+ const source = { ...input, limit: undefined };
1040
+ const all = chartRows(source, locale, options);
1041
+ const layout = rankedBarLayout({ data: source.data, title: source.title, subtitle: source.subtitle, valueFormat, locale });
1042
+ for (const row of layout.rows) {
1043
+ const value = Number.isFinite(row.value) ? row.value : null;
1044
+ all.add({
1045
+ role: "item",
1046
+ index: row.index,
1047
+ label: row.label,
1048
+ anchor: [0, row.y],
1049
+ values: [
1050
+ { key: "value", value: value ?? "–", formatted: value === null ? "–" : valueFormat(value) },
1051
+ {
1052
+ key: "percent",
1053
+ value: row.share === null ? "–" : row.share * 100,
1054
+ formatted: row.share === null ? "–" : percent.format(row.share),
1055
+ },
1056
+ { key: "rank", value: row.rank },
1057
+ ],
1058
+ });
1059
+ }
1060
+ return all.table();
1061
+ }
1062
+ return { columns: [...columns.values()], rows };
1063
+ },
1064
+ };
864
1065
  }
865
1066
 
866
1067
  /** The data behind every mark of the chart as a table: the accessible alternative and what Copy data copies. */
@@ -873,6 +1074,10 @@ export function chartTable(input: ChartInput, locale: string): ChartTable {
873
1074
  /** Input values count against the budget, including observations summarized into boxes or bins. */
874
1075
  export const chartMarks = (input: ChartInput): number => {
875
1076
  switch (input.kind) {
1077
+ case "pareto":
1078
+ return input.data.length * 3;
1079
+ case "radar":
1080
+ return input.axes.length * input.series.length;
876
1081
  case "waterfall":
877
1082
  return input.steps.length + 2;
878
1083
  case "area":
@@ -1,6 +1,7 @@
1
1
  import { highlight, text } from "@k2b/stdlib";
2
2
  import { prepareChartSnapshot } from "@k2b/ui";
3
3
  import { barChartLayout, hbarLayout } from "@k2b/ui/charts";
4
+ import { paretoLayout } from "@k2b/ui/charts/core";
4
5
  import { Lexer, type Tokens } from "marked";
5
6
  import type { z } from "zod";
6
7
  import {
@@ -457,6 +458,10 @@ export const chartBlockFrame = (body: string[], options: Pick<MarkdownChartOptio
457
458
  */
458
459
  export const notebookChartHeight = (input: ChartInput, locale: string): number | undefined => {
459
460
  const options = chartRenderOptions(input, locale, { legend: "outside" });
461
+ if (options.kind === "treemap") return 280;
462
+ if (options.kind === "pareto") return paretoLayout(options).height;
463
+
464
+ if (options.kind === "bubble" || options.kind === "radar") return 280;
460
465
  if (options.kind === "hbar") return hbarLayout(options).height;
461
466
  if (options.kind === "heatmap" && options.layout === "calendar") return 280;
462
467
  if (options.kind === "waterfall" || options.kind === "area") return 280;