@seed-ship/mcp-ui-solid 6.23.0 → 6.24.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +107 -2
  3. package/dist/components/ChartJSRenderer.cjs +192 -66
  4. package/dist/components/ChartJSRenderer.cjs.map +1 -1
  5. package/dist/components/ChartJSRenderer.d.ts.map +1 -1
  6. package/dist/components/ChartJSRenderer.js +192 -66
  7. package/dist/components/ChartJSRenderer.js.map +1 -1
  8. package/dist/components/DegradedFallback.cjs +34 -2
  9. package/dist/components/DegradedFallback.cjs.map +1 -1
  10. package/dist/components/DegradedFallback.d.ts +7 -0
  11. package/dist/components/DegradedFallback.d.ts.map +1 -1
  12. package/dist/components/DegradedFallback.js +34 -2
  13. package/dist/components/DegradedFallback.js.map +1 -1
  14. package/dist/components/MissingValue.cjs +43 -0
  15. package/dist/components/MissingValue.cjs.map +1 -0
  16. package/dist/components/MissingValue.d.ts +27 -0
  17. package/dist/components/MissingValue.d.ts.map +1 -0
  18. package/dist/components/MissingValue.js +43 -0
  19. package/dist/components/MissingValue.js.map +1 -0
  20. package/dist/components/UIResourceRenderer.cjs +237 -199
  21. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  22. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  23. package/dist/components/UIResourceRenderer.js +237 -199
  24. package/dist/components/UIResourceRenderer.js.map +1 -1
  25. package/dist/components/chart-data-table.cjs +30 -5
  26. package/dist/components/chart-data-table.cjs.map +1 -1
  27. package/dist/components/chart-data-table.d.ts +18 -1
  28. package/dist/components/chart-data-table.d.ts.map +1 -1
  29. package/dist/components/chart-data-table.js +30 -5
  30. package/dist/components/chart-data-table.js.map +1 -1
  31. package/dist/components/chart-missing-plugin.cjs +194 -0
  32. package/dist/components/chart-missing-plugin.cjs.map +1 -0
  33. package/dist/components/chart-missing-plugin.d.ts +127 -0
  34. package/dist/components/chart-missing-plugin.d.ts.map +1 -0
  35. package/dist/components/chart-missing-plugin.js +194 -0
  36. package/dist/components/chart-missing-plugin.js.map +1 -0
  37. package/dist/context/MCPUIStringsContext.cjs +12 -1
  38. package/dist/context/MCPUIStringsContext.cjs.map +1 -1
  39. package/dist/context/MCPUIStringsContext.d.ts +62 -0
  40. package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
  41. package/dist/context/MCPUIStringsContext.js +12 -1
  42. package/dist/context/MCPUIStringsContext.js.map +1 -1
  43. package/dist/services/component-registry.cjs +9 -2
  44. package/dist/services/component-registry.cjs.map +1 -1
  45. package/dist/services/component-registry.d.ts.map +1 -1
  46. package/dist/services/component-registry.js +9 -2
  47. package/dist/services/component-registry.js.map +1 -1
  48. package/dist/services/validation.cjs +31 -8
  49. package/dist/services/validation.cjs.map +1 -1
  50. package/dist/services/validation.d.ts.map +1 -1
  51. package/dist/services/validation.js +31 -8
  52. package/dist/services/validation.js.map +1 -1
  53. package/dist/types/index.d.ts +14 -2
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/dist/utils/degraded-projections.cjs +19 -3
  56. package/dist/utils/degraded-projections.cjs.map +1 -1
  57. package/dist/utils/degraded-projections.d.ts +22 -2
  58. package/dist/utils/degraded-projections.d.ts.map +1 -1
  59. package/dist/utils/degraded-projections.js +19 -3
  60. package/dist/utils/degraded-projections.js.map +1 -1
  61. package/dist/utils/missing-value.cjs +60 -0
  62. package/dist/utils/missing-value.cjs.map +1 -0
  63. package/dist/utils/missing-value.d.ts +79 -0
  64. package/dist/utils/missing-value.d.ts.map +1 -0
  65. package/dist/utils/missing-value.js +60 -0
  66. package/dist/utils/missing-value.js.map +1 -0
  67. package/package.json +3 -3
  68. package/src/components/ChartJSRenderer.tsx +159 -6
  69. package/src/components/DegradedFallback.tsx +24 -2
  70. package/src/components/MissingValue.tsx +61 -0
  71. package/src/components/UIResourceRenderer.tsx +42 -9
  72. package/src/components/chart-data-table.ts +50 -6
  73. package/src/components/chart-missing-plugin.ts +334 -0
  74. package/src/context/MCPUIStringsContext.tsx +76 -0
  75. package/src/services/component-registry.ts +10 -2
  76. package/src/services/validation.ts +44 -9
  77. package/src/types/index.ts +13 -1
  78. package/src/utils/degraded-projections.ts +44 -4
  79. package/src/utils/missing-value.ts +127 -0
@@ -1,4 +1,5 @@
1
1
  import { formatMCPUIString } from "./format-string.js";
2
+ import { isMissingChartValue } from "./missing-value.js";
2
3
  const DEGRADED_PROJECTION_LABELS = {
3
4
  source: "Source",
4
5
  target: "Target",
@@ -10,7 +11,8 @@ const DEGRADED_PROJECTION_LABELS = {
10
11
  info: "Info",
11
12
  marker: "marker",
12
13
  feature: "feature",
13
- series: "Series {n}"
14
+ series: "Series {n}",
15
+ withUnit: "{label} ({unit})"
14
16
  };
15
17
  const withLabels = (labels) => ({
16
18
  ...DEGRADED_PROJECTION_LABELS,
@@ -84,6 +86,7 @@ function mapToDegradedTable(params, labels) {
84
86
  function chartToDegradedTable(params, labels) {
85
87
  var _a, _b;
86
88
  const l = withLabels(labels);
89
+ const unit = typeof params.unit === "string" && params.unit !== "" ? params.unit : void 0;
87
90
  const datasets = ((_a = params.data) == null ? void 0 : _a.datasets) ?? [];
88
91
  const dataLabels = ((_b = params.data) == null ? void 0 : _b.labels) ?? [];
89
92
  const rowCount = Math.max(dataLabels.length, ...datasets.map((d) => {
@@ -92,16 +95,29 @@ function chartToDegradedTable(params, labels) {
92
95
  }), 0);
93
96
  const columns = [
94
97
  "",
95
- ...datasets.map((d, i) => d.label ?? formatMCPUIString(l.series, { n: i + 1 }))
98
+ ...datasets.map((d, i) => {
99
+ const name = d.label ?? formatMCPUIString(l.series, { n: i + 1 });
100
+ return unit === void 0 ? name : formatMCPUIString(l.withUnit ?? DEGRADED_PROJECTION_LABELS.withUnit, { label: name, unit });
101
+ })
96
102
  ];
97
103
  const rows = [];
104
+ const missing = [];
98
105
  for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {
99
106
  rows.push([cell(dataLabels[r] ?? r + 1), ...datasets.map((d) => {
100
107
  var _a2;
101
108
  return cell((_a2 = d.data) == null ? void 0 : _a2[r]);
102
109
  })]);
110
+ missing.push([
111
+ false,
112
+ ...datasets.map((d) => {
113
+ var _a2, _b2;
114
+ if (r >= (((_a2 = d.data) == null ? void 0 : _a2.length) ?? 0)) return false;
115
+ const entry = (_b2 = d.data) == null ? void 0 : _b2[r];
116
+ return (entry === null || typeof entry !== "object") && isMissingChartValue(entry);
117
+ })
118
+ ]);
103
119
  }
104
- return { columns, rows };
120
+ return { columns, rows, missing };
105
121
  }
106
122
  export {
107
123
  DEGRADED_PROJECTION_LABELS,
@@ -1 +1 @@
1
- {"version":3,"file":"degraded-projections.js","sources":["../../src/utils/degraded-projections.ts"],"sourcesContent":["/**\n * Degraded-fallback projections (audit 2026-05-30, P2.5).\n *\n * Pure functions that turn a heavy renderer's params into a flat\n * `{ columns, rows }` table for `<DegradedFallback>` — the middle rung of\n * the fallback ladder shown when the native render (G6 / Leaflet / Chart.js)\n * is unavailable or throws. No peer deps, no side effects, fully testable.\n *\n * These are best-effort views: they surface the underlying data so the user\n * isn't left with a blank space, not faithful reproductions of the chart/\n * map/graph.\n */\n\nimport { formatMCPUIString } from './format-string';\n\n/**\n * Column headers and series names of the degraded tables.\n *\n * The projections are pure helpers with no access to `MCPUIStrings`, so the\n * wording is injected: each function takes an optional trailing `labels`\n * partial, and the renderers feed it from `useMCPUIStrings()`. Left out, the\n * English defaults below apply — the pre-v6.20.0 behaviour.\n *\n * @since v6.20.0\n */\nexport interface DegradedProjectionLabels {\n /** Graph edge table — source column. */\n source: string;\n /** Graph edge table — target column. */\n target: string;\n /** Graph edge / node table — label column. */\n label: string;\n /** Graph node table — node id column. */\n node: string;\n /** Map table — feature type column. */\n type: string;\n /** Map table — latitude column. */\n lat: string;\n /** Map table — longitude column. */\n lng: string;\n /** Map table — property summary column. */\n info: string;\n /** Map table — `Type` cell of a marker row (v6.20.0, 6th pass). */\n marker: string;\n /** Map table — `Type` cell of a GeoJSON feature without a geometry type (v6.20.0). */\n feature: string;\n /** Chart table — fallback dataset name. Template — `{n}`. */\n series: string;\n}\n\n/** English defaults for {@link DegradedProjectionLabels}. */\nexport const DEGRADED_PROJECTION_LABELS: DegradedProjectionLabels = {\n source: 'Source',\n target: 'Target',\n label: 'Label',\n node: 'Node',\n type: 'Type',\n lat: 'Lat',\n lng: 'Lng',\n info: 'Info',\n marker: 'marker',\n feature: 'feature',\n series: 'Series {n}',\n};\n\nconst withLabels = (labels?: Partial<DegradedProjectionLabels>): DegradedProjectionLabels => ({\n ...DEGRADED_PROJECTION_LABELS,\n ...labels,\n});\n\nexport interface DegradedTable {\n columns: string[];\n rows: Array<Array<string | number>>;\n}\n\nconst MAX_PROJECTED_ROWS = 200;\n\n/** Compact a value to a single table cell string. */\nfunction cell(value: unknown): string {\n if (value == null) return '';\n if (typeof value === 'object') {\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n }\n return String(value);\n}\n\n// ─── Graph ───────────────────────────────────────────────────────────────\n\n/**\n * Graph → edge table when edges exist (Source / Target / Label), else a\n * node list (Node / Label). The graph renderer can therefore degrade to a\n * readable relationship listing instead of a blank canvas.\n */\nexport function graphToDegradedTable(params: {\n nodes?: Array<{ id: string; label?: string }>;\n edges?: Array<{ source: string; target: string; label?: string; weight?: number }>;\n}, labels?: Partial<DegradedProjectionLabels>): DegradedTable {\n const l = withLabels(labels);\n const edges = params.edges ?? [];\n if (edges.length > 0) {\n return {\n columns: [l.source, l.target, l.label],\n rows: edges.slice(0, MAX_PROJECTED_ROWS).map((e) => {\n const label = [e.weight != null ? String(e.weight) : '', e.label ?? '']\n .filter(Boolean)\n .join(' · ');\n return [cell(e.source), cell(e.target), label];\n }),\n };\n }\n const nodes = params.nodes ?? [];\n return {\n columns: [l.node, l.label],\n rows: nodes.slice(0, MAX_PROJECTED_ROWS).map((n) => [cell(n.id), cell(n.label ?? n.id)]),\n };\n}\n\n// ─── Map ───────────────────────────────────────────────────────────────────\n\ninterface GeoJSONLikeFeature {\n geometry?: { type?: string; coordinates?: unknown } | null;\n properties?: Record<string, unknown> | null;\n}\n\n/** Pull a representative `[lng, lat]` pair out of a geometry's coordinates. */\nfunction firstLngLat(coords: unknown): [number, number] | null {\n let c: unknown = coords;\n // Descend nested arrays until we reach a [number, number, ...] position.\n while (Array.isArray(c) && Array.isArray(c[0])) c = c[0];\n if (Array.isArray(c) && typeof c[0] === 'number' && typeof c[1] === 'number') {\n return [c[0], c[1]];\n }\n return null;\n}\n\n/**\n * Map → a coordinate table. Markers become Lat/Lng/Label rows; GeoJSON\n * features become Type / Lat / Lng (+ a compact properties summary). So a\n * map that can't paint still lists where its points are.\n */\nexport function mapToDegradedTable(params: {\n markers?: Array<{\n position: [number, number] | { lat: number; lng: number };\n tooltip?: string;\n popup?: string;\n }>;\n geojson?: unknown;\n}, labels?: Partial<DegradedProjectionLabels>): DegradedTable {\n const l = withLabels(labels);\n const rows: Array<Array<string | number>> = [];\n\n for (const m of params.markers ?? []) {\n const lat = Array.isArray(m.position) ? m.position[0] : m.position?.lat;\n const lng = Array.isArray(m.position) ? m.position[1] : m.position?.lng;\n rows.push([l.marker, cell(lat), cell(lng), cell(m.tooltip ?? m.popup ?? '')]);\n }\n\n const fc = params.geojson as { features?: GeoJSONLikeFeature[] } | undefined;\n const features = Array.isArray(fc?.features) ? fc!.features : [];\n for (const f of features) {\n const ll = firstLngLat(f.geometry?.coordinates);\n const props = f.properties ?? {};\n const propSummary = Object.keys(props)\n .slice(0, 3)\n .map((k) => `${k}=${cell(props[k])}`)\n .join(', ');\n rows.push([\n cell(f.geometry?.type ?? l.feature),\n ll ? cell(ll[1]) : '',\n ll ? cell(ll[0]) : '',\n propSummary,\n ]);\n }\n\n return {\n columns: [l.type, l.lat, l.lng, l.info],\n rows: rows.slice(0, MAX_PROJECTED_ROWS),\n };\n}\n\n// ─── Chart ───────────────────────────────────────────────────────────────\n\n/**\n * Chart → a series table: one row per label, one column per dataset. So a\n * chart that can't draw still shows its numbers. Point/object data (scatter,\n * bubble, time series) is stringified per cell.\n */\nexport function chartToDegradedTable(params: {\n data?: {\n labels?: Array<string | number>;\n datasets?: Array<{ label?: string; data?: unknown[] }>;\n };\n}, labels?: Partial<DegradedProjectionLabels>): DegradedTable {\n const l = withLabels(labels);\n const datasets = params.data?.datasets ?? [];\n const dataLabels = params.data?.labels ?? [];\n const rowCount = Math.max(dataLabels.length, ...datasets.map((d) => d.data?.length ?? 0), 0);\n\n const columns = [\n '',\n ...datasets.map((d, i) => d.label ?? formatMCPUIString(l.series, { n: i + 1 })),\n ];\n const rows: Array<Array<string | number>> = [];\n for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {\n rows.push([cell(dataLabels[r] ?? r + 1), ...datasets.map((d) => cell(d.data?.[r]))]);\n }\n return { columns, rows };\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"degraded-projections.js","sources":["../../src/utils/degraded-projections.ts"],"sourcesContent":["/**\n * Degraded-fallback projections (audit 2026-05-30, P2.5).\n *\n * Pure functions that turn a heavy renderer's params into a flat\n * `{ columns, rows }` table for `<DegradedFallback>` — the middle rung of\n * the fallback ladder shown when the native render (G6 / Leaflet / Chart.js)\n * is unavailable or throws. No peer deps, no side effects, fully testable.\n *\n * These are best-effort views: they surface the underlying data so the user\n * isn't left with a blank space, not faithful reproductions of the chart/\n * map/graph.\n */\n\nimport { formatMCPUIString } from './format-string';\nimport { isMissingChartValue } from './missing-value';\n\n/**\n * Column headers and series names of the degraded tables.\n *\n * The projections are pure helpers with no access to `MCPUIStrings`, so the\n * wording is injected: each function takes an optional trailing `labels`\n * partial, and the renderers feed it from `useMCPUIStrings()`. Left out, the\n * English defaults below apply — the pre-v6.20.0 behaviour.\n *\n * @since v6.20.0\n */\nexport interface DegradedProjectionLabels {\n /** Graph edge table — source column. */\n source: string;\n /** Graph edge table — target column. */\n target: string;\n /** Graph edge / node table — label column. */\n label: string;\n /** Graph node table — node id column. */\n node: string;\n /** Map table — feature type column. */\n type: string;\n /** Map table — latitude column. */\n lat: string;\n /** Map table — longitude column. */\n lng: string;\n /** Map table — property summary column. */\n info: string;\n /** Map table — `Type` cell of a marker row (v6.20.0, 6th pass). */\n marker: string;\n /** Map table — `Type` cell of a GeoJSON feature without a geometry type (v6.20.0). */\n feature: string;\n /** Chart table — fallback dataset name. Template — `{n}`. */\n series: string;\n /**\n * Chart table — a dataset column header followed by the chart's `unit`.\n * Template — `{label}`, `{unit}`. Only applied when the chart has a unit.\n * Optional so that a complete labels table written for 6.20.0–6.23.0 still\n * type-checks. @since v6.24.0\n */\n withUnit?: string;\n}\n\n/** English defaults for {@link DegradedProjectionLabels}. */\nexport const DEGRADED_PROJECTION_LABELS: Required<DegradedProjectionLabels> = {\n source: 'Source',\n target: 'Target',\n label: 'Label',\n node: 'Node',\n type: 'Type',\n lat: 'Lat',\n lng: 'Lng',\n info: 'Info',\n marker: 'marker',\n feature: 'feature',\n series: 'Series {n}',\n withUnit: '{label} ({unit})',\n};\n\nconst withLabels = (labels?: Partial<DegradedProjectionLabels>): DegradedProjectionLabels => ({\n ...DEGRADED_PROJECTION_LABELS,\n ...labels,\n});\n\nexport interface DegradedTable {\n columns: string[];\n rows: Array<Array<string | number>>;\n}\n\n/** A degraded chart table: its cells, plus which of them are missing values. */\nexport interface DegradedChartTable extends DegradedTable {\n /** Same shape as `rows`; `true` for a dataset entry that is missing. @since v6.24.0 */\n missing: boolean[][];\n}\n\nconst MAX_PROJECTED_ROWS = 200;\n\n/** Compact a value to a single table cell string. */\nfunction cell(value: unknown): string {\n if (value == null) return '';\n if (typeof value === 'object') {\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n }\n return String(value);\n}\n\n// ─── Graph ───────────────────────────────────────────────────────────────\n\n/**\n * Graph → edge table when edges exist (Source / Target / Label), else a\n * node list (Node / Label). The graph renderer can therefore degrade to a\n * readable relationship listing instead of a blank canvas.\n */\nexport function graphToDegradedTable(params: {\n nodes?: Array<{ id: string; label?: string }>;\n edges?: Array<{ source: string; target: string; label?: string; weight?: number }>;\n}, labels?: Partial<DegradedProjectionLabels>): DegradedTable {\n const l = withLabels(labels);\n const edges = params.edges ?? [];\n if (edges.length > 0) {\n return {\n columns: [l.source, l.target, l.label],\n rows: edges.slice(0, MAX_PROJECTED_ROWS).map((e) => {\n const label = [e.weight != null ? String(e.weight) : '', e.label ?? '']\n .filter(Boolean)\n .join(' · ');\n return [cell(e.source), cell(e.target), label];\n }),\n };\n }\n const nodes = params.nodes ?? [];\n return {\n columns: [l.node, l.label],\n rows: nodes.slice(0, MAX_PROJECTED_ROWS).map((n) => [cell(n.id), cell(n.label ?? n.id)]),\n };\n}\n\n// ─── Map ───────────────────────────────────────────────────────────────────\n\ninterface GeoJSONLikeFeature {\n geometry?: { type?: string; coordinates?: unknown } | null;\n properties?: Record<string, unknown> | null;\n}\n\n/** Pull a representative `[lng, lat]` pair out of a geometry's coordinates. */\nfunction firstLngLat(coords: unknown): [number, number] | null {\n let c: unknown = coords;\n // Descend nested arrays until we reach a [number, number, ...] position.\n while (Array.isArray(c) && Array.isArray(c[0])) c = c[0];\n if (Array.isArray(c) && typeof c[0] === 'number' && typeof c[1] === 'number') {\n return [c[0], c[1]];\n }\n return null;\n}\n\n/**\n * Map → a coordinate table. Markers become Lat/Lng/Label rows; GeoJSON\n * features become Type / Lat / Lng (+ a compact properties summary). So a\n * map that can't paint still lists where its points are.\n */\nexport function mapToDegradedTable(params: {\n markers?: Array<{\n position: [number, number] | { lat: number; lng: number };\n tooltip?: string;\n popup?: string;\n }>;\n geojson?: unknown;\n}, labels?: Partial<DegradedProjectionLabels>): DegradedTable {\n const l = withLabels(labels);\n const rows: Array<Array<string | number>> = [];\n\n for (const m of params.markers ?? []) {\n const lat = Array.isArray(m.position) ? m.position[0] : m.position?.lat;\n const lng = Array.isArray(m.position) ? m.position[1] : m.position?.lng;\n rows.push([l.marker, cell(lat), cell(lng), cell(m.tooltip ?? m.popup ?? '')]);\n }\n\n const fc = params.geojson as { features?: GeoJSONLikeFeature[] } | undefined;\n const features = Array.isArray(fc?.features) ? fc!.features : [];\n for (const f of features) {\n const ll = firstLngLat(f.geometry?.coordinates);\n const props = f.properties ?? {};\n const propSummary = Object.keys(props)\n .slice(0, 3)\n .map((k) => `${k}=${cell(props[k])}`)\n .join(', ');\n rows.push([\n cell(f.geometry?.type ?? l.feature),\n ll ? cell(ll[1]) : '',\n ll ? cell(ll[0]) : '',\n propSummary,\n ]);\n }\n\n return {\n columns: [l.type, l.lat, l.lng, l.info],\n rows: rows.slice(0, MAX_PROJECTED_ROWS),\n };\n}\n\n// ─── Chart ───────────────────────────────────────────────────────────────\n\n/**\n * Chart → a series table: one row per label, one column per dataset. So a\n * chart that can't draw still shows its numbers. Point/object data (scatter,\n * bubble, time series) is stringified per cell.\n *\n * `missing` flags the cells whose dataset entry is a missing value (`null`\n * or non-finite) so the table can mark them instead of showing an empty\n * cell. A slot past the end of a shorter or empty dataset has no entry and is\n * not flagged; a point object keeps its JSON text (where a `\"y\":null` reads\n * as such) and is not flagged either. With a `unit`, dataset column headers\n * become `labels.withUnit`.\n */\nexport function chartToDegradedTable(params: {\n unit?: string;\n data?: {\n labels?: Array<string | number>;\n datasets?: Array<{ label?: string; data?: unknown[] }>;\n };\n}, labels?: Partial<DegradedProjectionLabels>): DegradedChartTable {\n const l = withLabels(labels);\n const unit = typeof params.unit === 'string' && params.unit !== '' ? params.unit : undefined;\n const datasets = params.data?.datasets ?? [];\n const dataLabels = params.data?.labels ?? [];\n const rowCount = Math.max(dataLabels.length, ...datasets.map((d) => d.data?.length ?? 0), 0);\n\n const columns = [\n '',\n ...datasets.map((d, i) => {\n const name = d.label ?? formatMCPUIString(l.series, { n: i + 1 });\n return unit === undefined\n ? name\n : formatMCPUIString(l.withUnit ?? DEGRADED_PROJECTION_LABELS.withUnit, { label: name, unit });\n }),\n ];\n const rows: Array<Array<string | number>> = [];\n const missing: boolean[][] = [];\n for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {\n rows.push([cell(dataLabels[r] ?? r + 1), ...datasets.map((d) => cell(d.data?.[r]))]);\n missing.push([\n false,\n ...datasets.map((d) => {\n // Past the end of a shorter or empty dataset there is no entry, so\n // nothing to flag (the cell stays empty, as before 6.24.0).\n if (r >= (d.data?.length ?? 0)) return false;\n const entry = d.data?.[r];\n return (entry === null || typeof entry !== 'object') && isMissingChartValue(entry);\n }),\n ]);\n }\n return { columns, rows, missing };\n}\n"],"names":["_a","_b"],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const MISSING_VALUE_MARKER = "-";
4
+ const PART_TO_WHOLE_CHART_TYPES = /* @__PURE__ */ new Set([
5
+ "pie",
6
+ "doughnut",
7
+ "polarArea"
8
+ ]);
9
+ function isMissingCellValue(value) {
10
+ if (value === null || value === void 0) return true;
11
+ if (typeof value === "object") return false;
12
+ const cleaned = stripUndefinedDebris(String(value)).trim();
13
+ return cleaned === "" || cleaned === MISSING_VALUE_MARKER || cleaned.toLowerCase() === "undefined";
14
+ }
15
+ function stripUndefinedDebris(value) {
16
+ return value.replace(/\s*[–-]\s*undefined\s*$/gi, "").replace(/^undefined\s*[–-]\s*/gi, "");
17
+ }
18
+ function isMissingChartValue(value) {
19
+ if (value === null || value === void 0) return true;
20
+ if (typeof value === "number") return !Number.isFinite(value);
21
+ if (typeof value === "object" && !Array.isArray(value)) {
22
+ const y = value.y;
23
+ return y === null || y === void 0 || typeof y === "number" && !Number.isFinite(y);
24
+ }
25
+ return false;
26
+ }
27
+ function summarizeChartMissing(params) {
28
+ var _a;
29
+ const datasets = Array.isArray((_a = params == null ? void 0 : params.data) == null ? void 0 : _a.datasets) ? params.data.datasets : [];
30
+ const fullyMissing = [];
31
+ const partiallyMissing = [];
32
+ let anyMissing = false;
33
+ let anyObserved = false;
34
+ for (const dataset of datasets) {
35
+ const data = Array.isArray(dataset == null ? void 0 : dataset.data) ? dataset.data : [];
36
+ let observed = 0;
37
+ let missing = 0;
38
+ for (const value of data) {
39
+ if (isMissingChartValue(value)) missing += 1;
40
+ else observed += 1;
41
+ }
42
+ fullyMissing.push(missing > 0 && observed === 0);
43
+ partiallyMissing.push(observed > 0 && missing > 0);
44
+ anyMissing || (anyMissing = missing > 0);
45
+ anyObserved || (anyObserved = observed > 0);
46
+ }
47
+ return {
48
+ hasMissing: anyMissing,
49
+ fullyMissing,
50
+ partiallyMissing,
51
+ allMissing: anyMissing && !anyObserved
52
+ };
53
+ }
54
+ exports.MISSING_VALUE_MARKER = MISSING_VALUE_MARKER;
55
+ exports.PART_TO_WHOLE_CHART_TYPES = PART_TO_WHOLE_CHART_TYPES;
56
+ exports.isMissingCellValue = isMissingCellValue;
57
+ exports.isMissingChartValue = isMissingChartValue;
58
+ exports.stripUndefinedDebris = stripUndefinedDebris;
59
+ exports.summarizeChartMissing = summarizeChartMissing;
60
+ //# sourceMappingURL=missing-value.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-value.cjs","sources":["../../src/utils/missing-value.ts"],"sourcesContent":["/**\n * Missing values — one definition shared by tables, chart data views,\n * degraded projections, chart validation and the chart renderer.\n *\n * @since v6.24.0\n *\n * A missing value is a value the payload says EXISTS but was not observed:\n * `null` in a chart dataset (or as a point's `y`), `null` / `undefined` in a\n * table cell. It is not a zero, and it is not the absence of a row or of a\n * label: every view keeps its slot and marks it.\n *\n * Runtime-free on purpose (no `solid-js`): the pure projections in\n * `chart-data-table.ts` and `degraded-projections.ts`, and `validation.ts`\n * (published as the `/validation` subpath), import it.\n */\n\n/** The glyph a table cell shows for a missing value — `renderCellValue`'s historical `-`. */\nexport const MISSING_VALUE_MARKER = '-'\n\n/**\n * Chart types that encode a value as a share of a whole (or, for polar area,\n * as a wedge whose emptiness reads as zero). Chart.js parses a `null` share\n * as `0`, so these types refuse missing values instead of drawing them.\n */\nexport const PART_TO_WHOLE_CHART_TYPES: ReadonlySet<string> = new Set([\n 'pie',\n 'doughnut',\n 'polarArea',\n])\n\n/**\n * Whether a raw TABLE cell value is displayed as {@link MISSING_VALUE_MARKER}.\n *\n * Mirrors the rules `renderCellValue` applies before any formatting — `null`,\n * `undefined`, and a string that is empty or the word `undefined` once the\n * backend's `\"X – undefined\"` debris is stripped — plus the literal marker\n * itself (whitespace around it aside, which a table cell does not render),\n * since it looks identical on screen and so must read identically.\n *\n * Objects are never missing here: `renderCellValue` renders them (link,\n * name, or escaped JSON).\n */\nexport function isMissingCellValue(value: unknown): boolean {\n if (value === null || value === undefined) return true\n if (typeof value === 'object') return false\n const cleaned = stripUndefinedDebris(String(value)).trim()\n return cleaned === '' || cleaned === MISSING_VALUE_MARKER || cleaned.toLowerCase() === 'undefined'\n}\n\n/**\n * Removes the `\"Text – undefined\"` / `\"undefined – Text\"` debris some\n * backends concatenate. Shared with `renderCellValue` so both apply the very\n * same cleanup.\n */\nexport function stripUndefinedDebris(value: string): string {\n return value.replace(/\\s*[–-]\\s*undefined\\s*$/gi, '').replace(/^undefined\\s*[–-]\\s*/gi, '')\n}\n\n/**\n * Whether one CHART data entry is missing: `null` / `undefined`, a non-finite\n * number (Chart.js parses those to `null` too), or a point whose `y` is.\n */\nexport function isMissingChartValue(value: unknown): boolean {\n if (value === null || value === undefined) return true\n if (typeof value === 'number') return !Number.isFinite(value)\n if (typeof value === 'object' && !Array.isArray(value)) {\n const y = (value as { y?: unknown }).y\n return y === null || y === undefined || (typeof y === 'number' && !Number.isFinite(y))\n }\n return false\n}\n\n/**\n * What a chart's missing values amount to.\n *\n * Only ENTRIES count: an explicit `null` (or non-finite number, or a point\n * whose `y` is one). A dataset with no entry at all (`data: []`) is not\n * \"missing\" — it is how 6.23.0 and earlier payloads said \"nothing yet\", e.g.\n * while streaming, and it keeps rendering exactly as it did then.\n */\nexport interface ChartMissingSummary {\n /** At least one entry of one dataset is missing. */\n hasMissing: boolean\n /** Per dataset, in order: it has entries and every one of them is missing. */\n fullyMissing: boolean[]\n /**\n * Per dataset, in order: at least one observed value AND at least one\n * missing entry — the series a reader actually sees broken.\n */\n partiallyMissing: boolean[]\n /** Something is missing and no dataset has a single observed value: nothing to plot. */\n allMissing: boolean\n}\n\ninterface ChartDataLike {\n data?: {\n datasets?: Array<{ data?: unknown[] } | null | undefined>\n }\n}\n\n/** Summarises the missing values of a chart payload. Never throws on a malformed payload. */\nexport function summarizeChartMissing(params: ChartDataLike | null | undefined): ChartMissingSummary {\n const datasets = Array.isArray(params?.data?.datasets) ? params.data.datasets : []\n const fullyMissing: boolean[] = []\n const partiallyMissing: boolean[] = []\n let anyMissing = false\n let anyObserved = false\n for (const dataset of datasets) {\n const data = Array.isArray(dataset?.data) ? dataset.data : []\n let observed = 0\n let missing = 0\n for (const value of data) {\n if (isMissingChartValue(value)) missing += 1\n else observed += 1\n }\n fullyMissing.push(missing > 0 && observed === 0)\n partiallyMissing.push(observed > 0 && missing > 0)\n anyMissing ||= missing > 0\n anyObserved ||= observed > 0\n }\n return {\n hasMissing: anyMissing,\n fullyMissing,\n partiallyMissing,\n allMissing: anyMissing && !anyObserved,\n }\n}\n"],"names":[],"mappings":";;AAiBO,MAAM,uBAAuB;AAO7B,MAAM,gDAAqD,IAAI;AAAA,EACpE;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAcM,SAAS,mBAAmB,OAAyB;AAC1D,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,qBAAqB,OAAO,KAAK,CAAC,EAAE,KAAA;AACpD,SAAO,YAAY,MAAM,YAAY,wBAAwB,QAAQ,kBAAkB;AACzF;AAOO,SAAS,qBAAqB,OAAuB;AAC1D,SAAO,MAAM,QAAQ,6BAA6B,EAAE,EAAE,QAAQ,0BAA0B,EAAE;AAC5F;AAMO,SAAS,oBAAoB,OAAyB;AAC3D,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,iBAAiB,CAAC,OAAO,SAAS,KAAK;AAC5D,MAAI,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAAG;AACtD,UAAM,IAAK,MAA0B;AACrC,WAAO,MAAM,QAAQ,MAAM,UAAc,OAAO,MAAM,YAAY,CAAC,OAAO,SAAS,CAAC;AAAA,EACtF;AACA,SAAO;AACT;AA+BO,SAAS,sBAAsB,QAA+D;;AACnG,QAAM,WAAW,MAAM,SAAQ,sCAAQ,SAAR,mBAAc,QAAQ,IAAI,OAAO,KAAK,WAAW,CAAA;AAChF,QAAM,eAA0B,CAAA;AAChC,QAAM,mBAA8B,CAAA;AACpC,MAAI,aAAa;AACjB,MAAI,cAAc;AAClB,aAAW,WAAW,UAAU;AAC9B,UAAM,OAAO,MAAM,QAAQ,mCAAS,IAAI,IAAI,QAAQ,OAAO,CAAA;AAC3D,QAAI,WAAW;AACf,QAAI,UAAU;AACd,eAAW,SAAS,MAAM;AACxB,UAAI,oBAAoB,KAAK,EAAG,YAAW;AAAA,UACtC,aAAY;AAAA,IACnB;AACA,iBAAa,KAAK,UAAU,KAAK,aAAa,CAAC;AAC/C,qBAAiB,KAAK,WAAW,KAAK,UAAU,CAAC;AACjD,gCAAe,UAAU;AACzB,kCAAgB,WAAW;AAAA,EAC7B;AACA,SAAO;AAAA,IACL,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA,YAAY,cAAc,CAAC;AAAA,EAAA;AAE/B;;;;;;;"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Missing values — one definition shared by tables, chart data views,
3
+ * degraded projections, chart validation and the chart renderer.
4
+ *
5
+ * @since v6.24.0
6
+ *
7
+ * A missing value is a value the payload says EXISTS but was not observed:
8
+ * `null` in a chart dataset (or as a point's `y`), `null` / `undefined` in a
9
+ * table cell. It is not a zero, and it is not the absence of a row or of a
10
+ * label: every view keeps its slot and marks it.
11
+ *
12
+ * Runtime-free on purpose (no `solid-js`): the pure projections in
13
+ * `chart-data-table.ts` and `degraded-projections.ts`, and `validation.ts`
14
+ * (published as the `/validation` subpath), import it.
15
+ */
16
+ /** The glyph a table cell shows for a missing value — `renderCellValue`'s historical `-`. */
17
+ export declare const MISSING_VALUE_MARKER = "-";
18
+ /**
19
+ * Chart types that encode a value as a share of a whole (or, for polar area,
20
+ * as a wedge whose emptiness reads as zero). Chart.js parses a `null` share
21
+ * as `0`, so these types refuse missing values instead of drawing them.
22
+ */
23
+ export declare const PART_TO_WHOLE_CHART_TYPES: ReadonlySet<string>;
24
+ /**
25
+ * Whether a raw TABLE cell value is displayed as {@link MISSING_VALUE_MARKER}.
26
+ *
27
+ * Mirrors the rules `renderCellValue` applies before any formatting — `null`,
28
+ * `undefined`, and a string that is empty or the word `undefined` once the
29
+ * backend's `"X – undefined"` debris is stripped — plus the literal marker
30
+ * itself (whitespace around it aside, which a table cell does not render),
31
+ * since it looks identical on screen and so must read identically.
32
+ *
33
+ * Objects are never missing here: `renderCellValue` renders them (link,
34
+ * name, or escaped JSON).
35
+ */
36
+ export declare function isMissingCellValue(value: unknown): boolean;
37
+ /**
38
+ * Removes the `"Text – undefined"` / `"undefined – Text"` debris some
39
+ * backends concatenate. Shared with `renderCellValue` so both apply the very
40
+ * same cleanup.
41
+ */
42
+ export declare function stripUndefinedDebris(value: string): string;
43
+ /**
44
+ * Whether one CHART data entry is missing: `null` / `undefined`, a non-finite
45
+ * number (Chart.js parses those to `null` too), or a point whose `y` is.
46
+ */
47
+ export declare function isMissingChartValue(value: unknown): boolean;
48
+ /**
49
+ * What a chart's missing values amount to.
50
+ *
51
+ * Only ENTRIES count: an explicit `null` (or non-finite number, or a point
52
+ * whose `y` is one). A dataset with no entry at all (`data: []`) is not
53
+ * "missing" — it is how 6.23.0 and earlier payloads said "nothing yet", e.g.
54
+ * while streaming, and it keeps rendering exactly as it did then.
55
+ */
56
+ export interface ChartMissingSummary {
57
+ /** At least one entry of one dataset is missing. */
58
+ hasMissing: boolean;
59
+ /** Per dataset, in order: it has entries and every one of them is missing. */
60
+ fullyMissing: boolean[];
61
+ /**
62
+ * Per dataset, in order: at least one observed value AND at least one
63
+ * missing entry — the series a reader actually sees broken.
64
+ */
65
+ partiallyMissing: boolean[];
66
+ /** Something is missing and no dataset has a single observed value: nothing to plot. */
67
+ allMissing: boolean;
68
+ }
69
+ interface ChartDataLike {
70
+ data?: {
71
+ datasets?: Array<{
72
+ data?: unknown[];
73
+ } | null | undefined>;
74
+ };
75
+ }
76
+ /** Summarises the missing values of a chart payload. Never throws on a malformed payload. */
77
+ export declare function summarizeChartMissing(params: ChartDataLike | null | undefined): ChartMissingSummary;
78
+ export {};
79
+ //# sourceMappingURL=missing-value.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-value.d.ts","sourceRoot":"","sources":["../../src/utils/missing-value.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,6FAA6F;AAC7F,eAAO,MAAM,oBAAoB,MAAM,CAAA;AAEvC;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAIxD,CAAA;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAK1D;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQ3D;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IAClC,oDAAoD;IACpD,UAAU,EAAE,OAAO,CAAA;IACnB,8EAA8E;IAC9E,YAAY,EAAE,OAAO,EAAE,CAAA;IACvB;;;OAGG;IACH,gBAAgB,EAAE,OAAO,EAAE,CAAA;IAC3B,wFAAwF;IACxF,UAAU,EAAE,OAAO,CAAA;CACpB;AAED,UAAU,aAAa;IACrB,IAAI,CAAC,EAAE;QACL,QAAQ,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,CAAC,EAAE,OAAO,EAAE,CAAA;SAAE,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;KAC1D,CAAA;CACF;AAED,6FAA6F;AAC7F,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,GAAG,mBAAmB,CAyBnG"}
@@ -0,0 +1,60 @@
1
+ const MISSING_VALUE_MARKER = "-";
2
+ const PART_TO_WHOLE_CHART_TYPES = /* @__PURE__ */ new Set([
3
+ "pie",
4
+ "doughnut",
5
+ "polarArea"
6
+ ]);
7
+ function isMissingCellValue(value) {
8
+ if (value === null || value === void 0) return true;
9
+ if (typeof value === "object") return false;
10
+ const cleaned = stripUndefinedDebris(String(value)).trim();
11
+ return cleaned === "" || cleaned === MISSING_VALUE_MARKER || cleaned.toLowerCase() === "undefined";
12
+ }
13
+ function stripUndefinedDebris(value) {
14
+ return value.replace(/\s*[–-]\s*undefined\s*$/gi, "").replace(/^undefined\s*[–-]\s*/gi, "");
15
+ }
16
+ function isMissingChartValue(value) {
17
+ if (value === null || value === void 0) return true;
18
+ if (typeof value === "number") return !Number.isFinite(value);
19
+ if (typeof value === "object" && !Array.isArray(value)) {
20
+ const y = value.y;
21
+ return y === null || y === void 0 || typeof y === "number" && !Number.isFinite(y);
22
+ }
23
+ return false;
24
+ }
25
+ function summarizeChartMissing(params) {
26
+ var _a;
27
+ const datasets = Array.isArray((_a = params == null ? void 0 : params.data) == null ? void 0 : _a.datasets) ? params.data.datasets : [];
28
+ const fullyMissing = [];
29
+ const partiallyMissing = [];
30
+ let anyMissing = false;
31
+ let anyObserved = false;
32
+ for (const dataset of datasets) {
33
+ const data = Array.isArray(dataset == null ? void 0 : dataset.data) ? dataset.data : [];
34
+ let observed = 0;
35
+ let missing = 0;
36
+ for (const value of data) {
37
+ if (isMissingChartValue(value)) missing += 1;
38
+ else observed += 1;
39
+ }
40
+ fullyMissing.push(missing > 0 && observed === 0);
41
+ partiallyMissing.push(observed > 0 && missing > 0);
42
+ anyMissing || (anyMissing = missing > 0);
43
+ anyObserved || (anyObserved = observed > 0);
44
+ }
45
+ return {
46
+ hasMissing: anyMissing,
47
+ fullyMissing,
48
+ partiallyMissing,
49
+ allMissing: anyMissing && !anyObserved
50
+ };
51
+ }
52
+ export {
53
+ MISSING_VALUE_MARKER,
54
+ PART_TO_WHOLE_CHART_TYPES,
55
+ isMissingCellValue,
56
+ isMissingChartValue,
57
+ stripUndefinedDebris,
58
+ summarizeChartMissing
59
+ };
60
+ //# sourceMappingURL=missing-value.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-value.js","sources":["../../src/utils/missing-value.ts"],"sourcesContent":["/**\n * Missing values — one definition shared by tables, chart data views,\n * degraded projections, chart validation and the chart renderer.\n *\n * @since v6.24.0\n *\n * A missing value is a value the payload says EXISTS but was not observed:\n * `null` in a chart dataset (or as a point's `y`), `null` / `undefined` in a\n * table cell. It is not a zero, and it is not the absence of a row or of a\n * label: every view keeps its slot and marks it.\n *\n * Runtime-free on purpose (no `solid-js`): the pure projections in\n * `chart-data-table.ts` and `degraded-projections.ts`, and `validation.ts`\n * (published as the `/validation` subpath), import it.\n */\n\n/** The glyph a table cell shows for a missing value — `renderCellValue`'s historical `-`. */\nexport const MISSING_VALUE_MARKER = '-'\n\n/**\n * Chart types that encode a value as a share of a whole (or, for polar area,\n * as a wedge whose emptiness reads as zero). Chart.js parses a `null` share\n * as `0`, so these types refuse missing values instead of drawing them.\n */\nexport const PART_TO_WHOLE_CHART_TYPES: ReadonlySet<string> = new Set([\n 'pie',\n 'doughnut',\n 'polarArea',\n])\n\n/**\n * Whether a raw TABLE cell value is displayed as {@link MISSING_VALUE_MARKER}.\n *\n * Mirrors the rules `renderCellValue` applies before any formatting — `null`,\n * `undefined`, and a string that is empty or the word `undefined` once the\n * backend's `\"X – undefined\"` debris is stripped — plus the literal marker\n * itself (whitespace around it aside, which a table cell does not render),\n * since it looks identical on screen and so must read identically.\n *\n * Objects are never missing here: `renderCellValue` renders them (link,\n * name, or escaped JSON).\n */\nexport function isMissingCellValue(value: unknown): boolean {\n if (value === null || value === undefined) return true\n if (typeof value === 'object') return false\n const cleaned = stripUndefinedDebris(String(value)).trim()\n return cleaned === '' || cleaned === MISSING_VALUE_MARKER || cleaned.toLowerCase() === 'undefined'\n}\n\n/**\n * Removes the `\"Text – undefined\"` / `\"undefined – Text\"` debris some\n * backends concatenate. Shared with `renderCellValue` so both apply the very\n * same cleanup.\n */\nexport function stripUndefinedDebris(value: string): string {\n return value.replace(/\\s*[–-]\\s*undefined\\s*$/gi, '').replace(/^undefined\\s*[–-]\\s*/gi, '')\n}\n\n/**\n * Whether one CHART data entry is missing: `null` / `undefined`, a non-finite\n * number (Chart.js parses those to `null` too), or a point whose `y` is.\n */\nexport function isMissingChartValue(value: unknown): boolean {\n if (value === null || value === undefined) return true\n if (typeof value === 'number') return !Number.isFinite(value)\n if (typeof value === 'object' && !Array.isArray(value)) {\n const y = (value as { y?: unknown }).y\n return y === null || y === undefined || (typeof y === 'number' && !Number.isFinite(y))\n }\n return false\n}\n\n/**\n * What a chart's missing values amount to.\n *\n * Only ENTRIES count: an explicit `null` (or non-finite number, or a point\n * whose `y` is one). A dataset with no entry at all (`data: []`) is not\n * \"missing\" — it is how 6.23.0 and earlier payloads said \"nothing yet\", e.g.\n * while streaming, and it keeps rendering exactly as it did then.\n */\nexport interface ChartMissingSummary {\n /** At least one entry of one dataset is missing. */\n hasMissing: boolean\n /** Per dataset, in order: it has entries and every one of them is missing. */\n fullyMissing: boolean[]\n /**\n * Per dataset, in order: at least one observed value AND at least one\n * missing entry — the series a reader actually sees broken.\n */\n partiallyMissing: boolean[]\n /** Something is missing and no dataset has a single observed value: nothing to plot. */\n allMissing: boolean\n}\n\ninterface ChartDataLike {\n data?: {\n datasets?: Array<{ data?: unknown[] } | null | undefined>\n }\n}\n\n/** Summarises the missing values of a chart payload. Never throws on a malformed payload. */\nexport function summarizeChartMissing(params: ChartDataLike | null | undefined): ChartMissingSummary {\n const datasets = Array.isArray(params?.data?.datasets) ? params.data.datasets : []\n const fullyMissing: boolean[] = []\n const partiallyMissing: boolean[] = []\n let anyMissing = false\n let anyObserved = false\n for (const dataset of datasets) {\n const data = Array.isArray(dataset?.data) ? dataset.data : []\n let observed = 0\n let missing = 0\n for (const value of data) {\n if (isMissingChartValue(value)) missing += 1\n else observed += 1\n }\n fullyMissing.push(missing > 0 && observed === 0)\n partiallyMissing.push(observed > 0 && missing > 0)\n anyMissing ||= missing > 0\n anyObserved ||= observed > 0\n }\n return {\n hasMissing: anyMissing,\n fullyMissing,\n partiallyMissing,\n allMissing: anyMissing && !anyObserved,\n }\n}\n"],"names":[],"mappings":"AAiBO,MAAM,uBAAuB;AAO7B,MAAM,gDAAqD,IAAI;AAAA,EACpE;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAcM,SAAS,mBAAmB,OAAyB;AAC1D,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,qBAAqB,OAAO,KAAK,CAAC,EAAE,KAAA;AACpD,SAAO,YAAY,MAAM,YAAY,wBAAwB,QAAQ,kBAAkB;AACzF;AAOO,SAAS,qBAAqB,OAAuB;AAC1D,SAAO,MAAM,QAAQ,6BAA6B,EAAE,EAAE,QAAQ,0BAA0B,EAAE;AAC5F;AAMO,SAAS,oBAAoB,OAAyB;AAC3D,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,iBAAiB,CAAC,OAAO,SAAS,KAAK;AAC5D,MAAI,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAAG;AACtD,UAAM,IAAK,MAA0B;AACrC,WAAO,MAAM,QAAQ,MAAM,UAAc,OAAO,MAAM,YAAY,CAAC,OAAO,SAAS,CAAC;AAAA,EACtF;AACA,SAAO;AACT;AA+BO,SAAS,sBAAsB,QAA+D;AApF9F;AAqFL,QAAM,WAAW,MAAM,SAAQ,sCAAQ,SAAR,mBAAc,QAAQ,IAAI,OAAO,KAAK,WAAW,CAAA;AAChF,QAAM,eAA0B,CAAA;AAChC,QAAM,mBAA8B,CAAA;AACpC,MAAI,aAAa;AACjB,MAAI,cAAc;AAClB,aAAW,WAAW,UAAU;AAC9B,UAAM,OAAO,MAAM,QAAQ,mCAAS,IAAI,IAAI,QAAQ,OAAO,CAAA;AAC3D,QAAI,WAAW;AACf,QAAI,UAAU;AACd,eAAW,SAAS,MAAM;AACxB,UAAI,oBAAoB,KAAK,EAAG,YAAW;AAAA,UACtC,aAAY;AAAA,IACnB;AACA,iBAAa,KAAK,UAAU,KAAK,aAAa,CAAC;AAC/C,qBAAiB,KAAK,WAAW,KAAK,UAAU,CAAC;AACjD,gCAAe,UAAU;AACzB,kCAAgB,WAAW;AAAA,EAC7B;AACA,SAAO;AAAA,IACL,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA,YAAY,cAAc,CAAC;AAAA,EAAA;AAE/B;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-ship/mcp-ui-solid",
3
- "version": "6.23.0",
3
+ "version": "6.24.0",
4
4
  "description": "SolidJS components for rendering MCP-generated UI resources",
5
5
  "type": "module",
6
6
  "files": [
@@ -229,7 +229,7 @@
229
229
  }
230
230
  },
231
231
  "dependencies": {
232
- "@seed-ship/mcp-ui-spec": "^5.6.0",
232
+ "@seed-ship/mcp-ui-spec": "^5.7.0",
233
233
  "@types/dompurify": "^3.0.5",
234
234
  "dompurify": "^3.4.12",
235
235
  "marked": "^16.3.0",
@@ -289,7 +289,7 @@
289
289
  "test:watch": "vitest",
290
290
  "test:browser": "vitest --mode browser",
291
291
  "lint": "eslint src",
292
- "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.contracts.json",
292
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.contracts.json && tsc --noEmit -p tsconfig.examples.json",
293
293
  "clean": "rm -rf dist",
294
294
  "size": "size-limit",
295
295
  "size:analyze": "size-limit --why"
@@ -32,7 +32,17 @@ import type { Accessor, JSX } from 'solid-js';
32
32
  import type { UIComponent, ChartComponentParams } from '../types';
33
33
  import { ExpandableWrapper, useExpanded } from './ExpandableWrapper';
34
34
  import { DegradedFallback } from './DegradedFallback';
35
+ import { MissingValueLegend, MissingValueMark } from './MissingValue';
35
36
  import { chartToDataTable } from './chart-data-table';
37
+ import {
38
+ applyUnitAxisTitle,
39
+ createMissingLegendGenerator,
40
+ createMissingValuesPlugin,
41
+ createTooltipLabel,
42
+ missingNoteKind,
43
+ withDatasetsNoGapSpan,
44
+ } from './chart-missing-plugin';
45
+ import { PART_TO_WHOLE_CHART_TYPES, summarizeChartMissing } from '../utils/missing-value';
36
46
  import { useTelemetry } from '../context/MCPUITelemetryContext';
37
47
  import { useMCPUIConfig } from '../context/MCPUIConfigContext';
38
48
  import { formatMCPUIString, useMCPUIStrings } from '../context/MCPUIStringsContext';
@@ -320,9 +330,51 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
320
330
  point: strings.chartTablePoint,
321
331
  label: strings.degradedColLabel,
322
332
  seriesName: strings.degradedSeries,
333
+ withUnit: strings.chartLabelWithUnit,
323
334
  })
324
335
  );
336
+ // Absence is never zero: which entries are missing, and which series have
337
+ // nothing at all. Drives the notes, the plugin, the legend and the guard.
338
+ const missingSummary = createMemo(() => summarizeChartMissing(params()));
339
+ const unit = () => {
340
+ const value = params().unit;
341
+ return typeof value === 'string' && value !== '' ? value : undefined;
342
+ };
343
+ const anyMissingCell = () => tableData().missing.some((row) => row.some(Boolean));
344
+ // Never for a part-to-whole chart: it refuses missing values outright (the
345
+ // render effect turns it into the data table), so no convention applies —
346
+ // and the async guard must not leave a wrong note in the SSR markup.
347
+ const notesShown = () =>
348
+ missingSummary().hasMissing &&
349
+ !PART_TO_WHOLE_CHART_TYPES.has(params().type) &&
350
+ activeView() === 'chart' &&
351
+ !error();
352
+ const noteKind = () => missingNoteKind(params().type, params().options);
353
+ // The convention line explains a mark the reader can SEE: every missing bar
354
+ // gets a dashed stub, but a line or a scatter only shows an absence inside a
355
+ // series that still has values — a series with no value at all draws
356
+ // nothing, and the "no data" line alone covers it.
357
+ const conventionNoteShown = () =>
358
+ noteKind() === 'bars'
359
+ ? missingSummary().hasMissing
360
+ : missingSummary().partiallyMissing.some(Boolean);
361
+ // The series the chart has no data for, as a locale-aware list.
362
+ const noDataSeries = () => {
363
+ const names = (params().data?.datasets ?? [])
364
+ .map((dataset, index) =>
365
+ missingSummary().fullyMissing[index]
366
+ ? dataset?.label || formatMCPUIString(strings.degradedSeries, { n: index + 1 })
367
+ : null
368
+ )
369
+ .filter((name): name is string => name !== null);
370
+ try {
371
+ return new Intl.ListFormat(strings.locale, { type: 'conjunction' }).format(names);
372
+ } catch {
373
+ return names.join(', ');
374
+ }
375
+ };
325
376
  const descriptionId = createUniqueId();
377
+ const notesId = createUniqueId();
326
378
  const title = () => params().title || strings.chartView;
327
379
 
328
380
  // v6.1.0 — export visibility :
@@ -456,6 +508,8 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
456
508
  // plugin options at construction and the plugin only hooks charts created
457
509
  // after it was registered).
458
510
  const chartParams = params();
511
+ const chartMissing = missingSummary();
512
+ const chartUnit = unit();
459
513
  const zoom = zoomOptions();
460
514
  const version = ++renderVersion;
461
515
 
@@ -474,6 +528,14 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
474
528
  chartInstance = null;
475
529
  }
476
530
 
531
+ // A share of a whole cannot be missing: Chart.js parses `null` as 0 and
532
+ // would draw it as a zero slice. Validation rejects this earlier; this
533
+ // is the guard for a host that bypassed it. Thrown here so the catch
534
+ // below shows the data table (with the missing cells marked).
535
+ if (chartMissing.hasMissing && PART_TO_WHOLE_CHART_TYPES.has(chartParams.type)) {
536
+ throw new Error(strings.chartMissingUnsupported);
537
+ }
538
+
477
539
  // Build options, merging time-axis config if present (v3.1.0)
478
540
  const hostZoomOptions = chartParams.options?.plugins?.zoom;
479
541
  const baseOptions: any = {
@@ -517,12 +579,69 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
517
579
  };
518
580
  }
519
581
 
520
- // Create new chart
521
- chartInstance = new Chart(canvasRef, {
582
+ // Unit (v6.24.0): value-axis title and tooltip suffix. Both are no-ops
583
+ // on a payload without a unit.
584
+ applyUnitAxisTitle(baseOptions, chartParams.type, chartUnit);
585
+
586
+ const chartConfig: any = {
522
587
  type: chartParams.type,
523
588
  data: chartParams.data,
524
589
  options: baseOptions,
525
- });
590
+ };
591
+
592
+ if (chartUnit !== undefined || chartMissing.hasMissing) {
593
+ const tooltip = baseOptions.plugins.tooltip;
594
+ // `tooltip: false` (or `null`) is Chart.js's per-chart plugin opt-out:
595
+ // the payload turned tooltips off, and adding a callback object here
596
+ // would turn them back on. A host-supplied label callback is the
597
+ // host's word too. Both are left exactly as written.
598
+ if (tooltip !== false && tooltip !== null && !tooltip?.callbacks?.label) {
599
+ baseOptions.plugins.tooltip = {
600
+ ...tooltip,
601
+ callbacks: {
602
+ ...tooltip?.callbacks,
603
+ label: createTooltipLabel(() => strings, chartUnit, chartParams.type),
604
+ },
605
+ };
606
+ }
607
+ }
608
+
609
+ if (chartMissing.hasMissing) {
610
+ // A line must break at a missing value, not join its neighbours.
611
+ chartConfig.data = withDatasetsNoGapSpan(chartParams.data);
612
+
613
+ // Legend of a fully-missing series: "{series} (no data)". Dataset
614
+ // legends only — pie-like legends list categories, not series.
615
+ const defaultGenerate =
616
+ baseOptions.plugins.legend?.labels?.generateLabels ??
617
+ Chart?.defaults?.plugins?.legend?.labels?.generateLabels;
618
+ if (typeof defaultGenerate === 'function') {
619
+ baseOptions.plugins.legend = {
620
+ ...baseOptions.plugins.legend,
621
+ labels: {
622
+ ...baseOptions.plugins.legend?.labels,
623
+ generateLabels: createMissingLegendGenerator(
624
+ defaultGenerate,
625
+ chartMissing.fullyMissing,
626
+ () => strings
627
+ ),
628
+ },
629
+ };
630
+ }
631
+
632
+ // Per-chart plugin (never `Chart.register`): the dashed stub of a
633
+ // missing bar, and "no data" on a chart with nothing to plot.
634
+ chartConfig.plugins = [
635
+ createMissingValuesPlugin({
636
+ allMissing: chartMissing.allMissing,
637
+ getNoDataText: () => strings.chartNoData,
638
+ getFontFamily: () => Chart?.defaults?.font?.family,
639
+ }),
640
+ ];
641
+ }
642
+
643
+ // Create new chart
644
+ chartInstance = new Chart(canvasRef, chartConfig);
526
645
 
527
646
  // A freshly built chart always sits on its initial viewport, so the
528
647
  // reset control goes away until the user zooms again.
@@ -761,12 +880,17 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
761
880
  </thead>
762
881
  <tbody>
763
882
  <For each={tableData().rows}>
764
- {(row) => (
883
+ {(row, rowIndex) => (
765
884
  <tr class="border-t border-gray-100 dark:border-gray-700">
766
885
  <For each={tableData().columns}>
767
886
  {(_column, index) => (
768
887
  <td class="px-3 py-2 text-gray-700 dark:text-gray-300">
769
- {String(row[index()] ?? '')}
888
+ <Show
889
+ when={tableData().missing[rowIndex()]?.[index()]}
890
+ fallback={String(row[index()] ?? '')}
891
+ >
892
+ <MissingValueMark />
893
+ </Show>
770
894
  </td>
771
895
  )}
772
896
  </For>
@@ -786,6 +910,9 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
786
910
  </tbody>
787
911
  </table>
788
912
  </div>
913
+ <Show when={anyMissingCell()}>
914
+ <MissingValueLegend class="mt-2" />
915
+ </Show>
789
916
  </Show>
790
917
 
791
918
  <div
@@ -802,7 +929,7 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
802
929
  ref={canvasRef}
803
930
  role="img"
804
931
  aria-label={title()}
805
- aria-describedby={descriptionId}
932
+ aria-describedby={notesShown() ? `${descriptionId} ${notesId}` : descriptionId}
806
933
  aria-hidden={activeView() !== 'chart' || Boolean(error())}
807
934
  />
808
935
  <Show when={isLoading() && activeView() === 'chart'}>
@@ -814,6 +941,32 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
814
941
  </div>
815
942
  </Show>
816
943
  </div>
944
+
945
+ {/* Missing values (v6.24.0): how THIS chart type shows them, and which
946
+ series have no data at all. Linked from the canvas, so a screen
947
+ reader hears the convention with the chart. */}
948
+ <Show when={notesShown()}>
949
+ <div
950
+ data-mcp-chart-notes=""
951
+ id={notesId}
952
+ class="mt-2 space-y-0.5 text-xs text-gray-600 dark:text-gray-400"
953
+ >
954
+ <Show when={conventionNoteShown()}>
955
+ <p data-mcp-chart-note={noteKind()}>
956
+ {noteKind() === 'bars'
957
+ ? strings.chartMissingBars
958
+ : noteKind() === 'omitted'
959
+ ? strings.chartMissingPoints
960
+ : strings.chartMissingGaps}
961
+ </p>
962
+ </Show>
963
+ <Show when={missingSummary().fullyMissing.some(Boolean)}>
964
+ <p data-mcp-chart-note="no-data">
965
+ {formatMCPUIString(strings.chartSeriesNoData, { series: noDataSeries() })}
966
+ </p>
967
+ </Show>
968
+ </div>
969
+ </Show>
817
970
  </div>
818
971
  )}
819
972
  />