@seed-ship/mcp-ui-solid 6.23.0 → 6.25.1

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 (172) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/README.md +154 -2
  3. package/dist/components/ActionGroupRenderer.cjs +6 -6
  4. package/dist/components/ActionGroupRenderer.cjs.map +1 -1
  5. package/dist/components/ActionGroupRenderer.js +6 -6
  6. package/dist/components/ActionGroupRenderer.js.map +1 -1
  7. package/dist/components/AutocompleteDropdown.cjs +6 -6
  8. package/dist/components/AutocompleteDropdown.cjs.map +1 -1
  9. package/dist/components/AutocompleteDropdown.js +6 -6
  10. package/dist/components/AutocompleteDropdown.js.map +1 -1
  11. package/dist/components/AutocompleteFormField.cjs +6 -5
  12. package/dist/components/AutocompleteFormField.cjs.map +1 -1
  13. package/dist/components/AutocompleteFormField.d.ts.map +1 -1
  14. package/dist/components/AutocompleteFormField.js +6 -5
  15. package/dist/components/AutocompleteFormField.js.map +1 -1
  16. package/dist/components/ChartJSRenderer.cjs +406 -70
  17. package/dist/components/ChartJSRenderer.cjs.map +1 -1
  18. package/dist/components/ChartJSRenderer.d.ts.map +1 -1
  19. package/dist/components/ChartJSRenderer.js +407 -71
  20. package/dist/components/ChartJSRenderer.js.map +1 -1
  21. package/dist/components/ChatPrompt.cjs +5 -5
  22. package/dist/components/ChatPrompt.cjs.map +1 -1
  23. package/dist/components/ChatPrompt.js +5 -5
  24. package/dist/components/ChatPrompt.js.map +1 -1
  25. package/dist/components/ComponentToolbar.cjs +5 -5
  26. package/dist/components/ComponentToolbar.cjs.map +1 -1
  27. package/dist/components/ComponentToolbar.js +5 -5
  28. package/dist/components/ComponentToolbar.js.map +1 -1
  29. package/dist/components/DegradedFallback.cjs +35 -3
  30. package/dist/components/DegradedFallback.cjs.map +1 -1
  31. package/dist/components/DegradedFallback.d.ts +7 -0
  32. package/dist/components/DegradedFallback.d.ts.map +1 -1
  33. package/dist/components/DegradedFallback.js +35 -3
  34. package/dist/components/DegradedFallback.js.map +1 -1
  35. package/dist/components/ExpandableWrapper.cjs +10 -7
  36. package/dist/components/ExpandableWrapper.cjs.map +1 -1
  37. package/dist/components/ExpandableWrapper.d.ts.map +1 -1
  38. package/dist/components/ExpandableWrapper.js +10 -7
  39. package/dist/components/ExpandableWrapper.js.map +1 -1
  40. package/dist/components/FormFieldRenderer.cjs +13 -12
  41. package/dist/components/FormFieldRenderer.cjs.map +1 -1
  42. package/dist/components/FormFieldRenderer.d.ts.map +1 -1
  43. package/dist/components/FormFieldRenderer.js +13 -12
  44. package/dist/components/FormFieldRenderer.js.map +1 -1
  45. package/dist/components/FormRenderer.cjs +1 -1
  46. package/dist/components/FormRenderer.cjs.map +1 -1
  47. package/dist/components/FormRenderer.js +1 -1
  48. package/dist/components/FormRenderer.js.map +1 -1
  49. package/dist/components/GenerativeUIErrorBoundary.cjs +1 -1
  50. package/dist/components/GenerativeUIErrorBoundary.cjs.map +1 -1
  51. package/dist/components/GenerativeUIErrorBoundary.js +1 -1
  52. package/dist/components/GenerativeUIErrorBoundary.js.map +1 -1
  53. package/dist/components/GhostText.cjs +2 -2
  54. package/dist/components/GhostText.cjs.map +1 -1
  55. package/dist/components/GhostText.js +2 -2
  56. package/dist/components/GhostText.js.map +1 -1
  57. package/dist/components/GraphRenderer.cjs +100 -29
  58. package/dist/components/GraphRenderer.cjs.map +1 -1
  59. package/dist/components/GraphRenderer.d.ts +1 -1
  60. package/dist/components/GraphRenderer.d.ts.map +1 -1
  61. package/dist/components/GraphRenderer.js +100 -29
  62. package/dist/components/GraphRenderer.js.map +1 -1
  63. package/dist/components/MissingValue.cjs +43 -0
  64. package/dist/components/MissingValue.cjs.map +1 -0
  65. package/dist/components/MissingValue.d.ts +27 -0
  66. package/dist/components/MissingValue.d.ts.map +1 -0
  67. package/dist/components/MissingValue.js +43 -0
  68. package/dist/components/MissingValue.js.map +1 -0
  69. package/dist/components/ModalRenderer.cjs +39 -31
  70. package/dist/components/ModalRenderer.cjs.map +1 -1
  71. package/dist/components/ModalRenderer.d.ts.map +1 -1
  72. package/dist/components/ModalRenderer.js +40 -32
  73. package/dist/components/ModalRenderer.js.map +1 -1
  74. package/dist/components/PortalDropdownMenu.cjs +6 -3
  75. package/dist/components/PortalDropdownMenu.cjs.map +1 -1
  76. package/dist/components/PortalDropdownMenu.d.ts.map +1 -1
  77. package/dist/components/PortalDropdownMenu.js +7 -4
  78. package/dist/components/PortalDropdownMenu.js.map +1 -1
  79. package/dist/components/ScratchpadPanel.cjs +38 -44
  80. package/dist/components/ScratchpadPanel.cjs.map +1 -1
  81. package/dist/components/ScratchpadPanel.d.ts.map +1 -1
  82. package/dist/components/ScratchpadPanel.js +38 -44
  83. package/dist/components/ScratchpadPanel.js.map +1 -1
  84. package/dist/components/UIResourceRenderer.cjs +255 -217
  85. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  86. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  87. package/dist/components/UIResourceRenderer.js +255 -217
  88. package/dist/components/UIResourceRenderer.js.map +1 -1
  89. package/dist/components/chart-data-table.cjs +30 -5
  90. package/dist/components/chart-data-table.cjs.map +1 -1
  91. package/dist/components/chart-data-table.d.ts +18 -1
  92. package/dist/components/chart-data-table.d.ts.map +1 -1
  93. package/dist/components/chart-data-table.js +30 -5
  94. package/dist/components/chart-data-table.js.map +1 -1
  95. package/dist/components/chart-missing-plugin.cjs +194 -0
  96. package/dist/components/chart-missing-plugin.cjs.map +1 -0
  97. package/dist/components/chart-missing-plugin.d.ts +129 -0
  98. package/dist/components/chart-missing-plugin.d.ts.map +1 -0
  99. package/dist/components/chart-missing-plugin.js +194 -0
  100. package/dist/components/chart-missing-plugin.js.map +1 -0
  101. package/dist/context/MCPUIStringsContext.cjs +12 -1
  102. package/dist/context/MCPUIStringsContext.cjs.map +1 -1
  103. package/dist/context/MCPUIStringsContext.d.ts +62 -0
  104. package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
  105. package/dist/context/MCPUIStringsContext.js +12 -1
  106. package/dist/context/MCPUIStringsContext.js.map +1 -1
  107. package/dist/services/component-registry.cjs +9 -2
  108. package/dist/services/component-registry.cjs.map +1 -1
  109. package/dist/services/component-registry.d.ts.map +1 -1
  110. package/dist/services/component-registry.js +9 -2
  111. package/dist/services/component-registry.js.map +1 -1
  112. package/dist/services/validation.cjs +31 -8
  113. package/dist/services/validation.cjs.map +1 -1
  114. package/dist/services/validation.d.ts.map +1 -1
  115. package/dist/services/validation.js +31 -8
  116. package/dist/services/validation.js.map +1 -1
  117. package/dist/types/index.d.ts +14 -2
  118. package/dist/types/index.d.ts.map +1 -1
  119. package/dist/utils/degraded-projections.cjs +19 -3
  120. package/dist/utils/degraded-projections.cjs.map +1 -1
  121. package/dist/utils/degraded-projections.d.ts +22 -2
  122. package/dist/utils/degraded-projections.d.ts.map +1 -1
  123. package/dist/utils/degraded-projections.js +19 -3
  124. package/dist/utils/degraded-projections.js.map +1 -1
  125. package/dist/utils/math-markdown.cjs +1 -1
  126. package/dist/utils/math-markdown.cjs.map +1 -1
  127. package/dist/utils/math-markdown.js +1 -1
  128. package/dist/utils/math-markdown.js.map +1 -1
  129. package/dist/utils/missing-value.cjs +60 -0
  130. package/dist/utils/missing-value.cjs.map +1 -0
  131. package/dist/utils/missing-value.d.ts +79 -0
  132. package/dist/utils/missing-value.d.ts.map +1 -0
  133. package/dist/utils/missing-value.js +60 -0
  134. package/dist/utils/missing-value.js.map +1 -0
  135. package/dist/utils/theme.cjs +91 -0
  136. package/dist/utils/theme.cjs.map +1 -0
  137. package/dist/utils/theme.d.ts +11 -0
  138. package/dist/utils/theme.d.ts.map +1 -0
  139. package/dist/utils/theme.js +91 -0
  140. package/dist/utils/theme.js.map +1 -0
  141. package/docs/SECURITY-DEPENDENCIES.md +81 -0
  142. package/docs/THEMING.md +205 -0
  143. package/package.json +7 -5
  144. package/src/components/ActionGroupRenderer.tsx +6 -6
  145. package/src/components/AutocompleteDropdown.tsx +17 -17
  146. package/src/components/AutocompleteFormField.tsx +10 -9
  147. package/src/components/ChartJSRenderer.tsx +609 -212
  148. package/src/components/ChatPrompt.tsx +16 -16
  149. package/src/components/ComponentToolbar.tsx +5 -5
  150. package/src/components/DegradedFallback.tsx +33 -11
  151. package/src/components/ExpandableWrapper.tsx +15 -11
  152. package/src/components/FormFieldRenderer.tsx +52 -51
  153. package/src/components/FormRenderer.tsx +12 -12
  154. package/src/components/GenerativeUIErrorBoundary.tsx +6 -6
  155. package/src/components/GhostText.tsx +7 -7
  156. package/src/components/GraphRenderer.tsx +83 -67
  157. package/src/components/MissingValue.tsx +61 -0
  158. package/src/components/ModalRenderer.tsx +11 -5
  159. package/src/components/PortalDropdownMenu.tsx +6 -2
  160. package/src/components/ScratchpadPanel.tsx +144 -141
  161. package/src/components/UIResourceRenderer.tsx +143 -110
  162. package/src/components/chart-data-table.ts +50 -6
  163. package/src/components/chart-missing-plugin.ts +336 -0
  164. package/src/context/MCPUIStringsContext.tsx +76 -0
  165. package/src/services/component-registry.ts +10 -2
  166. package/src/services/validation.ts +44 -9
  167. package/src/styles/autocomplete.css +53 -53
  168. package/src/types/index.ts +13 -1
  169. package/src/utils/degraded-projections.ts +44 -4
  170. package/src/utils/math-markdown.ts +1 -1
  171. package/src/utils/missing-value.ts +127 -0
  172. package/src/utils/theme.ts +101 -0
@@ -1,4 +1,5 @@
1
1
  import { formatMCPUIString } from '../utils/format-string';
2
+ import { isMissingChartValue } from '../utils/missing-value';
2
3
 
3
4
  /**
4
5
  * Column headers and series names of the accessible chart data table.
@@ -26,10 +27,17 @@ export interface ChartDataTableLabels {
26
27
  x: string;
27
28
  y: string;
28
29
  r: string;
30
+ /**
31
+ * A value column header followed by the chart's `unit`. Template — `{label}`,
32
+ * `{unit}`. Only applied when the chart has a unit. Optional so that a
33
+ * complete labels table written for 6.20.0–6.23.0 still type-checks.
34
+ * @since v6.24.0
35
+ */
36
+ withUnit?: string;
29
37
  }
30
38
 
31
39
  /** English defaults for {@link ChartDataTableLabels}. */
32
- export const CHART_DATA_TABLE_LABELS: ChartDataTableLabels = {
40
+ export const CHART_DATA_TABLE_LABELS: Required<ChartDataTableLabels> = {
33
41
  series: 'Series',
34
42
  point: 'Point',
35
43
  label: 'Label',
@@ -37,11 +45,20 @@ export const CHART_DATA_TABLE_LABELS: ChartDataTableLabels = {
37
45
  x: 'x',
38
46
  y: 'y',
39
47
  r: 'r',
48
+ withUnit: '{label} ({unit})',
40
49
  };
41
50
 
42
51
  export interface ChartDataTable {
43
52
  columns: string[];
44
53
  rows: Array<Array<string | number>>;
54
+ /**
55
+ * Same shape as `rows`: `true` for a VALUE cell whose source entry is
56
+ * missing (`null` / non-finite, or a point whose `y` is). A slot past the
57
+ * end of a shorter or empty dataset has no entry and is not flagged. Label,
58
+ * series, point-index, `x` and `r` cells are never missing. `rows` itself
59
+ * still holds `''` there. @since v6.24.0
60
+ */
61
+ missing: boolean[][];
45
62
  }
46
63
 
47
64
  interface ChartDatasetLike {
@@ -51,6 +68,8 @@ interface ChartDatasetLike {
51
68
 
52
69
  export interface ChartDataLike {
53
70
  type?: string;
71
+ /** Unit of the values: appended to the value column headers. @since v6.24.0 */
72
+ unit?: string;
54
73
  data?: {
55
74
  labels?: Array<string | number>;
56
75
  datasets?: ChartDatasetLike[];
@@ -87,6 +106,10 @@ export function chartToDataTable(
87
106
  labels?: Partial<ChartDataTableLabels>
88
107
  ): ChartDataTable {
89
108
  const l: ChartDataTableLabels = { ...CHART_DATA_TABLE_LABELS, ...labels };
109
+ const unit = typeof params.unit === 'string' && params.unit !== '' ? params.unit : undefined;
110
+ const unitTemplate = l.withUnit ?? CHART_DATA_TABLE_LABELS.withUnit;
111
+ const withUnit = (label: string) =>
112
+ unit === undefined ? label : formatMCPUIString(unitTemplate, { label, unit });
90
113
  const datasets = params.data?.datasets ?? [];
91
114
  const dataLabels = params.data?.labels ?? [];
92
115
  const pointData =
@@ -104,10 +127,11 @@ export function chartToDataTable(
104
127
  l.point,
105
128
  ...(hasLabels ? [l.label] : []),
106
129
  l.x,
107
- l.y,
130
+ withUnit(l.y),
108
131
  ...(hasRadius ? [l.r] : []),
109
132
  ];
110
133
  const rows: Array<Array<string | number>> = [];
134
+ const missing: boolean[][] = [];
111
135
 
112
136
  datasets.forEach((dataset, datasetIndex) => {
113
137
  const series = dataset.label || formatMCPUIString(l.seriesName, { n: datasetIndex + 1 });
@@ -121,10 +145,20 @@ export function chartToDataTable(
121
145
  tableCell(point.y),
122
146
  ...(hasRadius ? [tableCell(point.r)] : []),
123
147
  ]);
148
+ // Only the y cell can be missing; a bare entry is its own y.
149
+ const yMissing = isMissingChartValue(isPoint(value) ? point.y : value);
150
+ missing.push([
151
+ false,
152
+ false,
153
+ ...(hasLabels ? [false] : []),
154
+ false,
155
+ yMissing,
156
+ ...(hasRadius ? [false] : []),
157
+ ]);
124
158
  });
125
159
  });
126
160
 
127
- return { columns, rows };
161
+ return { columns, rows, missing };
128
162
  }
129
163
 
130
164
  const rowCount = Math.max(
@@ -134,18 +168,28 @@ export function chartToDataTable(
134
168
  );
135
169
  const columns = [
136
170
  l.label,
137
- ...datasets.map(
138
- (dataset, index) => dataset.label || formatMCPUIString(l.seriesName, { n: index + 1 })
171
+ ...datasets.map((dataset, index) =>
172
+ withUnit(dataset.label || formatMCPUIString(l.seriesName, { n: index + 1 }))
139
173
  ),
140
174
  ];
141
175
  const rows: Array<Array<string | number>> = [];
176
+ const missing: boolean[][] = [];
142
177
 
143
178
  for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
144
179
  rows.push([
145
180
  tableCell(dataLabels[rowIndex] ?? rowIndex + 1),
146
181
  ...datasets.map((dataset) => tableCell(dataset.data?.[rowIndex])),
147
182
  ]);
183
+ // Only an ENTRY can be missing: a slot past the end of a shorter (or
184
+ // empty) dataset has no entry, and renders as before (an empty cell).
185
+ missing.push([
186
+ false,
187
+ ...datasets.map(
188
+ (dataset) =>
189
+ rowIndex < (dataset.data?.length ?? 0) && isMissingChartValue(dataset.data?.[rowIndex])
190
+ ),
191
+ ]);
148
192
  }
149
193
 
150
- return { columns, rows };
194
+ return { columns, rows, missing };
151
195
  }
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Missing values in a Chart.js chart (v6.24.0) — everything the renderer adds
3
+ * to its Chart.js config when a payload says a value is MISSING (`null`).
4
+ *
5
+ * The rule: absence is never zero. Chart.js already keeps the slot of a `null`
6
+ * (no label dropped) and draws nothing for it; what it does not do is make the
7
+ * absence READABLE. A null bar is a zero-height bar that is skipped when
8
+ * drawn, a null line point is a hole the line may bridge (`spanGaps`), and the
9
+ * legend, tooltip and axis know nothing of either. This module supplies:
10
+ *
11
+ * - {@link createMissingValuesPlugin} — a PER-CHART plugin (never
12
+ * `Chart.register`ed, so it can leak into no other chart) that outlines a
13
+ * dashed stub where a bar should have been (not on stacked or overlaid
14
+ * bars, whose baseline belongs to another series), and says "no data" on a
15
+ * chart with nothing to draw;
16
+ * - {@link createMissingLegendGenerator} — marks a fully-missing series in the
17
+ * legend;
18
+ * - {@link createTooltipLabel} — names a missing value, and appends the unit;
19
+ * - {@link withDatasetsNoGapSpan} — the shallow-cloned data with
20
+ * `spanGaps: false`;
21
+ * - {@link applyUnitAxisTitle} — the unit as the value axis title.
22
+ *
23
+ * Runtime-free on purpose (no `solid-js`, no `chart.js` import): the renderer
24
+ * hands in the Chart.js pieces it loaded and the chrome strings (lazily, as
25
+ * getters, so a late locale change is honoured at draw time). Every function
26
+ * is defensive — a drawing hook that throws would blank the whole chart.
27
+ *
28
+ * Chart.js 4.5.1 facts this relies on (verified in `dist/chart.js`):
29
+ * - a bar whose parsed value is `null` gets `base = head = vScale.getBasePixel()`
30
+ * and no `skip` flag (BarController.updateElements), and `BarController.draw`
31
+ * skips it (`getParsed(i)[vScale.axis] !== null`) — so nothing is painted;
32
+ * - the bar element still carries its true index-axis thickness (`width` for a
33
+ * vertical bar, `height` for a horizontal one).
34
+ */
35
+
36
+ import type { MCPUIStrings } from '../context/MCPUIStringsContext';
37
+ import { formatMCPUIString } from '../utils/format-string';
38
+ import { isMissingChartValue } from '../utils/missing-value';
39
+
40
+ type Strings = Required<MCPUIStrings>;
41
+
42
+ /** Length, in px along the value axis, of the dashed stub of a missing bar. */
43
+ export const MISSING_BAR_STUB_LENGTH = 12;
44
+ /** Stroke of the stub when the dataset names no border colour. */
45
+ export const MISSING_BAR_FALLBACK_COLOR = '#9ca3af';
46
+ /** Colour of the centred "no data" text. */
47
+ export const NO_DATA_TEXT_COLOR = '#6b7280';
48
+ const STUB_DASH = [4, 3];
49
+
50
+ /** Chart types whose value axis is `y` and whose points are `{x, y}`. */
51
+ const POINT_CHART_TYPES = new Set(['scatter', 'bubble']);
52
+
53
+ /**
54
+ * The category of convention line a chart shows under its canvas:
55
+ * `'gaps'` (a line breaks), `'bars'` (a dashed outline stands in for the
56
+ * bar) or `'omitted'` (nothing is drawn: scatter, bubble, stacked bars).
57
+ */
58
+ export type MissingNoteKind = 'gaps' | 'bars' | 'omitted';
59
+
60
+ /**
61
+ * Whether a bar chart's bars are stacked or overlaid (`stacked` on either
62
+ * axis). A missing bar there has no slot of its own at the baseline — the
63
+ * baseline belongs to the series below — so no dashed outline is drawn.
64
+ */
65
+ export function isStackedChart(options: any): boolean {
66
+ return Boolean(options?.scales?.x?.stacked || options?.scales?.y?.stacked);
67
+ }
68
+
69
+ /** Which convention note a chart gets: how ITS missing values are drawn. */
70
+ export function missingNoteKind(chartType: string | undefined, options?: any): MissingNoteKind {
71
+ if (chartType === 'bar') return isStackedChart(options) ? 'omitted' : 'bars';
72
+ if (chartType === 'scatter' || chartType === 'bubble') return 'omitted';
73
+ return 'gaps';
74
+ }
75
+
76
+ // ─── Data ──────────────────────────────────────────────────────────────────
77
+
78
+ /**
79
+ * Chart.js data with every dataset shallow-cloned and `spanGaps: false`, so a
80
+ * line breaks at a missing value instead of joining its neighbours. Never
81
+ * mutates its input; `labels` and every other key are carried by reference.
82
+ */
83
+ export function withDatasetsNoGapSpan<D extends { datasets?: unknown }>(data: D): D {
84
+ const datasets = Array.isArray(data?.datasets) ? data.datasets : null;
85
+ if (!datasets) return data;
86
+ return {
87
+ ...data,
88
+ datasets: datasets.map((dataset: unknown) =>
89
+ dataset !== null && typeof dataset === 'object'
90
+ ? { ...(dataset as object), spanGaps: false }
91
+ : dataset
92
+ ),
93
+ };
94
+ }
95
+
96
+ // ─── Legend ────────────────────────────────────────────────────────────────
97
+
98
+ type LegendGenerator = (chart: any) => any[];
99
+
100
+ /**
101
+ * Wraps a Chart.js legend label generator so a fully-missing dataset reads
102
+ * `"{series} (no data)"` (`strings.chartLegendNoData`). Every other item is
103
+ * passed through untouched.
104
+ */
105
+ export function createMissingLegendGenerator(
106
+ base: LegendGenerator,
107
+ fullyMissing: readonly boolean[],
108
+ getStrings: () => Strings
109
+ ): LegendGenerator {
110
+ return function generateLabels(this: unknown, chart: any) {
111
+ const items = base.call(this, chart);
112
+ if (!Array.isArray(items)) return items;
113
+ return items.map((item) => {
114
+ const index = item?.datasetIndex;
115
+ if (typeof index !== 'number' || !fullyMissing[index]) return item;
116
+ const text = typeof item.text === 'string' ? item.text : String(item.text ?? '');
117
+ return {
118
+ ...item,
119
+ text: formatMCPUIString(getStrings().chartLegendNoData, { series: text }),
120
+ };
121
+ });
122
+ };
123
+ }
124
+
125
+ // ─── Tooltip ───────────────────────────────────────────────────────────────
126
+
127
+ /** Whether a Chart.js tooltip item stands for a missing value. */
128
+ export function isMissingTooltipItem(item: any): boolean {
129
+ if (item === null || item === undefined) return false;
130
+ if (item.raw === null || item.raw === undefined || isMissingChartValue(item.raw)) return true;
131
+ const parsed = item.parsed;
132
+ if (parsed !== null && typeof parsed === 'object') {
133
+ const chartType = item.chart?.config?.type;
134
+ const valueKey =
135
+ chartType === 'radar' ? 'r' : item.chart?.options?.indexAxis === 'y' ? 'x' : 'y';
136
+ if (valueKey in parsed && parsed[valueKey] === null) return true;
137
+ }
138
+ return false;
139
+ }
140
+
141
+ /**
142
+ * Tooltip `label` callback: names a missing value (`"{series}: Missing
143
+ * value"`) and, with a `unit`, appends it to a present one. Otherwise it is
144
+ * Chart.js's default composition (dataset label, `": "`, formatted value) —
145
+ * including its `mode: 'dataset'` branch, which prefixes the CATEGORY label
146
+ * instead, since every item of that tooltip belongs to the same dataset.
147
+ * Hence a regular function: Chart.js calls it with the tooltip as `this`.
148
+ *
149
+ * `unit` is left off scatter and bubble tooltips: their formatted value is the
150
+ * pair `(x, y)`, to which one unit does not belong.
151
+ */
152
+ export function createTooltipLabel(
153
+ getStrings: () => Strings,
154
+ unit: string | undefined,
155
+ chartType: string | undefined
156
+ ): (this: any, item: any) => string {
157
+ const applyUnit = unit !== undefined && !POINT_CHART_TYPES.has(chartType ?? '');
158
+ return function label(this: any, item: any) {
159
+ const strings = getStrings();
160
+ const prefixLabel = this?.options?.mode === 'dataset' ? item?.label : item?.dataset?.label;
161
+ const prefix = typeof prefixLabel === 'string' && prefixLabel !== '' ? `${prefixLabel}: ` : '';
162
+
163
+ if (isMissingTooltipItem(item)) return `${prefix}${strings.missingValue}`;
164
+
165
+ const formatted = item?.formattedValue;
166
+ if (formatted === null || formatted === undefined) return prefix;
167
+ const value = applyUnit
168
+ ? formatMCPUIString(strings.chartValueWithUnit, { value: formatted, unit: unit as string })
169
+ : String(formatted);
170
+ return `${prefix}${value}`;
171
+ };
172
+ }
173
+
174
+ // ─── Axis ──────────────────────────────────────────────────────────────────
175
+
176
+ /**
177
+ * Sets the chart's `unit` as the title of its value axis, in place on the
178
+ * `options` object the renderer just built (never on the payload's).
179
+ *
180
+ * Only bar, line, scatter and bubble have a value axis to title. The value
181
+ * axis is `y`, or `x` when the index axis is `y` (a horizontal bar or line).
182
+ * An existing `scales[axis].title` is the payload's own word and is kept; the
183
+ * rest of the scale config (a `timeAxis` block sets `scales.x`) is kept too.
184
+ */
185
+ export function applyUnitAxisTitle(
186
+ options: any,
187
+ chartType: string | undefined,
188
+ unit: string | undefined
189
+ ): void {
190
+ if (unit === undefined) return;
191
+ if (chartType !== 'bar' && chartType !== 'line' && !POINT_CHART_TYPES.has(chartType ?? '')) {
192
+ return;
193
+ }
194
+ const horizontal = (chartType === 'bar' || chartType === 'line') && options?.indexAxis === 'y';
195
+ const axis = horizontal ? 'x' : 'y';
196
+ if (options.scales?.[axis]?.title !== undefined) return;
197
+ options.scales = {
198
+ ...options.scales,
199
+ [axis]: { ...options.scales?.[axis], title: { display: true, text: unit } },
200
+ };
201
+ }
202
+
203
+ // ─── Plugin ────────────────────────────────────────────────────────────────
204
+
205
+ export interface MissingValuesPluginOptions {
206
+ /** At least one dataset and every one of them has no observed value. */
207
+ allMissing: boolean;
208
+ /** Lazy: read at draw time. */
209
+ getNoDataText: () => string;
210
+ /** Lazy: `Chart.defaults.font.family`, when the build exposes it. */
211
+ getFontFamily?: () => string | undefined;
212
+ /** Lazy host theme override; omission retains the historical grey. */
213
+ getTextColor?: () => string | undefined;
214
+ }
215
+
216
+ /** A dataset colour for entry `index`, as a string, or `undefined`. */
217
+ function colorAt(color: unknown, index: number): string | undefined {
218
+ if (typeof color === 'string' && color !== '') return color;
219
+ if (Array.isArray(color) && color.length > 0) {
220
+ const picked = color[index % color.length];
221
+ return typeof picked === 'string' && picked !== '' ? picked : undefined;
222
+ }
223
+ return undefined;
224
+ }
225
+
226
+ /**
227
+ * Draws a dashed outline stub for every missing bar of every visible,
228
+ * non-stacked bar dataset, so a missing value is a visible mark, not a bar
229
+ * that "is" zero.
230
+ *
231
+ * The stub sits on the value-axis base, as thick as the bar would have been,
232
+ * and runs {@link MISSING_BAR_STUB_LENGTH}px toward the middle of the chart
233
+ * area (up from a bottom baseline, down from a top one, right from a left
234
+ * one). Clipped to the chart area.
235
+ */
236
+ export function drawMissingBarStubs(chart: any): void {
237
+ const ctx = chart?.ctx;
238
+ const area = chart?.chartArea;
239
+ const datasets = chart?.data?.datasets;
240
+ if (!ctx || !area || !Array.isArray(datasets)) return;
241
+
242
+ const left = area.left;
243
+ const top = area.top;
244
+ const right = area.right ?? left + (area.width ?? 0);
245
+ const bottom = area.bottom ?? top + (area.height ?? 0);
246
+
247
+ let saved = false;
248
+ datasets.forEach((dataset: any, datasetIndex: number) => {
249
+ const meta = chart.getDatasetMeta?.(datasetIndex);
250
+ if (!meta) return;
251
+ const isBar = meta.type === 'bar' || meta.controller?.constructor?.id === 'bar';
252
+ if (!isBar) return;
253
+ // Stacked or overlaid bars: the baseline under a missing segment is the
254
+ // series below, so an outline there would point at the wrong series.
255
+ if (meta.vScale?.options?.stacked || meta.iScale?.options?.stacked) return;
256
+ if (typeof chart.isDatasetVisible === 'function' && !chart.isDatasetVisible(datasetIndex)) {
257
+ return;
258
+ }
259
+ const values: unknown[] = Array.isArray(dataset?.data) ? dataset.data : [];
260
+ const elements: any[] = Array.isArray(meta.data) ? meta.data : [];
261
+
262
+ values.forEach((value, index) => {
263
+ if (!isMissingChartValue(value)) return;
264
+ const element = elements[index];
265
+ if (!element) return;
266
+ const props = element.getProps?.(['x', 'y', 'base', 'width', 'height'], true) ?? element;
267
+ const { x, y, base, width, height } = props;
268
+ if (![x, y, base].every((n) => Number.isFinite(n))) return;
269
+ const horizontal = element.horizontal === true;
270
+ const thickness = horizontal ? height : width;
271
+ if (!Number.isFinite(thickness) || thickness <= 0) return;
272
+
273
+ let rect: [number, number, number, number];
274
+ if (horizontal) {
275
+ const dir = base < (left + right) / 2 ? 1 : -1;
276
+ rect = [Math.min(base, base + dir * MISSING_BAR_STUB_LENGTH), y - thickness / 2, MISSING_BAR_STUB_LENGTH, thickness];
277
+ } else {
278
+ const dir = base > (top + bottom) / 2 ? -1 : 1;
279
+ rect = [x - thickness / 2, Math.min(base, base + dir * MISSING_BAR_STUB_LENGTH), thickness, MISSING_BAR_STUB_LENGTH];
280
+ }
281
+
282
+ if (!saved) {
283
+ ctx.save();
284
+ ctx.beginPath();
285
+ ctx.rect(left, top, right - left, bottom - top);
286
+ ctx.clip();
287
+ saved = true;
288
+ }
289
+ ctx.strokeStyle = colorAt(dataset?.borderColor, index) ?? MISSING_BAR_FALLBACK_COLOR;
290
+ ctx.lineWidth = 1;
291
+ ctx.setLineDash(STUB_DASH);
292
+ ctx.strokeRect(...rect);
293
+ });
294
+ });
295
+ if (saved) {
296
+ ctx.setLineDash?.([]);
297
+ ctx.restore();
298
+ }
299
+ }
300
+
301
+ /** Draws `text` centred in the chart area (a chart with nothing to plot). */
302
+ export function drawNoDataText(chart: any, text: string, fontFamily?: string, color?: string): void {
303
+ const ctx = chart?.ctx;
304
+ const area = chart?.chartArea;
305
+ if (!ctx || !area || !text) return;
306
+ const cx = (area.left + (area.right ?? area.left + (area.width ?? 0))) / 2;
307
+ const cy = (area.top + (area.bottom ?? area.top + (area.height ?? 0))) / 2;
308
+ ctx.save();
309
+ ctx.font = `12px ${fontFamily || 'sans-serif'}`;
310
+ ctx.fillStyle = color || NO_DATA_TEXT_COLOR;
311
+ ctx.textAlign = 'center';
312
+ ctx.textBaseline = 'middle';
313
+ ctx.fillText(text, cx, cy);
314
+ ctx.restore();
315
+ }
316
+
317
+ /**
318
+ * The per-chart plugin object. Passed as `config.plugins = [plugin]`: Chart.js
319
+ * scopes it to that one chart. Both hooks swallow their own errors — a
320
+ * decoration must never take the chart down.
321
+ */
322
+ export function createMissingValuesPlugin(options: MissingValuesPluginOptions) {
323
+ return {
324
+ id: 'mcpuiMissingValues',
325
+ afterDatasetsDraw(chart: any) {
326
+ try {
327
+ drawMissingBarStubs(chart);
328
+ if (options.allMissing) {
329
+ drawNoDataText(chart, options.getNoDataText(), options.getFontFamily?.(), options.getTextColor?.());
330
+ }
331
+ } catch {
332
+ // Cosmetic layer: never break the chart.
333
+ }
334
+ },
335
+ };
336
+ }
@@ -823,6 +823,70 @@ export interface MCPUIStrings {
823
823
  sizeKilobytes?: string
824
824
  /** ArtifactRenderer file size in megabytes. Template — `{size}`. */
825
825
  sizeMegabytes?: string
826
+
827
+ // ── Missing values (tables, chart data views, charts) ──────
828
+ /**
829
+ * Accessible name of a value that is explicitly missing: read by screen
830
+ * readers in place of the `-` a table cell shows, and the tooltip value of
831
+ * a bar whose value is `null`.
832
+ * @since 6.24.0
833
+ */
834
+ missingValue?: string
835
+ /**
836
+ * Legend under a table or data view that contains at least one missing
837
+ * value. Template — `{marker}` = the glyph the cells show (`-`).
838
+ * @since 6.24.0
839
+ */
840
+ missingValueLegend?: string
841
+ /**
842
+ * Note under a line or radar chart with a missing value: the line is broken
843
+ * there rather than drawn through, or down to zero.
844
+ * @since 6.24.0
845
+ */
846
+ chartMissingGaps?: string
847
+ /**
848
+ * Note under a bar chart with a missing value, which the chart draws as a
849
+ * dashed outline at the baseline so it cannot be read as a zero bar.
850
+ * @since 6.24.0
851
+ */
852
+ chartMissingBars?: string
853
+ /**
854
+ * Note under a chart that draws nothing for a missing value — a scatter or
855
+ * bubble chart, or a stacked bar chart (whose baseline belongs to the series
856
+ * below, so no outline can stand in for the bar).
857
+ * @since 6.24.0
858
+ */
859
+ chartMissingPoints?: string
860
+ /**
861
+ * Note naming the series whose values are ALL missing.
862
+ * Template — `{series}` = the series names, joined as a list in the locale.
863
+ * @since 6.24.0
864
+ */
865
+ chartSeriesNoData?: string
866
+ /**
867
+ * Legend entry of a series whose values are all missing.
868
+ * Template — `{series}` = the series name.
869
+ * @since 6.24.0
870
+ */
871
+ chartLegendNoData?: string
872
+ /**
873
+ * A value followed by the chart's `unit`, in tooltips.
874
+ * Template — `{value}` (already formatted), `{unit}`.
875
+ * @since 6.24.0
876
+ */
877
+ chartValueWithUnit?: string
878
+ /**
879
+ * A column header followed by the chart's `unit`, in the chart data view
880
+ * and the degraded tables. Template — `{label}`, `{unit}`.
881
+ * @since 6.24.0
882
+ */
883
+ chartLabelWithUnit?: string
884
+ /**
885
+ * Shown instead of a pie, doughnut or polar-area chart that received a
886
+ * missing value: those types would draw it as a zero share.
887
+ * @since 6.24.0
888
+ */
889
+ chartMissingUnsupported?: string
826
890
  }
827
891
 
828
892
  /**
@@ -1125,6 +1189,18 @@ export const DEFAULT_MCPUI_STRINGS: Required<MCPUIStrings> = {
1125
1189
  zoomOut: 'Zoom out',
1126
1190
  zoomReset: 'Reset zoom',
1127
1191
  zoomLevel: 'Zoom {percent}%',
1192
+
1193
+ // ── 6.24.0 — missing values ────────────────────────────────
1194
+ missingValue: 'Missing value',
1195
+ missingValueLegend: '“{marker}” marks a missing value.',
1196
+ chartMissingGaps: 'Breaks in a line mark missing values.',
1197
+ chartMissingBars: 'Dashed outlines mark missing values.',
1198
+ chartMissingPoints: 'Missing values are not plotted.',
1199
+ chartSeriesNoData: 'No data for {series}.',
1200
+ chartLegendNoData: '{series} (no data)',
1201
+ chartValueWithUnit: '{value} {unit}',
1202
+ chartLabelWithUnit: '{label} ({unit})',
1203
+ chartMissingUnsupported: 'This chart type cannot show missing values.',
1128
1204
  }
1129
1205
 
1130
1206
  /**
@@ -50,14 +50,15 @@ export const QuickchartRegistry: ComponentRegistryEntry = {
50
50
  data: {
51
51
  // Empty arrays match both branches; mirror Zod's inclusive union.
52
52
  anyOf: [
53
- { type: 'array', items: { type: 'number' } },
53
+ // `null` is an explicit MISSING value (v6.24.0): never a zero.
54
+ { type: 'array', items: { type: ['number', 'null'] } },
54
55
  {
55
56
  type: 'array',
56
57
  items: {
57
58
  type: 'object',
58
59
  properties: {
59
60
  x: { oneOf: [{ type: 'string' }, { type: 'number' }] },
60
- y: { type: 'number' },
61
+ y: { type: ['number', 'null'] },
61
62
  r: { type: 'number', minimum: 0 },
62
63
  },
63
64
  required: ['x', 'y'],
@@ -110,6 +111,13 @@ export const QuickchartRegistry: ComponentRegistryEntry = {
110
111
  max: { type: 'string' },
111
112
  },
112
113
  },
114
+ unit: {
115
+ type: 'string',
116
+ minLength: 1,
117
+ maxLength: 32,
118
+ description:
119
+ 'Unit of the values (e.g. "°C", "%"): shown on the value axis, in tooltips and in the data view',
120
+ },
113
121
  height: { type: 'string', description: 'Chart container height as a CSS value' },
114
122
  className: { type: 'string' },
115
123
  },
@@ -45,6 +45,8 @@ import type {
45
45
  // Runtime-free template helper: `src/validation.ts` (the SSR-safe subpath)
46
46
  // re-exports this module, so it must stay free of any solid-js import.
47
47
  import { formatMCPUIString } from '../utils/format-string';
48
+ // Runtime-free as well (v6.24.0): the missing-value rules shared with the renderers.
49
+ import { PART_TO_WHOLE_CHART_TYPES } from '../utils/missing-value';
48
50
 
49
51
  /**
50
52
  * All known ComponentType values — used to distinguish known-but-unvalidated
@@ -477,38 +479,71 @@ export function validateChartComponent(
477
479
  }
478
480
  }
479
481
 
480
- // Data type validation — numbers for categorical, {x,y} objects for point charts
482
+ // Data type validation — numbers for categorical, {x,y} objects for point charts.
483
+ //
484
+ // `null` is an explicit missing value (v6.24.0): a number slot, or a point's
485
+ // `y`, that exists but was not observed. Line, bar, radar, scatter and bubble
486
+ // charts keep its slot and mark it. Pie, doughnut and polar-area charts
487
+ // cannot: Chart.js parses a null share as `0`, so the other shares and the
488
+ // wedge itself would all be drawn wrong — those types refuse it.
489
+ const missingUnsupported = PART_TO_WHOLE_CHART_TYPES.has(chartType);
481
490
  for (const [index, dataset] of params.data.datasets.entries()) {
482
491
  if (!Array.isArray(dataset.data)) continue;
483
492
  const datasetIsPointChart = chartType === 'scatter' || chartType === 'bubble' || usesPoints(dataset.data);
484
493
  for (const [dataIndex, value] of dataset.data.entries()) {
494
+ const path = `params.data.datasets[${index}].data[${dataIndex}]`;
485
495
  if (datasetIsPointChart) {
486
496
  const vObj = value as any;
487
497
  if (
488
498
  typeof value !== 'object' ||
489
499
  value === null ||
490
500
  vObj.x == null ||
491
- typeof vObj.y !== 'number' ||
501
+ (vObj.y !== null && typeof vObj.y !== 'number') ||
492
502
  (vObj.r !== undefined && (typeof vObj.r !== 'number' || !Number.isFinite(vObj.r) || vObj.r < 0))
493
503
  ) {
494
504
  errors.push({
495
- path: `params.data.datasets[${index}].data[${dataIndex}]`,
496
- message: `Invalid point data: expected {x, y} object`,
505
+ path,
506
+ message: `Invalid point data: expected {x, y} object (y may be null for a missing value)`,
497
507
  code: 'INVALID_POINT_DATA',
498
508
  });
509
+ } else if (missingUnsupported && vObj.y === null) {
510
+ errors.push({
511
+ path,
512
+ message: `Missing value (null) is not supported by '${chartType}' charts, which would draw it as a zero share — use a bar or line chart, or a table`,
513
+ code: 'MISSING_VALUE_UNSUPPORTED',
514
+ });
499
515
  }
500
- } else {
501
- if (typeof value !== 'number' || !Number.isFinite(value)) {
516
+ } else if (value === null) {
517
+ if (missingUnsupported) {
502
518
  errors.push({
503
- path: `params.data.datasets[${index}].data[${dataIndex}]`,
504
- message: `Invalid data value: ${value} (must be finite number)`,
505
- code: 'INVALID_DATA_TYPE',
519
+ path,
520
+ message: `Missing value (null) is not supported by '${chartType}' charts, which would draw it as a zero share — use a bar or line chart, or a table`,
521
+ code: 'MISSING_VALUE_UNSUPPORTED',
506
522
  });
507
523
  }
524
+ } else if (typeof value !== 'number' || !Number.isFinite(value)) {
525
+ errors.push({
526
+ path,
527
+ message: `Invalid data value: ${value} (must be a finite number, or null for a missing value)`,
528
+ code: 'INVALID_DATA_TYPE',
529
+ });
508
530
  }
509
531
  }
510
532
  }
511
533
 
534
+ // `unit` (v6.24.0) ends up in the value-axis title, in every tooltip and in
535
+ // the data-view headers: a short string, or nothing. An empty string is
536
+ // tolerated — the renderer treats it as "no unit" — so a producer that
537
+ // emits `unit: ''` does not lose its whole chart over it.
538
+ const unit = (params as { unit?: unknown }).unit;
539
+ if (unit !== undefined && (typeof unit !== 'string' || unit.length > 32)) {
540
+ errors.push({
541
+ path: 'params.unit',
542
+ message: 'Invalid unit: expected a string of at most 32 characters',
543
+ code: 'INVALID_UNIT',
544
+ });
545
+ }
546
+
512
547
  return {
513
548
  valid: errors.length === 0,
514
549
  errors: errors.length > 0 ? errors : undefined,