@assistant-ui/react-generative-ui 0.0.21 → 0.0.22

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.
Files changed (189) hide show
  1. package/LICENSE +1 -1
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.client.js +30 -13
  4. package/dist/JSONGenerativeUI.client.js.map +1 -1
  5. package/dist/RadioGroupScope.d.ts +6 -0
  6. package/dist/RadioGroupScope.d.ts.map +1 -0
  7. package/dist/RadioGroupScope.js +25 -0
  8. package/dist/RadioGroupScope.js.map +1 -0
  9. package/dist/a2ui/BindingContext.d.ts +11 -0
  10. package/dist/a2ui/BindingContext.d.ts.map +1 -0
  11. package/dist/a2ui/BindingContext.js +12 -0
  12. package/dist/a2ui/BindingContext.js.map +1 -0
  13. package/dist/a2ui/PresentRenderer.d.ts +11 -0
  14. package/dist/a2ui/PresentRenderer.d.ts.map +1 -0
  15. package/dist/a2ui/PresentRenderer.js +39 -0
  16. package/dist/a2ui/PresentRenderer.js.map +1 -0
  17. package/dist/a2ui/convert.d.ts +6 -0
  18. package/dist/a2ui/convert.d.ts.map +1 -1
  19. package/dist/a2ui/convert.js +344 -83
  20. package/dist/a2ui/convert.js.map +1 -1
  21. package/dist/a2ui/dataModel.d.ts +9 -0
  22. package/dist/a2ui/dataModel.d.ts.map +1 -0
  23. package/dist/a2ui/dataModel.js +44 -0
  24. package/dist/a2ui/dataModel.js.map +1 -0
  25. package/dist/a2ui/reducer.d.ts +5 -0
  26. package/dist/a2ui/reducer.d.ts.map +1 -1
  27. package/dist/a2ui/reducer.js +3 -3
  28. package/dist/a2ui/reducer.js.map +1 -1
  29. package/dist/a2ui/types.d.ts +8 -2
  30. package/dist/a2ui/types.d.ts.map +1 -1
  31. package/dist/a2ui/types.js.map +1 -1
  32. package/dist/a2ui/valueFunctions.d.ts +16 -0
  33. package/dist/a2ui/valueFunctions.d.ts.map +1 -0
  34. package/dist/a2ui/valueFunctions.js +332 -0
  35. package/dist/a2ui/valueFunctions.js.map +1 -0
  36. package/dist/actionRegistry.d.ts +1 -1
  37. package/dist/actionRegistry.js.map +1 -1
  38. package/dist/answeredValues.d.ts +16 -0
  39. package/dist/answeredValues.d.ts.map +1 -0
  40. package/dist/answeredValues.js +37 -0
  41. package/dist/answeredValues.js.map +1 -0
  42. package/dist/constants.d.ts +2 -0
  43. package/dist/constants.d.ts.map +1 -1
  44. package/dist/constants.js +3 -1
  45. package/dist/constants.js.map +1 -1
  46. package/dist/fieldReferences.d.ts +9 -0
  47. package/dist/fieldReferences.d.ts.map +1 -0
  48. package/dist/fieldReferences.js +53 -0
  49. package/dist/fieldReferences.js.map +1 -0
  50. package/dist/generativeUIToJSX.js +3 -3
  51. package/dist/generativeUIToJSX.js.map +1 -1
  52. package/dist/index.js +2 -2
  53. package/dist/index.server.js +2 -2
  54. package/dist/index.shared.js +2 -2
  55. package/dist/ir.d.ts +1 -1
  56. package/dist/ir.js.map +1 -1
  57. package/dist/renderGenerativeUI.d.ts.map +1 -1
  58. package/dist/renderGenerativeUI.js +12 -1
  59. package/dist/renderGenerativeUI.js.map +1 -1
  60. package/dist/slack/constants.d.ts +2 -0
  61. package/dist/slack/constants.d.ts.map +1 -1
  62. package/dist/slack/constants.js +3 -1
  63. package/dist/slack/constants.js.map +1 -1
  64. package/dist/slack/decodeBlockAction.d.ts +3 -1
  65. package/dist/slack/decodeBlockAction.d.ts.map +1 -1
  66. package/dist/slack/decodeBlockAction.js +58 -7
  67. package/dist/slack/decodeBlockAction.js.map +1 -1
  68. package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
  69. package/dist/slack/fromSlackBlocks.js +20 -3
  70. package/dist/slack/fromSlackBlocks.js.map +1 -1
  71. package/dist/slack/toSlackBlocks.d.ts.map +1 -1
  72. package/dist/slack/toSlackBlocks.js +187 -19
  73. package/dist/slack/toSlackBlocks.js.map +1 -1
  74. package/dist/slack/types.d.ts +4 -0
  75. package/dist/slack/types.d.ts.map +1 -1
  76. package/dist/teams/constants.d.ts +1 -1
  77. package/dist/teams/constants.d.ts.map +1 -1
  78. package/dist/teams/constants.js +1 -1
  79. package/dist/teams/constants.js.map +1 -1
  80. package/dist/teams/decodeSubmitData.d.ts +4 -0
  81. package/dist/teams/decodeSubmitData.d.ts.map +1 -1
  82. package/dist/teams/decodeSubmitData.js +6 -1
  83. package/dist/teams/decodeSubmitData.js.map +1 -1
  84. package/dist/teams/toAdaptiveCard.d.ts +5 -4
  85. package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
  86. package/dist/teams/toAdaptiveCard.js +56 -17
  87. package/dist/teams/toAdaptiveCard.js.map +1 -1
  88. package/dist/teams/types.d.ts +4 -1
  89. package/dist/teams/types.d.ts.map +1 -1
  90. package/dist/vocabulary/alert.d.ts +9 -2
  91. package/dist/vocabulary/alert.d.ts.map +1 -1
  92. package/dist/vocabulary/alert.js +116 -19
  93. package/dist/vocabulary/alert.js.map +1 -1
  94. package/dist/vocabulary/collectFormValues.d.ts +2 -1
  95. package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
  96. package/dist/vocabulary/collectFormValues.js +10 -3
  97. package/dist/vocabulary/collectFormValues.js.map +1 -1
  98. package/dist/vocabulary/data.d.ts +22 -5
  99. package/dist/vocabulary/data.d.ts.map +1 -1
  100. package/dist/vocabulary/data.js +278 -83
  101. package/dist/vocabulary/data.js.map +1 -1
  102. package/dist/vocabulary/dispatch.d.ts +3 -2
  103. package/dist/vocabulary/dispatch.d.ts.map +1 -1
  104. package/dist/vocabulary/dispatch.js +33 -4
  105. package/dist/vocabulary/dispatch.js.map +1 -1
  106. package/dist/vocabulary/fact.d.ts +8 -1
  107. package/dist/vocabulary/fact.d.ts.map +1 -1
  108. package/dist/vocabulary/fact.js +37 -11
  109. package/dist/vocabulary/fact.js.map +1 -1
  110. package/dist/vocabulary/form.d.ts.map +1 -1
  111. package/dist/vocabulary/form.js +3 -3
  112. package/dist/vocabulary/form.js.map +1 -1
  113. package/dist/vocabulary/formatValue.d.ts +7 -0
  114. package/dist/vocabulary/formatValue.d.ts.map +1 -0
  115. package/dist/vocabulary/formatValue.js +76 -0
  116. package/dist/vocabulary/formatValue.js.map +1 -0
  117. package/dist/vocabulary/icon.d.ts +1 -1
  118. package/dist/vocabulary/interactive.d.ts +137 -8
  119. package/dist/vocabulary/interactive.d.ts.map +1 -1
  120. package/dist/vocabulary/interactive.js +404 -103
  121. package/dist/vocabulary/interactive.js.map +1 -1
  122. package/dist/vocabulary/layout.d.ts.map +1 -1
  123. package/dist/vocabulary/layout.js +4 -4
  124. package/dist/vocabulary/layout.js.map +1 -1
  125. package/dist/vocabulary/list.js +2 -2
  126. package/dist/vocabulary/list.js.map +1 -1
  127. package/dist/vocabulary/text.d.ts +1 -1
  128. package/package.json +6 -3
  129. package/src/JSONGenerativeUI.client.tsx +84 -40
  130. package/src/JSONGenerativeUI.test.tsx +964 -68
  131. package/src/RadioGroupScope.server.test.tsx +24 -0
  132. package/src/RadioGroupScope.tsx +34 -0
  133. package/src/a2ui/BindingContext.ts +26 -0
  134. package/src/a2ui/PresentRenderer.tsx +83 -0
  135. package/src/a2ui/convert.test.ts +1795 -179
  136. package/src/a2ui/convert.ts +629 -132
  137. package/src/a2ui/dataModel.ts +69 -0
  138. package/src/a2ui/reducer.test.ts +35 -0
  139. package/src/a2ui/reducer.ts +5 -4
  140. package/src/a2ui/types.ts +9 -2
  141. package/src/a2ui/valueFunctions.test.ts +271 -0
  142. package/src/a2ui/valueFunctions.ts +523 -0
  143. package/src/actionRegistry.ts +1 -1
  144. package/src/answeredValues.test.ts +33 -0
  145. package/src/answeredValues.tsx +70 -0
  146. package/src/constants.ts +4 -0
  147. package/src/fieldReferences.test.ts +66 -0
  148. package/src/fieldReferences.ts +83 -0
  149. package/src/generativeUIToJSX.test.ts +18 -0
  150. package/src/generativeUIToJSX.ts +4 -6
  151. package/src/index.server.test.ts +106 -0
  152. package/src/ir.ts +1 -1
  153. package/src/renderGenerativeUI.test.tsx +14 -5
  154. package/src/renderGenerativeUI.tsx +30 -1
  155. package/src/slack/a2uiSlack.test.ts +198 -0
  156. package/src/slack/constants.ts +3 -0
  157. package/src/slack/decodeBlockAction.test.ts +98 -0
  158. package/src/slack/decodeBlockAction.ts +110 -17
  159. package/src/slack/fromSlackBlocks.test.ts +222 -3
  160. package/src/slack/fromSlackBlocks.ts +53 -2
  161. package/src/slack/toSlackBlocks.test.ts +682 -3
  162. package/src/slack/toSlackBlocks.ts +374 -23
  163. package/src/slack/types.ts +4 -0
  164. package/src/teams/constants.ts +1 -1
  165. package/src/teams/decodeSubmitData.test.ts +33 -0
  166. package/src/teams/decodeSubmitData.ts +9 -1
  167. package/src/teams/toAdaptiveCard.test.ts +219 -0
  168. package/src/teams/toAdaptiveCard.ts +93 -19
  169. package/src/teams/types.ts +4 -1
  170. package/src/vocabulary/alert.test.tsx +288 -7
  171. package/src/vocabulary/alert.tsx +168 -26
  172. package/src/vocabulary/collectFormValues.test.ts +28 -1
  173. package/src/vocabulary/collectFormValues.ts +25 -5
  174. package/src/vocabulary/data.test.tsx +248 -6
  175. package/src/vocabulary/data.tsx +398 -100
  176. package/src/vocabulary/dispatch.ts +63 -2
  177. package/src/vocabulary/fact.test.tsx +67 -0
  178. package/src/vocabulary/fact.tsx +40 -9
  179. package/src/vocabulary/form.tsx +7 -2
  180. package/src/vocabulary/formatValue.test.ts +72 -0
  181. package/src/vocabulary/formatValue.ts +127 -0
  182. package/src/vocabulary/index.test.ts +2 -0
  183. package/src/vocabulary/interactive.dispatch.test.tsx +167 -79
  184. package/src/vocabulary/interactive.dom.test.tsx +1699 -0
  185. package/src/vocabulary/interactive.test.tsx +60 -4
  186. package/src/vocabulary/interactive.tsx +770 -151
  187. package/src/vocabulary/layout.test.tsx +6 -4
  188. package/src/vocabulary/layout.tsx +13 -2
  189. package/src/vocabulary/list.tsx +2 -2
@@ -1,18 +1,41 @@
1
+ import * as React from "react";
1
2
  import { z } from "zod";
2
3
  import type { GenerativeUILibrary } from "../types";
4
+ import { formatValue, isNumericTableFormat } from "./formatValue";
3
5
  import { toTextContent } from "./toTextContent";
4
6
 
5
7
  const columnSchema = z.object({
6
8
  label: z.string().describe("Column header label."),
9
+ align: z.enum(["start", "end"]).optional().describe("Cell alignment."),
10
+ format: z
11
+ .object({
12
+ kind: z
13
+ .enum(["number", "currency", "percent", "date"])
14
+ .describe(
15
+ "Format kind. `percent` takes a fraction, so 0.25 renders as 25%.",
16
+ ),
17
+ currency: z.string().optional().describe("Currency code."),
18
+ decimals: z.number().optional().describe("Decimal places."),
19
+ })
20
+ .optional()
21
+ .describe("Cell value format."),
7
22
  });
8
23
 
9
24
  const cellSchema = z
10
25
  .union([z.string(), z.number(), z.boolean()])
11
26
  .describe("A cell value.");
12
27
 
13
- type TableColumn = { label: string };
28
+ type TableColumn = {
29
+ label: string;
30
+ align?: "start" | "end";
31
+ format?: unknown;
32
+ };
14
33
  type TableCell = string | number | boolean;
15
34
 
35
+ type TableSort = { column: number; direction: "ascending" | "descending" };
36
+
37
+ const tableCollator = new Intl.Collator("en-US", { numeric: true });
38
+
16
39
  const isTableColumn = (column: unknown): column is TableColumn =>
17
40
  column !== null &&
18
41
  typeof column === "object" &&
@@ -24,14 +47,155 @@ const isTableCell = (cell: unknown): cell is TableCell =>
24
47
  typeof cell === "number" ||
25
48
  typeof cell === "boolean";
26
49
 
50
+ const tableAlignment = (column: unknown): "start" | "end" | undefined => {
51
+ if (!isTableColumn(column)) return undefined;
52
+ if (column.align === "start" || column.align === "end") return column.align;
53
+ return isNumericTableFormat(column.format) ? "end" : undefined;
54
+ };
55
+
56
+ const compareTableCells = (left: unknown, right: unknown): number => {
57
+ if (
58
+ typeof left === "number" &&
59
+ Number.isFinite(left) &&
60
+ typeof right === "number" &&
61
+ Number.isFinite(right)
62
+ ) {
63
+ return left - right;
64
+ }
65
+ if (typeof left === "number" && Number.isFinite(left)) return -1;
66
+ if (typeof right === "number" && Number.isFinite(right)) return 1;
67
+ return tableCollator.compare(
68
+ isTableCell(left) ? String(left) : "",
69
+ isTableCell(right) ? String(right) : "",
70
+ );
71
+ };
72
+
73
+ type TableViewProps = {
74
+ columns: unknown;
75
+ rows: unknown;
76
+ children: React.ReactNode;
77
+ sort?: TableSort | undefined;
78
+ onSort?: ((column: number) => void) | undefined;
79
+ };
80
+
81
+ function TableView({ columns, rows, children, sort, onSort }: TableViewProps) {
82
+ const safeColumns = Array.isArray(columns) ? columns : [];
83
+ const hasColumns = safeColumns.some(isTableColumn);
84
+ const safeRows = Array.isArray(rows) ? rows.filter(Array.isArray) : [];
85
+ const sortedRows =
86
+ sort === undefined
87
+ ? safeRows
88
+ : safeRows
89
+ .map((row, index) => ({ row, index }))
90
+ .sort((left, right) => {
91
+ const comparison = compareTableCells(
92
+ left.row[sort.column],
93
+ right.row[sort.column],
94
+ );
95
+ return comparison === 0
96
+ ? left.index - right.index
97
+ : sort.direction === "ascending"
98
+ ? comparison
99
+ : -comparison;
100
+ })
101
+ .map(({ row }) => row);
102
+
103
+ return (
104
+ <table data-aui="table">
105
+ {hasColumns ? (
106
+ <thead>
107
+ <tr>
108
+ {safeColumns.map((column, i) => {
109
+ const label = isTableColumn(column) ? column.label : "";
110
+ const direction = sort?.column === i ? sort.direction : undefined;
111
+ return (
112
+ <th
113
+ key={i}
114
+ data-aui="table-col"
115
+ data-aui-align={tableAlignment(column)}
116
+ aria-sort={direction}
117
+ >
118
+ {onSort ? (
119
+ <button
120
+ type="button"
121
+ data-aui="table-sort"
122
+ onClick={() => onSort(i)}
123
+ >
124
+ {label}
125
+ {direction ? (
126
+ <span
127
+ aria-hidden="true"
128
+ data-aui="table-sort-direction"
129
+ >
130
+ {direction === "ascending" ? "↑" : "↓"}
131
+ </span>
132
+ ) : null}
133
+ </button>
134
+ ) : (
135
+ label
136
+ )}
137
+ </th>
138
+ );
139
+ })}
140
+ </tr>
141
+ </thead>
142
+ ) : null}
143
+ {safeRows.length ? (
144
+ <tbody>
145
+ {sortedRows.map((row, r) => (
146
+ <tr key={r}>
147
+ {row.map((cell, c) => (
148
+ <td key={c} data-aui-align={tableAlignment(safeColumns[c])}>
149
+ {formatValue(
150
+ cell,
151
+ isTableColumn(safeColumns[c])
152
+ ? safeColumns[c].format
153
+ : undefined,
154
+ )}
155
+ </td>
156
+ ))}
157
+ </tr>
158
+ ))}
159
+ </tbody>
160
+ ) : null}
161
+ {children}
162
+ </table>
163
+ );
164
+ }
165
+
166
+ function SortableTable(props: Omit<TableViewProps, "sort" | "onSort">) {
167
+ const [sort, setSort] = React.useState<TableSort | undefined>();
168
+ return (
169
+ <TableView
170
+ {...props}
171
+ sort={sort}
172
+ onSort={(column) =>
173
+ setSort((current) =>
174
+ current?.column !== column
175
+ ? { column, direction: "ascending" }
176
+ : current.direction === "ascending"
177
+ ? { column, direction: "descending" }
178
+ : undefined,
179
+ )
180
+ }
181
+ />
182
+ );
183
+ }
184
+
27
185
  const CHART_HEIGHT = 40;
28
186
  const CHART_WIDTH = 100;
29
187
 
30
188
  const clampValue = (value: unknown): number =>
31
- typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
189
+ typeof value === "number" && Number.isFinite(value) ? value : 0;
190
+
191
+ type ChartScale = { min: number; max: number };
32
192
 
33
- const yFor = (value: number, max: number): number =>
34
- max > 0 ? CHART_HEIGHT - (value / max) * CHART_HEIGHT : CHART_HEIGHT;
193
+ const yFor = (value: number, scale: ChartScale): number => {
194
+ const span = scale.max - scale.min;
195
+ return span > 0
196
+ ? CHART_HEIGHT - ((value - scale.min) / span) * CHART_HEIGHT
197
+ : CHART_HEIGHT;
198
+ };
35
199
 
36
200
  const pointSchema = z.object({
37
201
  label: z.string().optional().describe("Point label."),
@@ -40,13 +204,24 @@ const pointSchema = z.object({
40
204
 
41
205
  const seriesSchema = z.object({
42
206
  label: z.string().optional().describe("Series label, shown in the legend."),
207
+ color: z
208
+ .string()
209
+ .optional()
210
+ .describe(
211
+ "Series color. One of `emphasis`, `secondary`, `alpha-70`, `white`, `white-70`, or `white-50`, matching Text's `color` tokens; other values have no visual effect.",
212
+ ),
43
213
  data: z.array(pointSchema).describe("Data points for this series."),
44
214
  });
45
215
 
46
- type ChartPoint = { label?: string; value: number };
47
- type ChartSeriesInput = { label?: string; data: ChartPoint[] };
216
+ type ChartPoint = { label?: string | undefined; value: number };
217
+ type ChartSeriesInput = {
218
+ label?: string | undefined;
219
+ color?: string | undefined;
220
+ data?: ChartPoint[] | undefined;
221
+ };
48
222
  type NormalizedChartSeries = {
49
223
  label: string | undefined;
224
+ color: string | undefined;
50
225
  values: number[];
51
226
  labels: (string | undefined)[];
52
227
  };
@@ -77,6 +252,7 @@ function normalizeSeries(
77
252
  }
78
253
  return {
79
254
  label: typeof s?.label === "string" ? s.label : undefined,
255
+ color: typeof s?.color === "string" ? s.color : undefined,
80
256
  values,
81
257
  labels,
82
258
  };
@@ -85,23 +261,38 @@ function normalizeSeries(
85
261
  return { count, series };
86
262
  }
87
263
 
88
- function computeMax(
264
+ function valueScale(values: number[]): ChartScale {
265
+ let min = 0;
266
+ let max = 0;
267
+ for (const value of values) {
268
+ min = Math.min(min, value);
269
+ max = Math.max(max, value);
270
+ }
271
+ return { min, max };
272
+ }
273
+
274
+ function computeScale(
89
275
  series: NormalizedChartSeries[],
90
276
  count: number,
91
277
  stacked: boolean,
92
- ): number {
278
+ ): ChartScale {
93
279
  if (stacked) {
280
+ let min = 0;
94
281
  let max = 0;
95
282
  for (let i = 0; i < count; i++) {
96
- let sum = 0;
97
- for (const s of series) sum += s.values[i] ?? 0;
98
- max = Math.max(max, sum);
283
+ let negative = 0;
284
+ let positive = 0;
285
+ for (const s of series) {
286
+ const value = s.values[i] ?? 0;
287
+ if (value < 0) negative += value;
288
+ else positive += value;
289
+ }
290
+ min = Math.min(min, negative);
291
+ max = Math.max(max, positive);
99
292
  }
100
- return max;
293
+ return { min, max };
101
294
  }
102
- let max = 0;
103
- for (const s of series) for (const v of s.values) max = Math.max(max, v);
104
- return max;
295
+ return valueScale(series.flatMap((s) => s.values));
105
296
  }
106
297
 
107
298
  const NICE_MULTIPLES = [1, 2, 5];
@@ -119,11 +310,27 @@ function niceMax(max: number): number {
119
310
 
120
311
  const TICK_COUNT = 5;
121
312
 
122
- /** `TICK_COUNT` evenly spaced ticks from `max` down to `0`, for a top-to-bottom y-axis column. */
123
- function tickValues(max: number): number[] {
313
+ function axisScale(scale: ChartScale): ChartScale {
314
+ if (scale.min === 0) return { min: 0, max: niceMax(scale.max) };
315
+ if (scale.max === 0) return { min: -niceMax(-scale.min), max: 0 };
316
+ const step = niceMax(Math.max(-scale.min, scale.max) / 2);
317
+ return { min: -2 * step, max: 2 * step };
318
+ }
319
+
320
+ /** `TICK_COUNT` evenly spaced ticks from the top of the scale down to its bottom, for a top-to-bottom y-axis column. */
321
+ function tickValues(scale: ChartScale): number[] {
124
322
  const ticks: number[] = [];
323
+ if (scale.min === 0) {
324
+ for (let i = 0; i < TICK_COUNT; i++) {
325
+ ticks.push((scale.max * (TICK_COUNT - 1 - i)) / (TICK_COUNT - 1));
326
+ }
327
+ return ticks;
328
+ }
125
329
  for (let i = 0; i < TICK_COUNT; i++) {
126
- ticks.push((max * (TICK_COUNT - 1 - i)) / (TICK_COUNT - 1));
330
+ ticks.push(
331
+ scale.min +
332
+ ((scale.max - scale.min) * (TICK_COUNT - 1 - i)) / (TICK_COUNT - 1),
333
+ );
127
334
  }
128
335
  return ticks;
129
336
  }
@@ -136,29 +343,84 @@ type ChartVariant = "bar" | "line" | "sparkline" | "area";
136
343
 
137
344
  const SERIES_PALETTE_SIZE = 5;
138
345
 
346
+ const tooltipText = (
347
+ seriesLabel: string | undefined,
348
+ pointLabel: string | undefined,
349
+ index: number,
350
+ value: number,
351
+ ) =>
352
+ `${seriesLabel ? `${seriesLabel}, ` : ""}${pointLabel || index + 1}: ${formatTick(value)}`;
353
+
354
+ const seriesColor = (
355
+ series: NormalizedChartSeries,
356
+ seriesCount: number,
357
+ color: unknown,
358
+ ) =>
359
+ series.color ??
360
+ (seriesCount === 1 && typeof color === "string" ? color : undefined);
361
+
362
+ const xFor = (index: number, count: number): number =>
363
+ count === 1 ? CHART_WIDTH / 2 : (index / (count - 1)) * CHART_WIDTH;
364
+
365
+ function pointTargets(
366
+ series: NormalizedChartSeries,
367
+ positions: number[],
368
+ count: number,
369
+ scale: ChartScale,
370
+ ) {
371
+ return series.values.map((value, i) => (
372
+ <circle
373
+ key={i}
374
+ data-aui="chart-point"
375
+ cx={xFor(i, count)}
376
+ cy={yFor(positions[i] ?? 0, scale)}
377
+ r={4}
378
+ fill="transparent"
379
+ >
380
+ <title>{tooltipText(series.label, series.labels[i], i, value)}</title>
381
+ </circle>
382
+ ));
383
+ }
384
+
385
+ function barGeometry(value: number, start: number, scale: ChartScale) {
386
+ const end = start + value;
387
+ if (scale.min === 0 && start >= 0 && end >= 0) {
388
+ const height = scale.max > 0 ? (value / scale.max) * CHART_HEIGHT : 0;
389
+ const belowHeight = scale.max > 0 ? (start / scale.max) * CHART_HEIGHT : 0;
390
+ return { y: CHART_HEIGHT - belowHeight - height, height };
391
+ }
392
+ const startY = yFor(start, scale);
393
+ const endY = yFor(end, scale);
394
+ return { y: Math.min(startY, endY), height: Math.abs(endY - startY) };
395
+ }
396
+
139
397
  function renderSeriesMarks(
140
398
  variant: ChartVariant,
141
399
  series: NormalizedChartSeries[],
142
400
  count: number,
143
- scaleMax: number,
401
+ scale: ChartScale,
144
402
  stacked: boolean,
403
+ color: unknown,
145
404
  ) {
405
+ const seriesCount = series.length;
406
+
146
407
  if (variant === "bar") {
147
408
  const slot = count > 0 ? CHART_WIDTH / count : 0;
148
409
  const gap = slot * 0.2;
149
410
  const groupWidth = slot - gap;
150
- const seriesCount = series.length;
151
411
  const perSeriesWidth =
152
412
  stacked || seriesCount <= 1 ? groupWidth : groupWidth / seriesCount;
153
- const cumulative = new Array(count).fill(0) as number[];
413
+ const negatives = new Array(count).fill(0) as number[];
414
+ const positives = new Array(count).fill(0) as number[];
154
415
 
155
416
  return series.map((s, seriesIndex) => {
156
417
  const marks = s.values.map((value, i) => {
157
- const height = scaleMax > 0 ? (value / scaleMax) * CHART_HEIGHT : 0;
158
- const belowHeight =
159
- stacked && scaleMax > 0
160
- ? ((cumulative[i] ?? 0) / scaleMax) * CHART_HEIGHT
161
- : 0;
418
+ const start = stacked
419
+ ? value < 0
420
+ ? (negatives[i] ?? 0)
421
+ : (positives[i] ?? 0)
422
+ : 0;
423
+ const { y, height } = barGeometry(value, start, scale);
162
424
  const x =
163
425
  stacked || seriesCount <= 1
164
426
  ? i * slot + gap / 2
@@ -167,16 +429,20 @@ function renderSeriesMarks(
167
429
  <rect
168
430
  key={i}
169
431
  x={x}
170
- y={CHART_HEIGHT - belowHeight - height}
432
+ y={y}
171
433
  width={perSeriesWidth}
172
434
  height={height}
173
435
  fill="currentColor"
174
- />
436
+ >
437
+ <title>{tooltipText(s.label, s.labels[i], i, value)}</title>
438
+ </rect>
175
439
  );
176
440
  });
177
441
  if (stacked) {
178
442
  for (let i = 0; i < count; i++) {
179
- cumulative[i] = (cumulative[i] ?? 0) + (s.values[i] ?? 0);
443
+ const value = s.values[i] ?? 0;
444
+ if (value < 0) negatives[i] = (negatives[i] ?? 0) + value;
445
+ else positives[i] = (positives[i] ?? 0) + value;
180
446
  }
181
447
  }
182
448
  return (
@@ -184,6 +450,7 @@ function renderSeriesMarks(
184
450
  key={seriesIndex}
185
451
  data-aui="chart-series"
186
452
  data-aui-series={seriesIndex % SERIES_PALETTE_SIZE}
453
+ data-aui-color={seriesColor(s, seriesCount, color)}
187
454
  >
188
455
  {marks}
189
456
  </g>
@@ -192,28 +459,35 @@ function renderSeriesMarks(
192
459
  }
193
460
 
194
461
  if (variant === "area") {
195
- const cumulative = new Array(count).fill(0) as number[];
462
+ const negatives = new Array(count).fill(0) as number[];
463
+ const positives = new Array(count).fill(0) as number[];
196
464
 
197
465
  return series.map((s, seriesIndex) => {
198
- const bottoms = stacked
199
- ? cumulative.slice()
200
- : (new Array(count).fill(0) as number[]);
466
+ const bottoms = s.values.map((value, i) =>
467
+ stacked ? (value < 0 ? (negatives[i] ?? 0) : (positives[i] ?? 0)) : 0,
468
+ );
469
+ const tops = s.values.map((value, i) =>
470
+ stacked ? (bottoms[i] ?? 0) + value : value,
471
+ );
201
472
  if (stacked) {
202
473
  for (let i = 0; i < count; i++) {
203
- cumulative[i] = (cumulative[i] ?? 0) + (s.values[i] ?? 0);
474
+ const value = s.values[i] ?? 0;
475
+ if (value < 0) negatives[i] = (negatives[i] ?? 0) + value;
476
+ else positives[i] = (positives[i] ?? 0) + value;
204
477
  }
205
478
  }
206
- const tops = stacked ? cumulative.slice() : s.values.slice();
207
479
 
208
480
  if (count === 1) {
209
- const y = yFor(tops[0] ?? 0, scaleMax);
481
+ const y = yFor(tops[0] ?? 0, scale);
210
482
  return (
211
483
  <g
212
484
  key={seriesIndex}
213
485
  data-aui="chart-series"
214
486
  data-aui-series={seriesIndex % SERIES_PALETTE_SIZE}
487
+ data-aui-color={seriesColor(s, seriesCount, color)}
215
488
  >
216
489
  <circle cx={CHART_WIDTH / 2} cy={y} r={2} fill="currentColor" />
490
+ {pointTargets(s, tops, count, scale)}
217
491
  </g>
218
492
  );
219
493
  }
@@ -223,17 +497,16 @@ function renderSeriesMarks(
223
497
  key={seriesIndex}
224
498
  data-aui="chart-series"
225
499
  data-aui-series={seriesIndex % SERIES_PALETTE_SIZE}
500
+ data-aui-color={seriesColor(s, seriesCount, color)}
226
501
  />
227
502
  );
228
503
  }
229
504
 
230
505
  const topPoints = tops.map(
231
- (v, i) => `${(i / (count - 1)) * CHART_WIDTH},${yFor(v, scaleMax)}`,
506
+ (v, i) => `${xFor(i, count)},${yFor(v, scale)}`,
232
507
  );
233
508
  const bottomPoints = bottoms
234
- .map(
235
- (v, i) => `${(i / (count - 1)) * CHART_WIDTH},${yFor(v, scaleMax)}`,
236
- )
509
+ .map((v, i) => `${xFor(i, count)},${yFor(v, scale)}`)
237
510
  .reverse();
238
511
 
239
512
  return (
@@ -241,6 +514,7 @@ function renderSeriesMarks(
241
514
  key={seriesIndex}
242
515
  data-aui="chart-series"
243
516
  data-aui-series={seriesIndex % SERIES_PALETTE_SIZE}
517
+ data-aui-color={seriesColor(s, seriesCount, color)}
244
518
  >
245
519
  <polygon
246
520
  points={[...topPoints, ...bottomPoints].join(" ")}
@@ -253,6 +527,7 @@ function renderSeriesMarks(
253
527
  stroke="currentColor"
254
528
  vectorEffect="non-scaling-stroke"
255
529
  />
530
+ {pointTargets(s, tops, count, scale)}
256
531
  </g>
257
532
  );
258
533
  });
@@ -264,27 +539,26 @@ function renderSeriesMarks(
264
539
  key={seriesIndex}
265
540
  data-aui="chart-series"
266
541
  data-aui-series={seriesIndex % SERIES_PALETTE_SIZE}
542
+ data-aui-color={seriesColor(s, series.length, color)}
267
543
  >
268
544
  {count === 1 ? (
269
545
  <circle
270
546
  cx={CHART_WIDTH / 2}
271
- cy={yFor(s.values[0] ?? 0, scaleMax)}
547
+ cy={yFor(s.values[0] ?? 0, scale)}
272
548
  r={2}
273
549
  fill="currentColor"
274
550
  />
275
551
  ) : count > 1 ? (
276
552
  <polyline
277
553
  points={s.values
278
- .map(
279
- (v, i) =>
280
- `${(i / (count - 1)) * CHART_WIDTH},${yFor(v, scaleMax)}`,
281
- )
554
+ .map((v, i) => `${xFor(i, count)},${yFor(v, scale)}`)
282
555
  .join(" ")}
283
556
  fill="none"
284
557
  stroke="currentColor"
285
558
  vectorEffect="non-scaling-stroke"
286
559
  />
287
560
  ) : null}
561
+ {variant === "line" ? pointTargets(s, s.values, count, scale) : null}
288
562
  </g>
289
563
  ));
290
564
  }
@@ -296,40 +570,18 @@ export const dataVocabulary = {
296
570
  properties: z.object({
297
571
  columns: z.array(columnSchema).optional().describe("Column definitions."),
298
572
  rows: z.array(z.array(cellSchema)).optional().describe("Rows of cells."),
573
+ sortable: z.boolean().optional().describe("Allow header sorting."),
299
574
  }),
300
- render: ({ columns, rows, children }) => {
301
- const safeColumns = Array.isArray(columns) ? columns : [];
302
- const hasColumns = safeColumns.some(isTableColumn);
303
- const safeRows = Array.isArray(rows) ? rows.filter(Array.isArray) : [];
304
-
305
- return (
306
- <table data-aui="table">
307
- {hasColumns ? (
308
- <thead>
309
- <tr>
310
- {safeColumns.map((column, i) => (
311
- <th key={i} data-aui="table-col">
312
- {isTableColumn(column) ? column.label : ""}
313
- </th>
314
- ))}
315
- </tr>
316
- </thead>
317
- ) : null}
318
- {safeRows.length ? (
319
- <tbody>
320
- {safeRows.map((row, r) => (
321
- <tr key={r}>
322
- {row.map((cell, c) => (
323
- <td key={c}>{isTableCell(cell) ? String(cell) : ""}</td>
324
- ))}
325
- </tr>
326
- ))}
327
- </tbody>
328
- ) : null}
575
+ render: ({ columns, rows, sortable, children }) =>
576
+ sortable === true ? (
577
+ <SortableTable columns={columns} rows={rows}>
329
578
  {children}
330
- </table>
331
- );
332
- },
579
+ </SortableTable>
580
+ ) : (
581
+ <TableView columns={columns} rows={rows}>
582
+ {children}
583
+ </TableView>
584
+ ),
333
585
  },
334
586
  Markdown: {
335
587
  description:
@@ -403,10 +655,19 @@ export const dataVocabulary = {
403
655
  const points = Array.isArray(data) ? data : [];
404
656
  const n = points.length;
405
657
  const values = points.map((d) => clampValue(d?.value));
406
- const max = values.reduce((m, v) => Math.max(m, v), 0);
658
+ const scale = valueScale(values);
659
+ const chartSeries: NormalizedChartSeries = {
660
+ label: undefined,
661
+ color: undefined,
662
+ values,
663
+ labels: points.map((point) =>
664
+ typeof point?.label === "string" ? point.label : undefined,
665
+ ),
666
+ };
407
667
  const slot = n > 0 ? CHART_WIDTH / n : 0;
408
668
  const gap = slot * 0.2;
409
669
  const barWidth = slot - gap;
670
+ const crossesZero = scale.min < 0 && scale.max > 0;
410
671
 
411
672
  return (
412
673
  <svg
@@ -418,37 +679,60 @@ export const dataVocabulary = {
418
679
  viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`}
419
680
  preserveAspectRatio="none"
420
681
  >
682
+ {crossesZero ? (
683
+ <line
684
+ data-aui="chart-zero"
685
+ x1={0}
686
+ x2={CHART_WIDTH}
687
+ y1={yFor(0, scale)}
688
+ y2={yFor(0, scale)}
689
+ stroke="currentColor"
690
+ vectorEffect="non-scaling-stroke"
691
+ />
692
+ ) : null}
421
693
  {variant === "bar" ? (
422
694
  values.map((v, i) => {
423
- const height = max > 0 ? (v / max) * CHART_HEIGHT : 0;
695
+ const { y, height } = barGeometry(v, 0, scale);
424
696
  return (
425
697
  <rect
426
698
  key={i}
427
699
  x={i * slot + gap / 2}
428
- y={CHART_HEIGHT - height}
700
+ y={y}
429
701
  width={barWidth}
430
702
  height={height}
431
703
  fill="currentColor"
432
- />
704
+ >
705
+ <title>
706
+ {tooltipText(undefined, chartSeries.labels[i], i, v)}
707
+ </title>
708
+ </rect>
433
709
  );
434
710
  })
435
711
  ) : n === 1 ? (
436
- <circle
437
- cx={CHART_WIDTH / 2}
438
- cy={yFor(values[0] ?? 0, max)}
439
- r={2}
440
- fill="currentColor"
441
- />
712
+ <>
713
+ <circle
714
+ cx={CHART_WIDTH / 2}
715
+ cy={yFor(values[0] ?? 0, scale)}
716
+ r={2}
717
+ fill="currentColor"
718
+ />
719
+ {variant === "line"
720
+ ? pointTargets(chartSeries, values, n, scale)
721
+ : null}
722
+ </>
442
723
  ) : n > 1 ? (
443
- <polyline
444
- points={values
445
- .map(
446
- (v, i) => `${(i / (n - 1)) * CHART_WIDTH},${yFor(v, max)}`,
447
- )
448
- .join(" ")}
449
- fill="none"
450
- stroke="currentColor"
451
- />
724
+ <>
725
+ <polyline
726
+ points={values
727
+ .map((v, i) => `${xFor(i, n)},${yFor(v, scale)}`)
728
+ .join(" ")}
729
+ fill="none"
730
+ stroke="currentColor"
731
+ />
732
+ {variant === "line"
733
+ ? pointTargets(chartSeries, values, n, scale)
734
+ : null}
735
+ </>
452
736
  ) : null}
453
737
  </svg>
454
738
  );
@@ -457,15 +741,17 @@ export const dataVocabulary = {
457
741
  const { count, series: normalized } = normalizeSeries(series, data);
458
742
  const stackedScale =
459
743
  !!stacked && (variant === "bar" || variant === "area");
460
- const rawMax = computeMax(normalized, count, stackedScale);
461
- const scaleMax = showAxis ? niceMax(rawMax) : rawMax;
744
+ const rawScale = computeScale(normalized, count, stackedScale);
745
+ const scale = showAxis ? axisScale(rawScale) : rawScale;
462
746
  const marks = renderSeriesMarks(
463
747
  variant,
464
748
  normalized,
465
749
  count,
466
- scaleMax,
750
+ scale,
467
751
  stackedScale,
752
+ color,
468
753
  );
754
+ const crossesZero = rawScale.min < 0 && rawScale.max > 0;
469
755
 
470
756
  const svg = (
471
757
  <svg
@@ -477,6 +763,17 @@ export const dataVocabulary = {
477
763
  viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`}
478
764
  preserveAspectRatio="none"
479
765
  >
766
+ {crossesZero ? (
767
+ <line
768
+ data-aui="chart-zero"
769
+ x1={0}
770
+ x2={CHART_WIDTH}
771
+ y1={yFor(0, scale)}
772
+ y2={yFor(0, scale)}
773
+ stroke="currentColor"
774
+ vectorEffect="non-scaling-stroke"
775
+ />
776
+ ) : null}
480
777
  {marks}
481
778
  </svg>
482
779
  );
@@ -487,7 +784,7 @@ export const dataVocabulary = {
487
784
  <div data-aui="chart-frame">
488
785
  {showAxis ? (
489
786
  <div data-aui="chart-ticks">
490
- {tickValues(scaleMax).map((tick, i) => (
787
+ {tickValues(scale).map((tick, i) => (
491
788
  <div key={i}>{formatTick(tick)}</div>
492
789
  ))}
493
790
  </div>
@@ -507,6 +804,7 @@ export const dataVocabulary = {
507
804
  key={i}
508
805
  data-aui="chart-legend-item"
509
806
  data-aui-series={i % SERIES_PALETTE_SIZE}
807
+ data-aui-color={seriesColor(s, normalized.length, color)}
510
808
  >
511
809
  <span data-aui="chart-legend-swatch" />
512
810
  {s.label ?? `Series ${i + 1}`}