@k2b/editor 0.7.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k2b/editor",
3
- "version": "0.7.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.50.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",
@@ -1,12 +1,10 @@
1
- import { chartLocaleSchema } from "./chart-locale";
2
-
3
- export { chartAcceptsLocale, chartLocaleSchema } from "./chart-locale";
4
-
1
+ import type { ChartInput, ChartLimits } from "@k2b/ui/charts";
5
2
  import { CALENDAR_MAX_DAYS, calendarDay } from "@k2b/ui/charts/core";
6
3
  import { z } from "zod";
4
+ import { chartLocaleSchema } from "./chart-locale";
7
5
 
8
- /** Optional caller policy. Rendering itself has no AI input budget. */
9
- export type ChartLimits = { maxSeries: number; maxValues: number; maxMarks: number };
6
+ export type { ChartDateFormat, ChartDescription, ChartFormat, ChartInput, ChartLimits, ChartNumberFormat } from "@k2b/ui/charts";
7
+ export { chartAcceptsLocale, chartLocaleSchema } from "./chart-locale";
10
8
 
11
9
  // Every chart schema is built by a function whose call is marked pure, so a bundle that imports neither chart contract
12
10
  // leaves them out. App frames include the app contract through `cloud.chart()`.
@@ -31,12 +29,24 @@ export const chartNumberFormatSchema = () =>
31
29
  const dateFormatSchema = () => z.strictObject({ date: z.enum(["day", "minute", "second"]) });
32
30
  export const chartFormatSchema = () => z.union([chartNumberFormatSchema(), dateFormatSchema()]);
33
31
 
34
- export type ChartNumberFormat = z.output<ReturnType<typeof chartNumberFormatSchema>>;
35
- export type ChartDateFormat = z.output<ReturnType<typeof dateFormatSchema>>;
36
- export type ChartFormat = ChartNumberFormat | ChartDateFormat;
37
-
38
32
  export const chartKindSchema = () =>
39
- z.enum(["area", "bar", "line", "scatter", "pie", "donut", "histogram", "gauge", "sparkline", "boxplot", "heatmap", "barGauge", "stat"]);
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
+ ]);
40
50
 
41
51
  export const chartParts = (limits?: ChartLimits) => {
42
52
  const ChartLabel = limits ? z.string().trim().min(1).max(80) : z.union([z.string(), z.number().transform(String)]);
@@ -154,6 +164,27 @@ export const chartKinds = (limits?: ChartLimits) => {
154
164
  const ChartKindCommon = { ...ChartCommon, tooltip: ChartTooltip.optional() };
155
165
  const numberFormat = chartNumberFormatSchema();
156
166
  return z.discriminatedUnion("kind", [
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
+ }),
157
188
  z
158
189
  .object({
159
190
  kind: z.literal("area"),
@@ -175,17 +206,65 @@ export const chartKinds = (limits?: ChartLimits) => {
175
206
  if (!Number.isFinite(sum)) ctx.addIssue({ code: "custom", path: ["x", i], message: "Area totals must be finite." });
176
207
  });
177
208
  }),
178
- z.object({
179
- kind: z.literal("bar"),
180
- ...ChartKindCommon,
181
- data: list(ChartCategory, 1, limits?.maxValues),
182
- xAxis: ChartAxis.pick({ label: true }).strict().optional(),
183
- yAxis: ChartAxis.optional(),
184
- colorByBar: z.boolean().optional(),
185
- showValues: z.boolean().optional(),
186
- legend: z.boolean().optional(),
187
- references: ChartReferences.optional(),
188
- }),
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
+ }),
189
268
  z.object({
190
269
  kind: z.literal("line"),
191
270
  ...ChartKindCommon,
@@ -339,5 +418,10 @@ export const chartDateTime = (value: string): number | undefined => {
339
418
  return date.toISOString().startsWith(`${year}-${month}-${day}T${hour}:${minute}:${second}`) ? date.getTime() : undefined;
340
419
  };
341
420
 
342
- /** One chart as the shared renderers read it. */
343
- export type ChartInput = z.output<ReturnType<typeof chartKinds>>;
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
@@ -15,6 +15,8 @@ import {
15
15
  extendDomainToNice,
16
16
  gauge,
17
17
  type HeatmapChartOptions,
18
+ type HorizontalBarOptions,
19
+ hbarLayout,
18
20
  histogram,
19
21
  line,
20
22
  niceLogTicks,
@@ -22,10 +24,13 @@ import {
22
24
  pie,
23
25
  renderAreaSvg,
24
26
  renderBarChartSvg,
27
+ renderHbarSvg,
25
28
  renderHeatmapSvg,
29
+ renderWaterfallSvg,
26
30
  scatter,
27
31
  sparkline,
28
32
  stat,
33
+ type WaterfallOptions,
29
34
  } from "@k2b/ui/charts/core";
30
35
  import { z } from "zod";
31
36
  import { type ChartFormat, type ChartInput, type ChartNumberFormat, chartDateTime, chartKinds } from "./chart-schema";
@@ -33,15 +38,19 @@ import { type ChartFormat, type ChartInput, type ChartNumberFormat, chartDateTim
33
38
  export * from "./chart-schema";
34
39
 
35
40
  type RenderOptions = {
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);
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);
45
54
  }[ChartInput["kind"]];
46
55
 
47
56
  /**
@@ -57,7 +66,13 @@ const labels = /* @__PURE__ */ i18n.define({
57
66
  en: {
58
67
  category: "Category",
59
68
  series: "Series",
69
+ balance: "Balance",
70
+ before: "Balance before",
71
+ inflow: "In",
72
+ outflow: "Out",
73
+ after: "Balance after",
60
74
  value: "Value",
75
+ total: "Total",
61
76
  share: "Share",
62
77
  position: "Position",
63
78
  from: "From",
@@ -75,7 +90,13 @@ const labels = /* @__PURE__ */ i18n.define({
75
90
  de: {
76
91
  category: "Kategorie",
77
92
  series: "Datenreihe",
93
+ balance: "Stand",
94
+ before: "Stand davor",
95
+ inflow: "Rein",
96
+ outflow: "Raus",
97
+ after: "Stand danach",
78
98
  value: "Wert",
99
+ total: "Gesamt",
79
100
  share: "Anteil",
80
101
  position: "Position",
81
102
  from: "Von",
@@ -137,7 +158,19 @@ export const chartReferenceValues = (input: ChartInput, name: "x" | "y") =>
137
158
 
138
159
  /** The value axes of a chart with what they show: the data checks and the label room read them alike. */
139
160
  export const chartValueAxes = (input: ChartInput): { values: number[]; axis?: ChartAxisInput; zero?: boolean; name: "x" | "y" }[] => {
140
- if (input.kind === "bar") return [{ values: input.data.map((item) => item.value), axis: input.yAxis, zero: true, name: "y" }];
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
+ ];
141
174
  if (input.kind === "boxplot") return [{ values: input.data.flatMap((item) => item.values), axis: input.yAxis, name: "y" }];
142
175
  if (input.kind === "histogram") {
143
176
  if (!input.references?.length) return [{ values: input.data, name: "x" }];
@@ -246,13 +279,22 @@ const numberFormat = (format: ChartFormat | undefined) => (format && !("date" in
246
279
  /** The values a chart writes with its value format, for whole-number currency. */
247
280
  const formattedValues = (input: ChartInput): number[] => {
248
281
  switch (input.kind) {
282
+ case "waterfall":
283
+ return [input.start.value, ...input.steps.map((step) => step.value)];
249
284
  case "area":
250
285
  return input.series.flatMap((s) => s.data.filter((v): v is number => v !== null));
251
286
  case "line":
252
287
  case "scatter":
253
288
  return [...input.series.flatMap((series) => series.data.map((point) => point.y)), ...chartReferenceValues(input, "y")];
254
289
  case "bar":
255
- return [...input.data.map((item) => item.value), ...chartReferenceValues(input, "y")];
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));
256
298
  case "pie":
257
299
  case "donut":
258
300
  return input.data.map((item) => item.value);
@@ -323,6 +365,8 @@ export function chartLegend(
323
365
  input: ChartInput,
324
366
  locale: string,
325
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 [];
326
370
  if (input.kind === "bar")
327
371
  return input.colorByBar && input.legend ? input.data.map((entry, index) => ({ label: entry.label, color: index % 8 })) : [];
328
372
  if (input.kind === "area") return input.legend === false ? [] : input.series.map((s, i) => ({ label: s.label, color: i % 8 }));
@@ -367,6 +411,25 @@ export function chartRenderOptions(
367
411
  const width = options.width ?? WIDTH;
368
412
  const references = renderReferences(input);
369
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
+ };
370
433
  case "area":
371
434
  return {
372
435
  kind: "area",
@@ -386,14 +449,20 @@ export function chartRenderOptions(
386
449
  if (input.kind === "bar")
387
450
  return {
388
451
  kind: "bar",
452
+ locale,
389
453
  data: input.data,
454
+ x: input.x,
455
+ series: input.series,
456
+ layout: input.layout,
457
+ normalize: input.normalize,
458
+ compare: input.compare,
390
459
  xAxis: input.xAxis,
391
460
  yAxis: axis(yAxis, options.formats?.y ?? format.tick),
392
461
  // A host's own function format writes printed values like its ticks and table cells.
393
462
  valueFormat: options.formats?.value ?? options.formats?.y ?? format.value,
394
463
  colorByBar: input.colorByBar,
395
464
  showValues: input.showValues,
396
- legend: options.legend === "outside" ? false : input.legend,
465
+ legend: input.series ? input.legend : options.legend === "outside" ? false : input.legend,
397
466
  references,
398
467
  ...(options.width === undefined ? {} : { displayWidth: options.width }),
399
468
  };
@@ -545,6 +614,10 @@ const decode = (value: string) =>
545
614
  export const drawChart = (options: RenderOptions): string => {
546
615
  const common = { width: WIDTH, height: HEIGHT, inspect: true };
547
616
  switch (options.kind) {
617
+ case "hbar":
618
+ return renderHbarSvg({ ...options, ...common, height: hbarLayout(options).height });
619
+ case "waterfall":
620
+ return renderWaterfallSvg({ ...options, ...common });
548
621
  case "area":
549
622
  return renderAreaSvg({ ...options, ...common });
550
623
  case "bar":
@@ -594,7 +667,12 @@ export const chartMarkKey = (datum: Pick<ChartDatum, "index" | "seriesIndex"> &
594
667
  `${datum.role === "outlier" ? "outlier:" : datum.role === "value" ? "value:" : ""}${datum.seriesIndex ?? 0}:${datum.index}`;
595
668
 
596
669
  export type ChartColumn = { id: string; label: string; numeric: boolean };
597
- export type ChartRow = { key: string; cells: Record<string, string>; values: Record<string, string | number> };
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
+ };
598
676
  export type ChartTable = { columns: ChartColumn[]; rows: ChartRow[] };
599
677
 
600
678
  /** The first text column titles the tooltip; the other columns become rows, matching the table in every host. */
@@ -603,6 +681,7 @@ export function chartTooltip(
603
681
  row: ChartRow,
604
682
  kind?: ChartInput["kind"],
605
683
  ): { title?: string; rows: { label: string; value: string }[] } {
684
+ if (row.tooltip) return row.tooltip;
606
685
  if (kind === "area")
607
686
  return {
608
687
  title: row.cells.x,
@@ -644,8 +723,19 @@ export function chartRows(
644
723
  const unit = "unit" in input && input.unit ? ` ${input.unit}` : "";
645
724
  const columns = new Map<string, ChartColumn>();
646
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
+ }
647
736
  const named = series.some((entry) => entry.label);
648
737
  const add = (datum: ChartDatum): ChartRow => {
738
+ const tooltip = input.kind === "waterfall" ? { title: datum.label, rows: [] as { label: string; value: string }[] } : undefined;
649
739
  const cells: Record<string, string> = {};
650
740
  const values: Record<string, string | number> = {};
651
741
  const cell = (id: string, label: string, value: string | number, text: string) => {
@@ -663,7 +753,9 @@ export function chartRows(
663
753
  // Bars may hide some axis labels; the table names every category.
664
754
  const label =
665
755
  input.kind === "bar" || input.kind === "pie" || input.kind === "donut" || input.kind === "boxplot" || input.kind === "barGauge"
666
- ? input.data[datum.role === "outlier" ? (datum.seriesIndex ?? 0) : datum.index]?.label
756
+ ? ((input.kind === "bar" && input.series
757
+ ? undefined
758
+ : input.data[datum.role === "outlier" ? (datum.seriesIndex ?? 0) : datum.index]?.label) ?? datum.label)
667
759
  : datum.label;
668
760
  if (label)
669
761
  cell("label", input.kind === "heatmap" && input.layout === "calendar" ? calendarWords(locale).day : t.category, label, label);
@@ -678,8 +770,44 @@ export function chartRows(
678
770
  cell(field.key, field.key.toUpperCase(), value, value);
679
771
  continue;
680
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
+ }
681
790
  if (typeof value !== "number" || field.key === "total") continue;
682
- if (field.key === "x") {
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") {
683
811
  const header =
684
812
  (input.kind === "line" || input.kind === "scatter" ? input.xAxis?.label : undefined) ??
685
813
  (input.kind === "area" ? t.position : "X");
@@ -728,7 +856,7 @@ export function chartRows(
728
856
  cell(field.key, t[field.key], value, `${fieldFormat(value)}${field.key === "min" || field.key === "max" ? unit : ""}`);
729
857
  }
730
858
  }
731
- const row = { key: chartMarkKey(datum), cells, values };
859
+ const row = { key: chartMarkKey(datum), cells, values, ...(tooltip ? { tooltip } : {}) };
732
860
  rows.push(row);
733
861
  return row;
734
862
  };
@@ -745,11 +873,17 @@ export function chartTable(input: ChartInput, locale: string): ChartTable {
745
873
  /** Input values count against the budget, including observations summarized into boxes or bins. */
746
874
  export const chartMarks = (input: ChartInput): number => {
747
875
  switch (input.kind) {
876
+ case "waterfall":
877
+ return input.steps.length + 2;
748
878
  case "area":
749
879
  return input.series.reduce((sum, s) => sum + s.data.length, 0);
750
880
  case "line":
751
881
  case "scatter":
752
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;
753
887
  case "boxplot":
754
888
  return input.data.reduce((sum, box) => sum + box.values.length, 0);
755
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,8 +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;
460
461
  if (options.kind === "heatmap" && options.layout === "calendar") return 280;
461
- if (options.kind === "area") return 280;
462
+ if (options.kind === "waterfall" || options.kind === "area") return 280;
462
463
  if (options.kind !== "bar") return undefined;
463
464
  const layout = barChartLayout(options);
464
465
  return layout.layout === "rows" ? layout.height : undefined;