@seed-ship/mcp-ui-solid 6.22.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.
- package/CHANGELOG.md +219 -0
- package/README.md +209 -2
- package/dist/components/ChartJSRenderer.cjs +192 -66
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +192 -66
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/DegradedFallback.cjs +34 -2
- package/dist/components/DegradedFallback.cjs.map +1 -1
- package/dist/components/DegradedFallback.d.ts +7 -0
- package/dist/components/DegradedFallback.d.ts.map +1 -1
- package/dist/components/DegradedFallback.js +34 -2
- package/dist/components/DegradedFallback.js.map +1 -1
- package/dist/components/MissingValue.cjs +43 -0
- package/dist/components/MissingValue.cjs.map +1 -0
- package/dist/components/MissingValue.d.ts +27 -0
- package/dist/components/MissingValue.d.ts.map +1 -0
- package/dist/components/MissingValue.js +43 -0
- package/dist/components/MissingValue.js.map +1 -0
- package/dist/components/UIResourceRenderer.cjs +250 -202
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts +2 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +250 -202
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/components/chart-data-table.cjs +30 -5
- package/dist/components/chart-data-table.cjs.map +1 -1
- package/dist/components/chart-data-table.d.ts +18 -1
- package/dist/components/chart-data-table.d.ts.map +1 -1
- package/dist/components/chart-data-table.js +30 -5
- package/dist/components/chart-data-table.js.map +1 -1
- package/dist/components/chart-missing-plugin.cjs +194 -0
- package/dist/components/chart-missing-plugin.cjs.map +1 -0
- package/dist/components/chart-missing-plugin.d.ts +127 -0
- package/dist/components/chart-missing-plugin.d.ts.map +1 -0
- package/dist/components/chart-missing-plugin.js +194 -0
- package/dist/components/chart-missing-plugin.js.map +1 -0
- package/dist/context/MCPUIConfigContext.cjs.map +1 -1
- package/dist/context/MCPUIConfigContext.d.ts +57 -28
- package/dist/context/MCPUIConfigContext.d.ts.map +1 -1
- package/dist/context/MCPUIConfigContext.js.map +1 -1
- package/dist/context/MCPUIMathContext.cjs +19 -0
- package/dist/context/MCPUIMathContext.cjs.map +1 -0
- package/dist/context/MCPUIMathContext.d.ts +18 -0
- package/dist/context/MCPUIMathContext.d.ts.map +1 -0
- package/dist/context/MCPUIMathContext.js +19 -0
- package/dist/context/MCPUIMathContext.js.map +1 -0
- package/dist/context/MCPUIStringsContext.cjs +12 -1
- package/dist/context/MCPUIStringsContext.cjs.map +1 -1
- package/dist/context/MCPUIStringsContext.d.ts +62 -0
- package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
- package/dist/context/MCPUIStringsContext.js +12 -1
- package/dist/context/MCPUIStringsContext.js.map +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins/katex.cjs +23 -0
- package/dist/plugins/katex.cjs.map +1 -0
- package/dist/plugins/katex.d.cts +7 -0
- package/dist/plugins/katex.d.ts +7 -0
- package/dist/plugins/katex.d.ts.map +1 -0
- package/dist/plugins/katex.js +23 -0
- package/dist/plugins/katex.js.map +1 -0
- package/dist/services/component-registry.cjs +9 -2
- package/dist/services/component-registry.cjs.map +1 -1
- package/dist/services/component-registry.d.ts.map +1 -1
- package/dist/services/component-registry.js +9 -2
- package/dist/services/component-registry.js.map +1 -1
- package/dist/services/validation.cjs +31 -8
- package/dist/services/validation.cjs.map +1 -1
- package/dist/services/validation.d.ts.map +1 -1
- package/dist/services/validation.js +31 -8
- package/dist/services/validation.js.map +1 -1
- package/dist/types/index.d.ts +14 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/degraded-projections.cjs +19 -3
- package/dist/utils/degraded-projections.cjs.map +1 -1
- package/dist/utils/degraded-projections.d.ts +22 -2
- package/dist/utils/degraded-projections.d.ts.map +1 -1
- package/dist/utils/degraded-projections.js +19 -3
- package/dist/utils/degraded-projections.js.map +1 -1
- package/dist/utils/iframe-coep.cjs.map +1 -1
- package/dist/utils/iframe-coep.d.ts +7 -6
- package/dist/utils/iframe-coep.d.ts.map +1 -1
- package/dist/utils/iframe-coep.js.map +1 -1
- package/dist/utils/math-markdown.cjs +240 -0
- package/dist/utils/math-markdown.cjs.map +1 -0
- package/dist/utils/math-markdown.d.ts +16 -0
- package/dist/utils/math-markdown.d.ts.map +1 -0
- package/dist/utils/math-markdown.js +240 -0
- package/dist/utils/math-markdown.js.map +1 -0
- package/dist/utils/missing-value.cjs +60 -0
- package/dist/utils/missing-value.cjs.map +1 -0
- package/dist/utils/missing-value.d.ts +79 -0
- package/dist/utils/missing-value.d.ts.map +1 -0
- package/dist/utils/missing-value.js +60 -0
- package/dist/utils/missing-value.js.map +1 -0
- package/package.json +23 -4
- package/src/components/ChartJSRenderer.tsx +159 -6
- package/src/components/DegradedFallback.tsx +24 -2
- package/src/components/MissingValue.tsx +61 -0
- package/src/components/UIResourceRenderer.tsx +64 -10
- package/src/components/chart-data-table.ts +50 -6
- package/src/components/chart-missing-plugin.ts +334 -0
- package/src/context/MCPUIConfigContext.tsx +57 -28
- package/src/context/MCPUIMathContext.tsx +32 -0
- package/src/context/MCPUIStringsContext.tsx +76 -0
- package/src/index.ts +4 -0
- package/src/plugins/katex.ts +25 -0
- package/src/services/component-registry.ts +10 -2
- package/src/services/validation.ts +44 -9
- package/src/types/index.ts +13 -1
- package/src/utils/degraded-projections.ts +44 -4
- package/src/utils/iframe-coep.ts +7 -6
- package/src/utils/math-markdown.ts +222 -0
- package/src/utils/missing-value.ts +127 -0
|
@@ -0,0 +1,334 @@
|
|
|
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
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A dataset colour for entry `index`, as a string, or `undefined`. */
|
|
215
|
+
function colorAt(color: unknown, index: number): string | undefined {
|
|
216
|
+
if (typeof color === 'string' && color !== '') return color;
|
|
217
|
+
if (Array.isArray(color) && color.length > 0) {
|
|
218
|
+
const picked = color[index % color.length];
|
|
219
|
+
return typeof picked === 'string' && picked !== '' ? picked : undefined;
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Draws a dashed outline stub for every missing bar of every visible,
|
|
226
|
+
* non-stacked bar dataset, so a missing value is a visible mark, not a bar
|
|
227
|
+
* that "is" zero.
|
|
228
|
+
*
|
|
229
|
+
* The stub sits on the value-axis base, as thick as the bar would have been,
|
|
230
|
+
* and runs {@link MISSING_BAR_STUB_LENGTH}px toward the middle of the chart
|
|
231
|
+
* area (up from a bottom baseline, down from a top one, right from a left
|
|
232
|
+
* one). Clipped to the chart area.
|
|
233
|
+
*/
|
|
234
|
+
export function drawMissingBarStubs(chart: any): void {
|
|
235
|
+
const ctx = chart?.ctx;
|
|
236
|
+
const area = chart?.chartArea;
|
|
237
|
+
const datasets = chart?.data?.datasets;
|
|
238
|
+
if (!ctx || !area || !Array.isArray(datasets)) return;
|
|
239
|
+
|
|
240
|
+
const left = area.left;
|
|
241
|
+
const top = area.top;
|
|
242
|
+
const right = area.right ?? left + (area.width ?? 0);
|
|
243
|
+
const bottom = area.bottom ?? top + (area.height ?? 0);
|
|
244
|
+
|
|
245
|
+
let saved = false;
|
|
246
|
+
datasets.forEach((dataset: any, datasetIndex: number) => {
|
|
247
|
+
const meta = chart.getDatasetMeta?.(datasetIndex);
|
|
248
|
+
if (!meta) return;
|
|
249
|
+
const isBar = meta.type === 'bar' || meta.controller?.constructor?.id === 'bar';
|
|
250
|
+
if (!isBar) return;
|
|
251
|
+
// Stacked or overlaid bars: the baseline under a missing segment is the
|
|
252
|
+
// series below, so an outline there would point at the wrong series.
|
|
253
|
+
if (meta.vScale?.options?.stacked || meta.iScale?.options?.stacked) return;
|
|
254
|
+
if (typeof chart.isDatasetVisible === 'function' && !chart.isDatasetVisible(datasetIndex)) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const values: unknown[] = Array.isArray(dataset?.data) ? dataset.data : [];
|
|
258
|
+
const elements: any[] = Array.isArray(meta.data) ? meta.data : [];
|
|
259
|
+
|
|
260
|
+
values.forEach((value, index) => {
|
|
261
|
+
if (!isMissingChartValue(value)) return;
|
|
262
|
+
const element = elements[index];
|
|
263
|
+
if (!element) return;
|
|
264
|
+
const props = element.getProps?.(['x', 'y', 'base', 'width', 'height'], true) ?? element;
|
|
265
|
+
const { x, y, base, width, height } = props;
|
|
266
|
+
if (![x, y, base].every((n) => Number.isFinite(n))) return;
|
|
267
|
+
const horizontal = element.horizontal === true;
|
|
268
|
+
const thickness = horizontal ? height : width;
|
|
269
|
+
if (!Number.isFinite(thickness) || thickness <= 0) return;
|
|
270
|
+
|
|
271
|
+
let rect: [number, number, number, number];
|
|
272
|
+
if (horizontal) {
|
|
273
|
+
const dir = base < (left + right) / 2 ? 1 : -1;
|
|
274
|
+
rect = [Math.min(base, base + dir * MISSING_BAR_STUB_LENGTH), y - thickness / 2, MISSING_BAR_STUB_LENGTH, thickness];
|
|
275
|
+
} else {
|
|
276
|
+
const dir = base > (top + bottom) / 2 ? -1 : 1;
|
|
277
|
+
rect = [x - thickness / 2, Math.min(base, base + dir * MISSING_BAR_STUB_LENGTH), thickness, MISSING_BAR_STUB_LENGTH];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!saved) {
|
|
281
|
+
ctx.save();
|
|
282
|
+
ctx.beginPath();
|
|
283
|
+
ctx.rect(left, top, right - left, bottom - top);
|
|
284
|
+
ctx.clip();
|
|
285
|
+
saved = true;
|
|
286
|
+
}
|
|
287
|
+
ctx.strokeStyle = colorAt(dataset?.borderColor, index) ?? MISSING_BAR_FALLBACK_COLOR;
|
|
288
|
+
ctx.lineWidth = 1;
|
|
289
|
+
ctx.setLineDash(STUB_DASH);
|
|
290
|
+
ctx.strokeRect(...rect);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
if (saved) {
|
|
294
|
+
ctx.setLineDash?.([]);
|
|
295
|
+
ctx.restore();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Draws `text` centred in the chart area (a chart with nothing to plot). */
|
|
300
|
+
export function drawNoDataText(chart: any, text: string, fontFamily?: string): void {
|
|
301
|
+
const ctx = chart?.ctx;
|
|
302
|
+
const area = chart?.chartArea;
|
|
303
|
+
if (!ctx || !area || !text) return;
|
|
304
|
+
const cx = (area.left + (area.right ?? area.left + (area.width ?? 0))) / 2;
|
|
305
|
+
const cy = (area.top + (area.bottom ?? area.top + (area.height ?? 0))) / 2;
|
|
306
|
+
ctx.save();
|
|
307
|
+
ctx.font = `12px ${fontFamily || 'sans-serif'}`;
|
|
308
|
+
ctx.fillStyle = NO_DATA_TEXT_COLOR;
|
|
309
|
+
ctx.textAlign = 'center';
|
|
310
|
+
ctx.textBaseline = 'middle';
|
|
311
|
+
ctx.fillText(text, cx, cy);
|
|
312
|
+
ctx.restore();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The per-chart plugin object. Passed as `config.plugins = [plugin]`: Chart.js
|
|
317
|
+
* scopes it to that one chart. Both hooks swallow their own errors — a
|
|
318
|
+
* decoration must never take the chart down.
|
|
319
|
+
*/
|
|
320
|
+
export function createMissingValuesPlugin(options: MissingValuesPluginOptions) {
|
|
321
|
+
return {
|
|
322
|
+
id: 'mcpuiMissingValues',
|
|
323
|
+
afterDatasetsDraw(chart: any) {
|
|
324
|
+
try {
|
|
325
|
+
drawMissingBarStubs(chart);
|
|
326
|
+
if (options.allMissing) {
|
|
327
|
+
drawNoDataText(chart, options.getNoDataText(), options.getFontFamily?.());
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
// Cosmetic layer: never break the chart.
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
@@ -43,24 +43,34 @@ import type { IframePolicy } from '../types'
|
|
|
43
43
|
* Host-level rendering policy. Every field has a default — a provider passes
|
|
44
44
|
* a partial override.
|
|
45
45
|
*
|
|
46
|
-
* ##
|
|
47
|
-
*
|
|
48
|
-
* This
|
|
49
|
-
*
|
|
50
|
-
* break
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* `
|
|
61
|
-
*
|
|
46
|
+
* ## Two names, because there are two jobs
|
|
47
|
+
*
|
|
48
|
+
* This type grows a key every time the library gains a policy switch, and
|
|
49
|
+
* those releases are MINOR — so neither AUTHORING a config nor READING one
|
|
50
|
+
* may break when a key appears. Those two demands pull in opposite
|
|
51
|
+
* directions, and one name cannot serve both:
|
|
52
|
+
*
|
|
53
|
+
* - an author must be allowed to omit the new key, so the fields they write
|
|
54
|
+
* have to be OPTIONAL;
|
|
55
|
+
* - a reader assigning `const p: IframePolicy = config.iframePolicy` must not
|
|
56
|
+
* suddenly face `undefined`, so the fields they read have to be REQUIRED.
|
|
57
|
+
*
|
|
58
|
+
* Hence {@link MCPUIConfigInput} (what a host writes — every field optional)
|
|
59
|
+
* and {@link MCPUIConfig} (what the library hands back — every field
|
|
60
|
+
* resolved). `MCPUIConfig` is derived from the input type, so the two can
|
|
61
|
+
* never drift and a key is declared exactly once.
|
|
62
|
+
*
|
|
63
|
+
* Which one you name depends on the surface:
|
|
64
|
+
*
|
|
65
|
+
* - `<MCPUIConfigProvider config={…}>` takes `MCPUIConfigInput` — name that
|
|
66
|
+
* type for a partial you build, and it merges the rest;
|
|
67
|
+
* - `<MCPUIConfigContext.Provider value={…}>` takes the RESOLVED
|
|
68
|
+
* `MCPUIConfig`, because that context is read as well as written, so a
|
|
69
|
+
* direct provider hands over `{ ...DEFAULT_MCPUI_CONFIG, ...mine }`;
|
|
70
|
+
* - anything you RECEIVE is `MCPUIConfig`, which is what
|
|
71
|
+
* {@link useMCPUIConfig} returns.
|
|
62
72
|
*/
|
|
63
|
-
export interface
|
|
73
|
+
export interface MCPUIConfigInput {
|
|
64
74
|
/**
|
|
65
75
|
* When the library puts the boolean `credentialless` attribute on an
|
|
66
76
|
* `<iframe>` it renders.
|
|
@@ -87,14 +97,14 @@ export interface MCPUIConfig {
|
|
|
87
97
|
*
|
|
88
98
|
* This only RECLASSIFIES a host the allow-list already accepts. To render an
|
|
89
99
|
* iframe whose host is outside `DEFAULT_IFRAME_DOMAINS`, add it to
|
|
90
|
-
* {@link
|
|
100
|
+
* {@link MCPUIConfigInput.customIframeDomains} with `iframePolicy: 'extend'`.
|
|
91
101
|
*/
|
|
92
102
|
customTrustedIframeDomains?: string[]
|
|
93
103
|
|
|
94
104
|
/**
|
|
95
105
|
* How `UIResourceRenderer` validates an `iframe` component's host:
|
|
96
106
|
* `'strict'` (default) accepts `DEFAULT_IFRAME_DOMAINS` only, `'extend'`
|
|
97
|
-
* also accepts {@link
|
|
107
|
+
* also accepts {@link MCPUIConfigInput.customIframeDomains}.
|
|
98
108
|
*/
|
|
99
109
|
iframePolicy?: IframePolicy
|
|
100
110
|
|
|
@@ -141,17 +151,27 @@ export interface MCPUIConfig {
|
|
|
141
151
|
chartZoom?: 'expanded' | 'always' | 'never'
|
|
142
152
|
}
|
|
143
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The resolved host policy: every key present. This is what the library hands
|
|
156
|
+
* back, so a reader never null-checks a policy.
|
|
157
|
+
*
|
|
158
|
+
* Extends `Required<`{@link MCPUIConfigInput}`>` rather than declaring the
|
|
159
|
+
* fields again, so a key exists in exactly one place and the two views cannot
|
|
160
|
+
* drift. It remains an interface so existing consumers can extend or augment
|
|
161
|
+
* the public contract as they could before 6.22.0.
|
|
162
|
+
*/
|
|
163
|
+
export interface MCPUIConfig extends Required<MCPUIConfigInput> {}
|
|
164
|
+
|
|
144
165
|
/**
|
|
145
166
|
* Defaults. Chosen so a host that sends no COEP header sees no change from
|
|
146
167
|
* 6.20.0 apart from the fallback link, which `'auto'` keeps hidden there —
|
|
147
168
|
* and, since 6.22.0, so no inline component ever captures the wheel
|
|
148
169
|
* (`chartZoom: 'expanded'`).
|
|
149
170
|
*
|
|
150
|
-
* Typed `
|
|
151
|
-
*
|
|
152
|
-
* new key from shipping without a default.
|
|
171
|
+
* Typed `MCPUIConfig` (i.e. resolved): that annotation is what stops a new
|
|
172
|
+
* key from shipping without a default.
|
|
153
173
|
*/
|
|
154
|
-
export const DEFAULT_MCPUI_CONFIG:
|
|
174
|
+
export const DEFAULT_MCPUI_CONFIG: MCPUIConfig = {
|
|
155
175
|
iframeCredentialless: 'auto',
|
|
156
176
|
customTrustedIframeDomains: [],
|
|
157
177
|
iframePolicy: 'strict',
|
|
@@ -161,10 +181,19 @@ export const DEFAULT_MCPUI_CONFIG: Required<MCPUIConfig> = {
|
|
|
161
181
|
}
|
|
162
182
|
|
|
163
183
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
184
|
+
* Typed with the RESOLVED view, because this context is read as well as
|
|
185
|
+
* written: it is exported, so a consumer may call
|
|
186
|
+
* `useContext(MCPUIConfigContext)` directly, and typing it as the input view
|
|
187
|
+
* would hand them `IframePolicy | undefined` — exactly the regression this
|
|
188
|
+
* whole split exists to undo.
|
|
189
|
+
*
|
|
190
|
+
* The cost lands on the rarer side: a host feeding this context directly
|
|
191
|
+
* passes a COMPLETE value, `{ ...DEFAULT_MCPUI_CONFIG, ...mine }`. Anyone who
|
|
192
|
+
* wants to pass only the keys they care about uses `MCPUIConfigProvider`,
|
|
193
|
+
* whose `config` prop takes `Partial<`{@link MCPUIConfig}`>` and does that
|
|
194
|
+
* merge. That preserves optional properties added through declaration
|
|
195
|
+
* merging; {@link MCPUIConfigInput} remains the unaugmented built-in input
|
|
196
|
+
* shape a host can name for ordinary authored configs.
|
|
168
197
|
*/
|
|
169
198
|
export const MCPUIConfigContext = createContext<MCPUIConfig>(DEFAULT_MCPUI_CONFIG)
|
|
170
199
|
|
|
@@ -173,7 +202,7 @@ export const MCPUIConfigContext = createContext<MCPUIConfig>(DEFAULT_MCPUI_CONFI
|
|
|
173
202
|
* came from the provider or from `DEFAULT_MCPUI_CONFIG`. Returns the defaults
|
|
174
203
|
* outright when no `<MCPUIConfigProvider>` is mounted above.
|
|
175
204
|
*/
|
|
176
|
-
export function useMCPUIConfig():
|
|
205
|
+
export function useMCPUIConfig(): MCPUIConfig {
|
|
177
206
|
return mergeProps(DEFAULT_MCPUI_CONFIG, useContext(MCPUIConfigContext))
|
|
178
207
|
}
|
|
179
208
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createContext, useContext, type Accessor, type JSX } from 'solid-js'
|
|
2
|
+
|
|
3
|
+
export interface MathRenderOptions {
|
|
4
|
+
displayMode: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** A synchronous, host-supplied renderer. Returned markup is always sanitized by MCP-UI. */
|
|
8
|
+
export type MathRenderer = (tex: string, options: MathRenderOptions) => string | null
|
|
9
|
+
|
|
10
|
+
const MCPUIMathContext = createContext<Accessor<MathRenderer | undefined>>(() => undefined)
|
|
11
|
+
|
|
12
|
+
export interface MCPUIMathProviderProps {
|
|
13
|
+
/** Omit or pass null to explicitly disable an inherited renderer. */
|
|
14
|
+
renderMath?: MathRenderer | null
|
|
15
|
+
children: JSX.Element
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function MCPUIMathProvider(props: MCPUIMathProviderProps): JSX.Element {
|
|
19
|
+
return (
|
|
20
|
+
<MCPUIMathContext.Provider value={() => props.renderMath ?? undefined}>
|
|
21
|
+
{props.children}
|
|
22
|
+
</MCPUIMathContext.Provider>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns a reactive accessor for the nearest math renderer. Call the accessor
|
|
28
|
+
* at use time; its value changes when a provider swaps or disables its renderer.
|
|
29
|
+
*/
|
|
30
|
+
export function useMCPUIMath(): Accessor<MathRenderer | undefined> {
|
|
31
|
+
return useContext(MCPUIMathContext)
|
|
32
|
+
}
|
|
@@ -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
|
/**
|
package/src/index.ts
CHANGED
|
@@ -167,6 +167,7 @@ export {
|
|
|
167
167
|
} from './context/MCPUIConfigContext'
|
|
168
168
|
export type {
|
|
169
169
|
MCPUIConfig,
|
|
170
|
+
MCPUIConfigInput,
|
|
170
171
|
MCPUIConfigProviderProps,
|
|
171
172
|
} from './context/MCPUIConfigContext'
|
|
172
173
|
|
|
@@ -389,6 +390,9 @@ export {
|
|
|
389
390
|
DEGRADED_PROJECTION_LABELS,
|
|
390
391
|
} from './utils/degraded-projections'
|
|
391
392
|
export type { DegradedProjectionLabels } from './utils/degraded-projections'
|
|
393
|
+
export { MCPUIMathProvider } from './context/MCPUIMathContext'
|
|
394
|
+
export type { MathRenderer, MathRenderOptions, MCPUIMathProviderProps } from './context/MCPUIMathContext'
|
|
395
|
+
|
|
392
396
|
export { chartToDataTable, CHART_DATA_TABLE_LABELS } from './components/chart-data-table'
|
|
393
397
|
export type { ChartDataTableLabels } from './components/chart-data-table'
|
|
394
398
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import katex from 'katex'
|
|
2
|
+
import type { MathRenderer } from '../context/MCPUIMathContext'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Optional KaTeX adapter. It emits MathML only: no HTML layout tree, inline
|
|
6
|
+
* styles, fonts, stylesheet, shared macro state, or trusted TeX commands.
|
|
7
|
+
*/
|
|
8
|
+
export const renderKatexMath: MathRenderer = (tex, { displayMode }) => {
|
|
9
|
+
try {
|
|
10
|
+
return katex.renderToString(tex, {
|
|
11
|
+
displayMode,
|
|
12
|
+
output: 'mathml',
|
|
13
|
+
// Invalid TeX must return null so the core can preserve the complete
|
|
14
|
+
// source token, including its delimiters, instead of showing KaTeX's
|
|
15
|
+
// red error MathML and silently consuming the dollars.
|
|
16
|
+
throwOnError: true,
|
|
17
|
+
trust: false,
|
|
18
|
+
maxExpand: 1000,
|
|
19
|
+
maxSize: 20,
|
|
20
|
+
macros: {},
|
|
21
|
+
})
|
|
22
|
+
} catch {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -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
|
-
|
|
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
|
},
|