@seed-ship/mcp-ui-solid 6.23.0 → 6.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +167 -0
- package/README.md +154 -2
- package/dist/components/ActionGroupRenderer.cjs +6 -6
- package/dist/components/ActionGroupRenderer.cjs.map +1 -1
- package/dist/components/ActionGroupRenderer.js +6 -6
- package/dist/components/ActionGroupRenderer.js.map +1 -1
- package/dist/components/AutocompleteDropdown.cjs +6 -6
- package/dist/components/AutocompleteDropdown.cjs.map +1 -1
- package/dist/components/AutocompleteDropdown.js +6 -6
- package/dist/components/AutocompleteDropdown.js.map +1 -1
- package/dist/components/AutocompleteFormField.cjs +6 -5
- package/dist/components/AutocompleteFormField.cjs.map +1 -1
- package/dist/components/AutocompleteFormField.d.ts.map +1 -1
- package/dist/components/AutocompleteFormField.js +6 -5
- package/dist/components/AutocompleteFormField.js.map +1 -1
- package/dist/components/ChartJSRenderer.cjs +406 -70
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +407 -71
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/ChatPrompt.cjs +5 -5
- package/dist/components/ChatPrompt.cjs.map +1 -1
- package/dist/components/ChatPrompt.js +5 -5
- package/dist/components/ChatPrompt.js.map +1 -1
- package/dist/components/ComponentToolbar.cjs +5 -5
- package/dist/components/ComponentToolbar.cjs.map +1 -1
- package/dist/components/ComponentToolbar.js +5 -5
- package/dist/components/ComponentToolbar.js.map +1 -1
- package/dist/components/DegradedFallback.cjs +35 -3
- 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 +35 -3
- package/dist/components/DegradedFallback.js.map +1 -1
- package/dist/components/ExpandableWrapper.cjs +10 -7
- package/dist/components/ExpandableWrapper.cjs.map +1 -1
- package/dist/components/ExpandableWrapper.d.ts.map +1 -1
- package/dist/components/ExpandableWrapper.js +10 -7
- package/dist/components/ExpandableWrapper.js.map +1 -1
- package/dist/components/FormFieldRenderer.cjs +13 -12
- package/dist/components/FormFieldRenderer.cjs.map +1 -1
- package/dist/components/FormFieldRenderer.d.ts.map +1 -1
- package/dist/components/FormFieldRenderer.js +13 -12
- package/dist/components/FormFieldRenderer.js.map +1 -1
- package/dist/components/FormRenderer.cjs +1 -1
- package/dist/components/FormRenderer.cjs.map +1 -1
- package/dist/components/FormRenderer.js +1 -1
- package/dist/components/FormRenderer.js.map +1 -1
- package/dist/components/GenerativeUIErrorBoundary.cjs +1 -1
- package/dist/components/GenerativeUIErrorBoundary.cjs.map +1 -1
- package/dist/components/GenerativeUIErrorBoundary.js +1 -1
- package/dist/components/GenerativeUIErrorBoundary.js.map +1 -1
- package/dist/components/GhostText.cjs +2 -2
- package/dist/components/GhostText.cjs.map +1 -1
- package/dist/components/GhostText.js +2 -2
- package/dist/components/GhostText.js.map +1 -1
- package/dist/components/GraphRenderer.cjs +100 -29
- package/dist/components/GraphRenderer.cjs.map +1 -1
- package/dist/components/GraphRenderer.d.ts +1 -1
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +100 -29
- package/dist/components/GraphRenderer.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/ModalRenderer.cjs +39 -31
- package/dist/components/ModalRenderer.cjs.map +1 -1
- package/dist/components/ModalRenderer.d.ts.map +1 -1
- package/dist/components/ModalRenderer.js +40 -32
- package/dist/components/ModalRenderer.js.map +1 -1
- package/dist/components/PortalDropdownMenu.cjs +6 -3
- package/dist/components/PortalDropdownMenu.cjs.map +1 -1
- package/dist/components/PortalDropdownMenu.d.ts.map +1 -1
- package/dist/components/PortalDropdownMenu.js +7 -4
- package/dist/components/PortalDropdownMenu.js.map +1 -1
- package/dist/components/ScratchpadPanel.cjs +38 -44
- package/dist/components/ScratchpadPanel.cjs.map +1 -1
- package/dist/components/ScratchpadPanel.d.ts.map +1 -1
- package/dist/components/ScratchpadPanel.js +38 -44
- package/dist/components/ScratchpadPanel.js.map +1 -1
- package/dist/components/UIResourceRenderer.cjs +255 -217
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +255 -217
- 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 +129 -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/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/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/math-markdown.cjs +1 -1
- package/dist/utils/math-markdown.cjs.map +1 -1
- package/dist/utils/math-markdown.js +1 -1
- package/dist/utils/math-markdown.js.map +1 -1
- 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/dist/utils/theme.cjs +91 -0
- package/dist/utils/theme.cjs.map +1 -0
- package/dist/utils/theme.d.ts +11 -0
- package/dist/utils/theme.d.ts.map +1 -0
- package/dist/utils/theme.js +91 -0
- package/dist/utils/theme.js.map +1 -0
- package/docs/SECURITY-DEPENDENCIES.md +81 -0
- package/docs/THEMING.md +205 -0
- package/package.json +7 -5
- package/src/components/ActionGroupRenderer.tsx +6 -6
- package/src/components/AutocompleteDropdown.tsx +17 -17
- package/src/components/AutocompleteFormField.tsx +10 -9
- package/src/components/ChartJSRenderer.tsx +609 -212
- package/src/components/ChatPrompt.tsx +16 -16
- package/src/components/ComponentToolbar.tsx +5 -5
- package/src/components/DegradedFallback.tsx +33 -11
- package/src/components/ExpandableWrapper.tsx +15 -11
- package/src/components/FormFieldRenderer.tsx +52 -51
- package/src/components/FormRenderer.tsx +12 -12
- package/src/components/GenerativeUIErrorBoundary.tsx +6 -6
- package/src/components/GhostText.tsx +7 -7
- package/src/components/GraphRenderer.tsx +83 -67
- package/src/components/MissingValue.tsx +61 -0
- package/src/components/ModalRenderer.tsx +11 -5
- package/src/components/PortalDropdownMenu.tsx +6 -2
- package/src/components/ScratchpadPanel.tsx +144 -141
- package/src/components/UIResourceRenderer.tsx +143 -110
- package/src/components/chart-data-table.ts +50 -6
- package/src/components/chart-missing-plugin.ts +336 -0
- package/src/context/MCPUIStringsContext.tsx +76 -0
- package/src/services/component-registry.ts +10 -2
- package/src/services/validation.ts +44 -9
- package/src/styles/autocomplete.css +53 -53
- package/src/types/index.ts +13 -1
- package/src/utils/degraded-projections.ts +44 -4
- package/src/utils/math-markdown.ts +1 -1
- package/src/utils/missing-value.ts +127 -0
- package/src/utils/theme.ts +101 -0
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
createMemo,
|
|
25
25
|
createUniqueId,
|
|
26
26
|
onCleanup,
|
|
27
|
+
onMount,
|
|
27
28
|
createSignal,
|
|
28
29
|
untrack,
|
|
29
30
|
Show,
|
|
@@ -32,10 +33,228 @@ import type { Accessor, JSX } from 'solid-js';
|
|
|
32
33
|
import type { UIComponent, ChartComponentParams } from '../types';
|
|
33
34
|
import { ExpandableWrapper, useExpanded } from './ExpandableWrapper';
|
|
34
35
|
import { DegradedFallback } from './DegradedFallback';
|
|
36
|
+
import { MissingValueLegend, MissingValueMark } from './MissingValue';
|
|
35
37
|
import { chartToDataTable } from './chart-data-table';
|
|
38
|
+
import {
|
|
39
|
+
applyUnitAxisTitle,
|
|
40
|
+
createMissingLegendGenerator,
|
|
41
|
+
createMissingValuesPlugin,
|
|
42
|
+
createTooltipLabel,
|
|
43
|
+
missingNoteKind,
|
|
44
|
+
withDatasetsNoGapSpan,
|
|
45
|
+
} from './chart-missing-plugin';
|
|
46
|
+
import { PART_TO_WHOLE_CHART_TYPES, summarizeChartMissing } from '../utils/missing-value';
|
|
36
47
|
import { useTelemetry } from '../context/MCPUITelemetryContext';
|
|
37
48
|
import { useMCPUIConfig } from '../context/MCPUIConfigContext';
|
|
38
49
|
import { formatMCPUIString, useMCPUIStrings } from '../context/MCPUIStringsContext';
|
|
50
|
+
import { observeTheme, readThemeTokens } from '../utils/theme';
|
|
51
|
+
|
|
52
|
+
const CHART_THEME_TOKENS = [
|
|
53
|
+
...Array.from({ length: 8 }, (_, i) => `--mcp-ui-chart-${i + 1}`),
|
|
54
|
+
'--mcp-ui-chart-grid',
|
|
55
|
+
'--mcp-ui-chart-axis',
|
|
56
|
+
'--mcp-ui-chart-label',
|
|
57
|
+
'--mcp-ui-surface', '--mcp-ui-fg', '--mcp-ui-border',
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
/** A per-instance adapter: never changes Chart.defaults or the caller's data. */
|
|
61
|
+
function createChartTheme(
|
|
62
|
+
params: ChartComponentParams,
|
|
63
|
+
baseOptions: any,
|
|
64
|
+
root: HTMLElement,
|
|
65
|
+
chartConfig: any
|
|
66
|
+
) {
|
|
67
|
+
let tokens = readThemeTokens(root, CHART_THEME_TOKENS);
|
|
68
|
+
const originalDatasets = params.data.datasets.map((dataset) => ({ ...dataset }));
|
|
69
|
+
let engineColors: Array<{ backgroundColor: any; borderColor: any }> | undefined;
|
|
70
|
+
const sharedPaintOptions = new WeakMap<object, any>();
|
|
71
|
+
const paths: Array<{ path: string[]; token: string }> = [
|
|
72
|
+
{ path: ['plugins', 'legend', 'labels', 'color'], token: '--mcp-ui-chart-label' },
|
|
73
|
+
{ path: ['plugins', 'tooltip', 'backgroundColor'], token: '--mcp-ui-surface' },
|
|
74
|
+
{ path: ['plugins', 'tooltip', 'titleColor'], token: '--mcp-ui-fg' },
|
|
75
|
+
{ path: ['plugins', 'tooltip', 'bodyColor'], token: '--mcp-ui-fg' },
|
|
76
|
+
{ path: ['plugins', 'tooltip', 'footerColor'], token: '--mcp-ui-fg' },
|
|
77
|
+
{ path: ['plugins', 'tooltip', 'borderColor'], token: '--mcp-ui-border' },
|
|
78
|
+
];
|
|
79
|
+
const configuredScales = Object.keys(baseOptions.scales ?? {});
|
|
80
|
+
const scales = configuredScales.length ? configuredScales :
|
|
81
|
+
params.type === 'radar' || params.type === 'polarArea' ? ['r'] :
|
|
82
|
+
params.type === 'pie' || params.type === 'doughnut' ? [] : ['x', 'y'];
|
|
83
|
+
const knownScales = new Set<string>();
|
|
84
|
+
const addScalePaths = (names: string[]) => { for (const scale of names) {
|
|
85
|
+
if (knownScales.has(scale)) continue;
|
|
86
|
+
knownScales.add(scale);
|
|
87
|
+
for (const [part, token] of [
|
|
88
|
+
['grid', '--mcp-ui-chart-grid'],
|
|
89
|
+
['border', '--mcp-ui-chart-axis'],
|
|
90
|
+
['ticks', '--mcp-ui-chart-label'],
|
|
91
|
+
['title', '--mcp-ui-chart-label'],
|
|
92
|
+
...(scale === 'r'
|
|
93
|
+
? [
|
|
94
|
+
['angleLines', '--mcp-ui-chart-grid'],
|
|
95
|
+
['pointLabels', '--mcp-ui-chart-label'],
|
|
96
|
+
]
|
|
97
|
+
: []),
|
|
98
|
+
])
|
|
99
|
+
paths.push({ path: ['scales', scale, part, 'color'], token });
|
|
100
|
+
} };
|
|
101
|
+
addScalePaths(scales);
|
|
102
|
+
const read = (value: any, path: string[]) => path.reduce((part, key) => part?.[key], value);
|
|
103
|
+
const owned = new Set<string>();
|
|
104
|
+
const applyOptions = (options: any) => {
|
|
105
|
+
for (const { path, token } of paths) {
|
|
106
|
+
// Explicit caller options, including plugin/scale opt-outs, always win.
|
|
107
|
+
if (read(baseOptions, path) !== undefined) continue;
|
|
108
|
+
if (token === '--mcp-ui-chart-label' && (params.options as any)?.color !== undefined)
|
|
109
|
+
continue;
|
|
110
|
+
let blocked = false;
|
|
111
|
+
for (let i = 1; i < path.length; i++) {
|
|
112
|
+
const part = read(baseOptions, path.slice(0, i));
|
|
113
|
+
if (part === false || part === null) blocked = true;
|
|
114
|
+
}
|
|
115
|
+
if (blocked) continue;
|
|
116
|
+
const key = path.join('.');
|
|
117
|
+
const color = tokens[token];
|
|
118
|
+
if (!color && !owned.has(key)) continue;
|
|
119
|
+
let target = options;
|
|
120
|
+
for (const part of path.slice(0, -1)) {
|
|
121
|
+
// Copy along the changed path only, retaining untouched caller branches.
|
|
122
|
+
target[part] = { ...target[part] };
|
|
123
|
+
target = target[part];
|
|
124
|
+
}
|
|
125
|
+
if (color) {
|
|
126
|
+
target[path.at(-1)!] = color;
|
|
127
|
+
owned.add(key);
|
|
128
|
+
} else {
|
|
129
|
+
delete target[path.at(-1)!];
|
|
130
|
+
owned.delete(key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const plugin = {
|
|
135
|
+
id: 'mcp-ui-theme',
|
|
136
|
+
beforeInit(chart: any) {
|
|
137
|
+
// Config has normalized implicit/default axes now. Never invent x/y
|
|
138
|
+
// beside a caller's named axes just to give them a color.
|
|
139
|
+
addScalePaths(Object.keys(chart.config.options.scales ?? {}));
|
|
140
|
+
applyOptions(chart.config.options);
|
|
141
|
+
},
|
|
142
|
+
// Global Colors runs first. Capture its actual output rather than copying
|
|
143
|
+
// a version-specific 7-color palette (or changing its opt-out semantics).
|
|
144
|
+
beforeLayout(chart: any) {
|
|
145
|
+
const datasets = chart.data.datasets;
|
|
146
|
+
engineColors ??= datasets.map((dataset: any) => ({
|
|
147
|
+
backgroundColor: Array.isArray(dataset.backgroundColor)
|
|
148
|
+
? [...dataset.backgroundColor]
|
|
149
|
+
: dataset.backgroundColor,
|
|
150
|
+
borderColor: Array.isArray(dataset.borderColor)
|
|
151
|
+
? [...dataset.borderColor]
|
|
152
|
+
: dataset.borderColor,
|
|
153
|
+
}));
|
|
154
|
+
const callerOptions = params.options as any;
|
|
155
|
+
let pointOffset = 0;
|
|
156
|
+
datasets.forEach((dataset: any, index: number) => {
|
|
157
|
+
const original = originalDatasets[index];
|
|
158
|
+
const baseline = engineColors![index];
|
|
159
|
+
if (!original || !baseline) return;
|
|
160
|
+
const datasetType = (original as any).type ?? params.type;
|
|
161
|
+
const perPoint = ['pie', 'doughnut', 'polarArea'].includes(datasetType);
|
|
162
|
+
const lineWithPoints = datasetType === 'line' || datasetType === 'radar';
|
|
163
|
+
const elementKinds = datasetType === 'bar' ? ['bar'] : perPoint ? ['arc'] :
|
|
164
|
+
lineWithPoints ? ['line'] : ['point'];
|
|
165
|
+
const hasSeriesToken = perPoint
|
|
166
|
+
? dataset.data.some((_: unknown, i: number) => tokens[`--mcp-ui-chart-${((pointOffset + i) % 8) + 1}`])
|
|
167
|
+
: Boolean(tokens[`--mcp-ui-chart-${(index % 8) + 1}`]);
|
|
168
|
+
for (const field of ['backgroundColor', 'borderColor'] as const) {
|
|
169
|
+
if (original[field] !== undefined) continue;
|
|
170
|
+
const callerDatasetColor = callerOptions?.datasets?.[datasetType]?.[field];
|
|
171
|
+
if (callerDatasetColor !== undefined) {
|
|
172
|
+
// Chart.js's global Colors plugin does not check options.datasets
|
|
173
|
+
// and can mask this explicit paint with its generated dataset field.
|
|
174
|
+
// Retain the caller paint when adapting this palette slot, but keep
|
|
175
|
+
// the engine baseline unchanged when no series token is requested.
|
|
176
|
+
const color = hasSeriesToken ? callerDatasetColor : baseline[field];
|
|
177
|
+
if (color !== undefined) dataset[field] = color;
|
|
178
|
+
else delete dataset[field];
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
// A caller's border must not suppress a host fill (or vice versa),
|
|
182
|
+
// nor may options for an unrelated chart/element type mask this one.
|
|
183
|
+
if (callerOptions?.[field] !== undefined ||
|
|
184
|
+
elementKinds.some((kind) => callerOptions?.elements?.[kind]?.[field] !== undefined)) continue;
|
|
185
|
+
if (lineWithPoints && hasSeriesToken) {
|
|
186
|
+
// Dataset paints also feed point defaults. Preserve an explicit
|
|
187
|
+
// point paint under its native prefix before theming the line;
|
|
188
|
+
// otherwise the generic themed field would outrank elements.point.
|
|
189
|
+
const pointField = field === 'borderColor' ? 'pointBorderColor' : 'pointBackgroundColor';
|
|
190
|
+
const scopedPointPaint = callerOptions?.datasets?.[datasetType]?.elements?.point?.[field];
|
|
191
|
+
const pointPaint = scopedPointPaint !== undefined ? scopedPointPaint :
|
|
192
|
+
callerOptions?.elements?.point?.[field];
|
|
193
|
+
if ((original as any)[pointField] === undefined &&
|
|
194
|
+
callerOptions?.datasets?.[datasetType]?.[pointField] === undefined &&
|
|
195
|
+
callerOptions?.[pointField] === undefined && pointPaint !== undefined) {
|
|
196
|
+
dataset[pointField] = pointPaint;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Pie-like charts have no automatic per-point border palette.
|
|
200
|
+
if (perPoint && field === 'borderColor') continue;
|
|
201
|
+
if (perPoint) {
|
|
202
|
+
const previous = baseline[field];
|
|
203
|
+
const colors = dataset.data.map(
|
|
204
|
+
(_: unknown, i: number) =>
|
|
205
|
+
tokens[`--mcp-ui-chart-${((pointOffset + i) % 8) + 1}`] ??
|
|
206
|
+
(Array.isArray(previous) ? previous[i % previous.length] : previous)
|
|
207
|
+
);
|
|
208
|
+
dataset[field] = colors.some(
|
|
209
|
+
(_: unknown, i: number) => tokens[`--mcp-ui-chart-${((pointOffset + i) % 8) + 1}`]
|
|
210
|
+
)
|
|
211
|
+
? colors
|
|
212
|
+
: previous;
|
|
213
|
+
} else {
|
|
214
|
+
const color = tokens[`--mcp-ui-chart-${(index % 8) + 1}`];
|
|
215
|
+
if (color || baseline[field] !== undefined) dataset[field] = color ?? baseline[field];
|
|
216
|
+
else delete dataset[field];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (perPoint) pointOffset += dataset.data.length;
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
afterDatasetUpdate(_chart: any, args: any) {
|
|
223
|
+
const meta = args.meta;
|
|
224
|
+
if (!meta?.data?.length || typeof meta.controller?.getStyle !== 'function') return;
|
|
225
|
+
const index = meta.data.findIndex((element: any) => element.options?.$shared && !element.active);
|
|
226
|
+
if (index >= 0 && !Object.isFrozen(meta.data[index].options)) {
|
|
227
|
+
sharedPaintOptions.set(meta.controller, meta.data[index].options);
|
|
228
|
+
}
|
|
229
|
+
if (args.mode !== 'none') return;
|
|
230
|
+
// Chart.js deliberately skips updateSharedOptions in direct ('none')
|
|
231
|
+
// mode. Refresh only its shared paint values from the public resolver;
|
|
232
|
+
// retain the object captured before hover, even when every element is
|
|
233
|
+
// active now, so later deactivation cannot revive an old theme paint.
|
|
234
|
+
const shared = sharedPaintOptions.get(meta.controller);
|
|
235
|
+
if (!shared) return;
|
|
236
|
+
const resolved = meta.controller.getStyle(index >= 0 ? index : 0, false);
|
|
237
|
+
if (!resolved.$shared) return;
|
|
238
|
+
shared.backgroundColor = resolved.backgroundColor;
|
|
239
|
+
shared.borderColor = resolved.borderColor;
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
// Bookkeeping must start before the first hover, even without host tokens.
|
|
243
|
+
// Without that capture a theme first enabled while all points are active
|
|
244
|
+
// cannot reach the native shared options using public APIs alone.
|
|
245
|
+
chartConfig.plugins = [...(chartConfig.plugins ?? []), plugin];
|
|
246
|
+
return {
|
|
247
|
+
applyOptions,
|
|
248
|
+
refresh(chart: any) {
|
|
249
|
+
const next = readThemeTokens(root, CHART_THEME_TOKENS);
|
|
250
|
+
if (CHART_THEME_TOKENS.every((name) => next[name] === tokens[name])) return;
|
|
251
|
+
tokens = next;
|
|
252
|
+
addScalePaths(Object.keys(chart.scales ?? chart.config.options.scales ?? {}));
|
|
253
|
+
applyOptions(chart.config.options);
|
|
254
|
+
chart.update('none');
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
39
258
|
|
|
40
259
|
// Lazy load Chart.js to avoid bundling if not used
|
|
41
260
|
let ChartJS: any = null;
|
|
@@ -305,9 +524,20 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
305
524
|
const [zoomPlugin, setZoomPlugin] = createSignal<unknown | null>(null);
|
|
306
525
|
const [isZoomed, setIsZoomed] = createSignal(false);
|
|
307
526
|
let canvasRef: HTMLCanvasElement | undefined;
|
|
527
|
+
let themeRoot: HTMLDivElement | undefined;
|
|
308
528
|
let chartInstance: any;
|
|
529
|
+
let chartTheme: ReturnType<typeof createChartTheme> | undefined;
|
|
309
530
|
let renderVersion = 0;
|
|
310
531
|
|
|
532
|
+
onMount(() => {
|
|
533
|
+
if (themeRoot) {
|
|
534
|
+
const stop = observeTheme(themeRoot, () => {
|
|
535
|
+
if (chartInstance) chartTheme?.refresh(chartInstance);
|
|
536
|
+
});
|
|
537
|
+
onCleanup(stop);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
311
541
|
const params = () => props.component.params as ChartComponentParams;
|
|
312
542
|
const telemetry = useTelemetry();
|
|
313
543
|
const config = useMCPUIConfig();
|
|
@@ -320,9 +550,51 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
320
550
|
point: strings.chartTablePoint,
|
|
321
551
|
label: strings.degradedColLabel,
|
|
322
552
|
seriesName: strings.degradedSeries,
|
|
553
|
+
withUnit: strings.chartLabelWithUnit,
|
|
323
554
|
})
|
|
324
555
|
);
|
|
556
|
+
// Absence is never zero: which entries are missing, and which series have
|
|
557
|
+
// nothing at all. Drives the notes, the plugin, the legend and the guard.
|
|
558
|
+
const missingSummary = createMemo(() => summarizeChartMissing(params()));
|
|
559
|
+
const unit = () => {
|
|
560
|
+
const value = params().unit;
|
|
561
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
562
|
+
};
|
|
563
|
+
const anyMissingCell = () => tableData().missing.some((row) => row.some(Boolean));
|
|
564
|
+
// Never for a part-to-whole chart: it refuses missing values outright (the
|
|
565
|
+
// render effect turns it into the data table), so no convention applies —
|
|
566
|
+
// and the async guard must not leave a wrong note in the SSR markup.
|
|
567
|
+
const notesShown = () =>
|
|
568
|
+
missingSummary().hasMissing &&
|
|
569
|
+
!PART_TO_WHOLE_CHART_TYPES.has(params().type) &&
|
|
570
|
+
activeView() === 'chart' &&
|
|
571
|
+
!error();
|
|
572
|
+
const noteKind = () => missingNoteKind(params().type, params().options);
|
|
573
|
+
// The convention line explains a mark the reader can SEE: every missing bar
|
|
574
|
+
// gets a dashed stub, but a line or a scatter only shows an absence inside a
|
|
575
|
+
// series that still has values — a series with no value at all draws
|
|
576
|
+
// nothing, and the "no data" line alone covers it.
|
|
577
|
+
const conventionNoteShown = () =>
|
|
578
|
+
noteKind() === 'bars'
|
|
579
|
+
? missingSummary().hasMissing
|
|
580
|
+
: missingSummary().partiallyMissing.some(Boolean);
|
|
581
|
+
// The series the chart has no data for, as a locale-aware list.
|
|
582
|
+
const noDataSeries = () => {
|
|
583
|
+
const names = (params().data?.datasets ?? [])
|
|
584
|
+
.map((dataset, index) =>
|
|
585
|
+
missingSummary().fullyMissing[index]
|
|
586
|
+
? dataset?.label || formatMCPUIString(strings.degradedSeries, { n: index + 1 })
|
|
587
|
+
: null
|
|
588
|
+
)
|
|
589
|
+
.filter((name): name is string => name !== null);
|
|
590
|
+
try {
|
|
591
|
+
return new Intl.ListFormat(strings.locale, { type: 'conjunction' }).format(names);
|
|
592
|
+
} catch {
|
|
593
|
+
return names.join(', ');
|
|
594
|
+
}
|
|
595
|
+
};
|
|
325
596
|
const descriptionId = createUniqueId();
|
|
597
|
+
const notesId = createUniqueId();
|
|
326
598
|
const title = () => params().title || strings.chartView;
|
|
327
599
|
|
|
328
600
|
// v6.1.0 — export visibility :
|
|
@@ -406,8 +678,7 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
406
678
|
* here (host policy × surface × chart type, and the peer actually loaded),
|
|
407
679
|
* and the chart is what is currently being shown.
|
|
408
680
|
*/
|
|
409
|
-
const zoomControlsShown = () =>
|
|
410
|
-
Boolean(zoomOptions()) && activeView() === 'chart' && !error();
|
|
681
|
+
const zoomControlsShown = () => Boolean(zoomOptions()) && activeView() === 'chart' && !error();
|
|
411
682
|
|
|
412
683
|
/**
|
|
413
684
|
* Zoom the chart by a fixed ratio, about its centre.
|
|
@@ -456,6 +727,8 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
456
727
|
// plugin options at construction and the plugin only hooks charts created
|
|
457
728
|
// after it was registered).
|
|
458
729
|
const chartParams = params();
|
|
730
|
+
const chartMissing = missingSummary();
|
|
731
|
+
const chartUnit = unit();
|
|
459
732
|
const zoom = zoomOptions();
|
|
460
733
|
const version = ++renderVersion;
|
|
461
734
|
|
|
@@ -474,6 +747,14 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
474
747
|
chartInstance = null;
|
|
475
748
|
}
|
|
476
749
|
|
|
750
|
+
// A share of a whole cannot be missing: Chart.js parses `null` as 0 and
|
|
751
|
+
// would draw it as a zero slice. Validation rejects this earlier; this
|
|
752
|
+
// is the guard for a host that bypassed it. Thrown here so the catch
|
|
753
|
+
// below shows the data table (with the missing cells marked).
|
|
754
|
+
if (chartMissing.hasMissing && PART_TO_WHOLE_CHART_TYPES.has(chartParams.type)) {
|
|
755
|
+
throw new Error(strings.chartMissingUnsupported);
|
|
756
|
+
}
|
|
757
|
+
|
|
477
758
|
// Build options, merging time-axis config if present (v3.1.0)
|
|
478
759
|
const hostZoomOptions = chartParams.options?.plugins?.zoom;
|
|
479
760
|
const baseOptions: any = {
|
|
@@ -517,12 +798,86 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
517
798
|
};
|
|
518
799
|
}
|
|
519
800
|
|
|
520
|
-
//
|
|
521
|
-
|
|
801
|
+
// Unit (v6.24.0): value-axis title and tooltip suffix. Both are no-ops
|
|
802
|
+
// on a payload without a unit.
|
|
803
|
+
applyUnitAxisTitle(baseOptions, chartParams.type, chartUnit);
|
|
804
|
+
|
|
805
|
+
const chartConfig: any = {
|
|
522
806
|
type: chartParams.type,
|
|
523
|
-
|
|
807
|
+
// The Colors plugin writes its generated palette onto datasets. Never
|
|
808
|
+
// let that turn a shared payload into caller-supplied colors for the
|
|
809
|
+
// next renderer, nor mutate data owned by a host application.
|
|
810
|
+
data: { ...chartParams.data, datasets: chartParams.data.datasets.map((dataset) => ({ ...dataset })) },
|
|
524
811
|
options: baseOptions,
|
|
525
|
-
}
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
if (chartUnit !== undefined || chartMissing.hasMissing) {
|
|
815
|
+
const tooltip = baseOptions.plugins.tooltip;
|
|
816
|
+
// `tooltip: false` (or `null`) is Chart.js's per-chart plugin opt-out:
|
|
817
|
+
// the payload turned tooltips off, and adding a callback object here
|
|
818
|
+
// would turn them back on. A host-supplied label callback is the
|
|
819
|
+
// host's word too. Both are left exactly as written.
|
|
820
|
+
if (tooltip !== false && tooltip !== null && !tooltip?.callbacks?.label) {
|
|
821
|
+
baseOptions.plugins.tooltip = {
|
|
822
|
+
...tooltip,
|
|
823
|
+
callbacks: {
|
|
824
|
+
...tooltip?.callbacks,
|
|
825
|
+
label: createTooltipLabel(() => strings, chartUnit, chartParams.type),
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (chartMissing.hasMissing) {
|
|
832
|
+
// A line must break at a missing value, not join its neighbours.
|
|
833
|
+
chartConfig.data = withDatasetsNoGapSpan(chartParams.data);
|
|
834
|
+
|
|
835
|
+
// Legend of a fully-missing series: "{series} (no data)". Dataset
|
|
836
|
+
// legends only — pie-like legends list categories, not series.
|
|
837
|
+
const defaultGenerate =
|
|
838
|
+
baseOptions.plugins.legend?.labels?.generateLabels ??
|
|
839
|
+
Chart?.defaults?.plugins?.legend?.labels?.generateLabels;
|
|
840
|
+
if (typeof defaultGenerate === 'function') {
|
|
841
|
+
baseOptions.plugins.legend = {
|
|
842
|
+
...baseOptions.plugins.legend,
|
|
843
|
+
labels: {
|
|
844
|
+
...baseOptions.plugins.legend?.labels,
|
|
845
|
+
generateLabels: createMissingLegendGenerator(
|
|
846
|
+
defaultGenerate,
|
|
847
|
+
chartMissing.fullyMissing,
|
|
848
|
+
() => strings
|
|
849
|
+
),
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Per-chart plugin (never `Chart.register`): the dashed stub of a
|
|
855
|
+
// missing bar, and "no data" on a chart with nothing to plot.
|
|
856
|
+
chartConfig.plugins = [
|
|
857
|
+
createMissingValuesPlugin({
|
|
858
|
+
allMissing: chartMissing.allMissing,
|
|
859
|
+
getNoDataText: () => strings.chartNoData,
|
|
860
|
+
getFontFamily: () => Chart?.defaults?.font?.family,
|
|
861
|
+
getTextColor: () => (chartParams.options as any)?.color ??
|
|
862
|
+
readThemeTokens(themeRoot, ['--mcp-ui-chart-label'])['--mcp-ui-chart-label'],
|
|
863
|
+
}),
|
|
864
|
+
];
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (themeRoot) {
|
|
868
|
+
// Keep the unthemed source separate: live updates must not mistake our
|
|
869
|
+
// own previously applied colors for explicit caller customization.
|
|
870
|
+
const originalOptions = {
|
|
871
|
+
...baseOptions,
|
|
872
|
+
plugins: { ...baseOptions.plugins },
|
|
873
|
+
scales: baseOptions.scales ? { ...baseOptions.scales } : undefined,
|
|
874
|
+
};
|
|
875
|
+
chartTheme = createChartTheme(chartParams, originalOptions, themeRoot, chartConfig);
|
|
876
|
+
chartTheme.applyOptions(baseOptions);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Create new chart
|
|
880
|
+
chartInstance = new Chart(canvasRef, chartConfig);
|
|
526
881
|
|
|
527
882
|
// A freshly built chart always sits on its initial viewport, so the
|
|
528
883
|
// reset control goes away until the user zooms again.
|
|
@@ -573,110 +928,139 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
573
928
|
<ContextAwareChartLayout
|
|
574
929
|
render={(isExpanded) => (
|
|
575
930
|
<div
|
|
576
|
-
|
|
931
|
+
ref={themeRoot}
|
|
932
|
+
class={`mcp-ui-root relative w-full bg-[color:var(--mcp-ui-surface,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-surface,var(--color-gray-800,#1f2937))] rounded-lg shadow-[var(--mcp-ui-shadow,var(--shadow-sm))] border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-700,#374151))] overflow-hidden p-4 group ${
|
|
577
933
|
isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''
|
|
578
934
|
} ${params().className || ''}`}
|
|
579
935
|
>
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
class="
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
936
|
+
<div class="flex items-center justify-between gap-3 mb-3 flex-shrink-0">
|
|
937
|
+
<div class="min-w-0">
|
|
938
|
+
<Show when={params().title}>
|
|
939
|
+
<h3 class="text-sm font-semibold text-[color:var(--mcp-ui-fg,var(--color-gray-900,#111827))] dark:text-[color:var(--mcp-ui-fg,var(--color-white,#fff))]">
|
|
940
|
+
{params().title}
|
|
941
|
+
</h3>
|
|
942
|
+
</Show>
|
|
943
|
+
</div>
|
|
944
|
+
<div class="flex items-center gap-2">
|
|
945
|
+
<div
|
|
946
|
+
class="inline-flex rounded-md border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] p-0.5 dark:border-[color:var(--mcp-ui-border,var(--color-gray-600,#4b5563))]"
|
|
947
|
+
role="group"
|
|
948
|
+
aria-label={strings.chartViewSelector}
|
|
949
|
+
>
|
|
950
|
+
<button
|
|
951
|
+
type="button"
|
|
952
|
+
aria-pressed={activeView() === 'chart'}
|
|
953
|
+
class={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
|
954
|
+
activeView() === 'chart'
|
|
955
|
+
? 'bg-[color:var(--mcp-ui-accent,var(--color-gray-900,#111827))] text-[color:var(--mcp-ui-accent-fg,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-accent,var(--color-gray-100,#f3f4f6))] dark:text-[color:var(--mcp-ui-accent-fg,var(--color-gray-900,#111827))]'
|
|
956
|
+
: 'text-[color:var(--mcp-ui-fg-muted,var(--color-gray-600,#4b5563))] hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-100,#f3f4f6))] dark:text-[color:var(--mcp-ui-fg,var(--color-gray-300,#d1d5db))] dark:hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-700,#374151))]'
|
|
957
|
+
}`}
|
|
958
|
+
onClick={() => setActiveView('chart')}
|
|
959
|
+
>
|
|
960
|
+
{strings.chartView}
|
|
961
|
+
</button>
|
|
962
|
+
<button
|
|
963
|
+
type="button"
|
|
964
|
+
aria-pressed={activeView() === 'data'}
|
|
965
|
+
class={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
|
966
|
+
activeView() === 'data'
|
|
967
|
+
? 'bg-[color:var(--mcp-ui-accent,var(--color-gray-900,#111827))] text-[color:var(--mcp-ui-accent-fg,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-accent,var(--color-gray-100,#f3f4f6))] dark:text-[color:var(--mcp-ui-accent-fg,var(--color-gray-900,#111827))]'
|
|
968
|
+
: 'text-[color:var(--mcp-ui-fg-muted,var(--color-gray-600,#4b5563))] hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-100,#f3f4f6))] dark:text-[color:var(--mcp-ui-fg,var(--color-gray-300,#d1d5db))] dark:hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-700,#374151))]'
|
|
969
|
+
}`}
|
|
970
|
+
onClick={() => setActiveView('data')}
|
|
971
|
+
>
|
|
972
|
+
{strings.chartDataView}
|
|
973
|
+
</button>
|
|
974
|
+
</div>
|
|
975
|
+
{/* Zoom controls — permanently visible (not hover-revealed like
|
|
618
976
|
the export button) for as long as this chart is zoomable at
|
|
619
977
|
all. Zoom in/out come FIRST and unconditionally: they are the
|
|
620
978
|
only keyboard route into a feature whose gestures are all
|
|
621
979
|
pointer gestures, on a canvas that takes no focus. Reset joins
|
|
622
980
|
them only once the chart is actually off its initial viewport,
|
|
623
981
|
so the toolbar stays quiet on a chart nobody moved. */}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
982
|
+
<Show when={zoomControlsShown()}>
|
|
983
|
+
<div class="inline-flex items-center gap-1">
|
|
984
|
+
<button
|
|
985
|
+
type="button"
|
|
986
|
+
onClick={() => handleZoomBy(1 / CHART_ZOOM_BUTTON_FACTOR)}
|
|
987
|
+
class="opacity-60 hover:opacity-100 px-2 py-1 bg-[color:var(--mcp-ui-surface,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-surface,var(--color-gray-800,#1f2937))] border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-600,#4b5563))] rounded-full hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-100,#f3f4f6))] dark:hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-700,#374151))] transition-all shadow-[var(--mcp-ui-shadow,var(--shadow-sm))]"
|
|
988
|
+
title={strings.zoomOut}
|
|
989
|
+
aria-label={strings.zoomOut}
|
|
990
|
+
>
|
|
991
|
+
<svg
|
|
992
|
+
class="w-3 h-3 text-[color:var(--mcp-ui-fg-muted,var(--color-gray-500,#6b7280))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]"
|
|
993
|
+
fill="none"
|
|
994
|
+
viewBox="0 0 24 24"
|
|
995
|
+
stroke="currentColor"
|
|
996
|
+
>
|
|
997
|
+
<path
|
|
998
|
+
stroke-linecap="round"
|
|
999
|
+
stroke-linejoin="round"
|
|
1000
|
+
stroke-width="2"
|
|
1001
|
+
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM13.5 10.5h-6"
|
|
1002
|
+
/>
|
|
1003
|
+
</svg>
|
|
1004
|
+
</button>
|
|
1005
|
+
|
|
1006
|
+
<button
|
|
1007
|
+
type="button"
|
|
1008
|
+
onClick={() => handleZoomBy(CHART_ZOOM_BUTTON_FACTOR)}
|
|
1009
|
+
class="opacity-60 hover:opacity-100 px-2 py-1 bg-[color:var(--mcp-ui-surface,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-surface,var(--color-gray-800,#1f2937))] border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-600,#4b5563))] rounded-full hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-100,#f3f4f6))] dark:hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-700,#374151))] transition-all shadow-[var(--mcp-ui-shadow,var(--shadow-sm))]"
|
|
1010
|
+
title={strings.zoomIn}
|
|
1011
|
+
aria-label={strings.zoomIn}
|
|
1012
|
+
>
|
|
1013
|
+
<svg
|
|
1014
|
+
class="w-3 h-3 text-[color:var(--mcp-ui-fg-muted,var(--color-gray-500,#6b7280))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]"
|
|
1015
|
+
fill="none"
|
|
1016
|
+
viewBox="0 0 24 24"
|
|
1017
|
+
stroke="currentColor"
|
|
1018
|
+
>
|
|
1019
|
+
<path
|
|
1020
|
+
stroke-linecap="round"
|
|
1021
|
+
stroke-linejoin="round"
|
|
1022
|
+
stroke-width="2"
|
|
1023
|
+
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6"
|
|
1024
|
+
/>
|
|
1025
|
+
</svg>
|
|
1026
|
+
</button>
|
|
1027
|
+
|
|
1028
|
+
<Show when={isZoomed()}>
|
|
1029
|
+
<button
|
|
1030
|
+
type="button"
|
|
1031
|
+
onClick={handleResetZoom}
|
|
1032
|
+
class="opacity-60 hover:opacity-100 px-2 py-1 bg-[color:var(--mcp-ui-surface,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-surface,var(--color-gray-800,#1f2937))] border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-600,#4b5563))] rounded-full hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-100,#f3f4f6))] dark:hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-700,#374151))] transition-all shadow-[var(--mcp-ui-shadow,var(--shadow-sm))]"
|
|
1033
|
+
title={strings.zoomReset}
|
|
1034
|
+
aria-label={strings.zoomReset}
|
|
1035
|
+
>
|
|
1036
|
+
<svg
|
|
1037
|
+
class="w-3 h-3 text-[color:var(--mcp-ui-fg-muted,var(--color-gray-500,#6b7280))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]"
|
|
1038
|
+
fill="none"
|
|
1039
|
+
viewBox="0 0 24 24"
|
|
1040
|
+
stroke="currentColor"
|
|
1041
|
+
>
|
|
1042
|
+
<path
|
|
1043
|
+
stroke-linecap="round"
|
|
1044
|
+
stroke-linejoin="round"
|
|
1045
|
+
stroke-width="2"
|
|
1046
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
1047
|
+
/>
|
|
1048
|
+
</svg>
|
|
1049
|
+
</button>
|
|
1050
|
+
</Show>
|
|
1051
|
+
</div>
|
|
1052
|
+
</Show>
|
|
1053
|
+
<Show when={exportEnabled()}>
|
|
671
1054
|
<button
|
|
672
1055
|
type="button"
|
|
673
|
-
onClick={
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1056
|
+
onClick={handleExportPNG}
|
|
1057
|
+
disabled={activeView() !== 'chart' || isLoading() || Boolean(error())}
|
|
1058
|
+
class="opacity-0 group-hover:opacity-60 hover:!opacity-100 disabled:!opacity-30 disabled:cursor-not-allowed px-2 py-1 bg-[color:var(--mcp-ui-surface,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-surface,var(--color-gray-800,#1f2937))] border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-600,#4b5563))] rounded-full hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-100,#f3f4f6))] dark:hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-700,#374151))] transition-all shadow-[var(--mcp-ui-shadow,var(--shadow-sm))]"
|
|
1059
|
+
title={strings.chartDownloadPng}
|
|
1060
|
+
aria-label={strings.chartDownloadPngAria}
|
|
677
1061
|
>
|
|
678
1062
|
<svg
|
|
679
|
-
class="w-3 h-3 text-gray-500 dark:text-gray-400"
|
|
1063
|
+
class="w-3 h-3 text-[color:var(--mcp-ui-fg-muted,var(--color-gray-500,#6b7280))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]"
|
|
680
1064
|
fill="none"
|
|
681
1065
|
viewBox="0 0 24 24"
|
|
682
1066
|
stroke="currentColor"
|
|
@@ -685,135 +1069,148 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
685
1069
|
stroke-linecap="round"
|
|
686
1070
|
stroke-linejoin="round"
|
|
687
1071
|
stroke-width="2"
|
|
688
|
-
d="
|
|
1072
|
+
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
689
1073
|
/>
|
|
690
1074
|
</svg>
|
|
691
1075
|
</button>
|
|
692
1076
|
</Show>
|
|
693
1077
|
</div>
|
|
694
|
-
</
|
|
695
|
-
<Show when={exportEnabled()}>
|
|
696
|
-
<button
|
|
697
|
-
type="button"
|
|
698
|
-
onClick={handleExportPNG}
|
|
699
|
-
disabled={activeView() !== 'chart' || isLoading() || Boolean(error())}
|
|
700
|
-
class="opacity-0 group-hover:opacity-60 hover:!opacity-100 disabled:!opacity-30 disabled:cursor-not-allowed px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-all shadow-sm"
|
|
701
|
-
title={strings.chartDownloadPng}
|
|
702
|
-
aria-label={strings.chartDownloadPngAria}
|
|
703
|
-
>
|
|
704
|
-
<svg
|
|
705
|
-
class="w-3 h-3 text-gray-500 dark:text-gray-400"
|
|
706
|
-
fill="none"
|
|
707
|
-
viewBox="0 0 24 24"
|
|
708
|
-
stroke="currentColor"
|
|
709
|
-
>
|
|
710
|
-
<path
|
|
711
|
-
stroke-linecap="round"
|
|
712
|
-
stroke-linejoin="round"
|
|
713
|
-
stroke-width="2"
|
|
714
|
-
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
715
|
-
/>
|
|
716
|
-
</svg>
|
|
717
|
-
</button>
|
|
718
|
-
</Show>
|
|
719
|
-
</div>
|
|
720
|
-
</div>
|
|
1078
|
+
</div>
|
|
721
1079
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1080
|
+
<p id={descriptionId} class="sr-only">
|
|
1081
|
+
{title()}. {strings.chartDataSummary}
|
|
1082
|
+
</p>
|
|
725
1083
|
|
|
726
|
-
|
|
1084
|
+
{/* Fallback ladder (P2.5): degrade to a series table on render error
|
|
727
1085
|
instead of a bare "Chart Error" message. */}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
<For each={tableData().columns}>
|
|
754
|
-
{(column) => (
|
|
755
|
-
<th scope="col" class="px-3 py-2 font-medium text-gray-900 dark:text-gray-100">
|
|
756
|
-
{column}
|
|
757
|
-
</th>
|
|
758
|
-
)}
|
|
759
|
-
</For>
|
|
760
|
-
</tr>
|
|
761
|
-
</thead>
|
|
762
|
-
<tbody>
|
|
763
|
-
<For each={tableData().rows}>
|
|
764
|
-
{(row) => (
|
|
765
|
-
<tr class="border-t border-gray-100 dark:border-gray-700">
|
|
1086
|
+
<Show when={error() && activeView() === 'chart'}>
|
|
1087
|
+
<DegradedFallback
|
|
1088
|
+
message={formatMCPUIString(strings.chartRenderFailed, { error: String(error()) })}
|
|
1089
|
+
caption={strings.chartDegradedCaption}
|
|
1090
|
+
{...tableData()}
|
|
1091
|
+
/>
|
|
1092
|
+
</Show>
|
|
1093
|
+
|
|
1094
|
+
<Show when={activeView() === 'data'}>
|
|
1095
|
+
<div
|
|
1096
|
+
class={`w-full overflow-auto rounded border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-700,#374151))] ${
|
|
1097
|
+
isExpanded() ? 'flex-1 min-h-0' : ''
|
|
1098
|
+
}`}
|
|
1099
|
+
style={
|
|
1100
|
+
isExpanded()
|
|
1101
|
+
? undefined
|
|
1102
|
+
: { height: params().height || '250px', 'max-height': '70vh' }
|
|
1103
|
+
}
|
|
1104
|
+
>
|
|
1105
|
+
<table class="w-full border-collapse text-left text-sm">
|
|
1106
|
+
<caption class="sr-only">
|
|
1107
|
+
{title()} — {strings.chartDataTable}
|
|
1108
|
+
</caption>
|
|
1109
|
+
<thead class="bg-[color:var(--mcp-ui-surface-muted,var(--color-gray-50,#f9fafb))] dark:bg-[color:var(--mcp-ui-surface,color-mix(in_oklab,var(--color-gray-900,#111827)_40%,transparent))]">
|
|
1110
|
+
<tr>
|
|
766
1111
|
<For each={tableData().columns}>
|
|
767
|
-
{(
|
|
768
|
-
<
|
|
769
|
-
|
|
770
|
-
|
|
1112
|
+
{(column) => (
|
|
1113
|
+
<th
|
|
1114
|
+
scope="col"
|
|
1115
|
+
class="px-3 py-2 font-medium text-[color:var(--mcp-ui-fg,var(--color-gray-900,#111827))] dark:text-[color:var(--mcp-ui-fg,var(--color-gray-100,#f3f4f6))]"
|
|
1116
|
+
>
|
|
1117
|
+
{column}
|
|
1118
|
+
</th>
|
|
771
1119
|
)}
|
|
772
1120
|
</For>
|
|
773
1121
|
</tr>
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
aria-hidden={activeView() !== 'chart' || Boolean(error())}
|
|
807
|
-
/>
|
|
808
|
-
<Show when={isLoading() && activeView() === 'chart'}>
|
|
809
|
-
<div class="absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-gray-800/80">
|
|
810
|
-
<div class="flex flex-col items-center gap-2">
|
|
811
|
-
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
|
812
|
-
<span class="text-sm text-gray-500 dark:text-gray-400">{strings.chartLoading}</span>
|
|
1122
|
+
</thead>
|
|
1123
|
+
<tbody>
|
|
1124
|
+
<For each={tableData().rows}>
|
|
1125
|
+
{(row, rowIndex) => (
|
|
1126
|
+
<tr class="border-t border-[color:var(--mcp-ui-border,var(--color-gray-100,#f3f4f6))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-700,#374151))]">
|
|
1127
|
+
<For each={tableData().columns}>
|
|
1128
|
+
{(_column, index) => (
|
|
1129
|
+
<td class="px-3 py-2 text-[color:var(--mcp-ui-fg,var(--color-gray-700,#374151))] dark:text-[color:var(--mcp-ui-fg,var(--color-gray-300,#d1d5db))]">
|
|
1130
|
+
<Show
|
|
1131
|
+
when={tableData().missing[rowIndex()]?.[index()]}
|
|
1132
|
+
fallback={String(row[index()] ?? '')}
|
|
1133
|
+
>
|
|
1134
|
+
<MissingValueMark />
|
|
1135
|
+
</Show>
|
|
1136
|
+
</td>
|
|
1137
|
+
)}
|
|
1138
|
+
</For>
|
|
1139
|
+
</tr>
|
|
1140
|
+
)}
|
|
1141
|
+
</For>
|
|
1142
|
+
<Show when={tableData().rows.length === 0}>
|
|
1143
|
+
<tr>
|
|
1144
|
+
<td
|
|
1145
|
+
colSpan={Math.max(1, tableData().columns.length)}
|
|
1146
|
+
class="px-3 py-6 text-center text-[color:var(--mcp-ui-fg-muted,var(--color-gray-500,#6b7280))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]"
|
|
1147
|
+
>
|
|
1148
|
+
{strings.chartNoData}
|
|
1149
|
+
</td>
|
|
1150
|
+
</tr>
|
|
1151
|
+
</Show>
|
|
1152
|
+
</tbody>
|
|
1153
|
+
</table>
|
|
813
1154
|
</div>
|
|
1155
|
+
<Show when={anyMissingCell()}>
|
|
1156
|
+
<MissingValueLegend class="mt-2" />
|
|
1157
|
+
</Show>
|
|
1158
|
+
</Show>
|
|
1159
|
+
|
|
1160
|
+
<div
|
|
1161
|
+
class={`relative w-full ${isExpanded() ? 'flex-1 min-h-0' : ''}`}
|
|
1162
|
+
style={
|
|
1163
|
+
error() || activeView() === 'data'
|
|
1164
|
+
? { display: 'none' }
|
|
1165
|
+
: isExpanded()
|
|
1166
|
+
? { height: '100%', display: 'block' }
|
|
1167
|
+
: { height: params().height || '250px', display: 'block' }
|
|
1168
|
+
}
|
|
1169
|
+
>
|
|
1170
|
+
<canvas
|
|
1171
|
+
ref={canvasRef}
|
|
1172
|
+
role="img"
|
|
1173
|
+
aria-label={title()}
|
|
1174
|
+
aria-describedby={notesShown() ? `${descriptionId} ${notesId}` : descriptionId}
|
|
1175
|
+
aria-hidden={activeView() !== 'chart' || Boolean(error())}
|
|
1176
|
+
/>
|
|
1177
|
+
<Show when={isLoading() && activeView() === 'chart'}>
|
|
1178
|
+
<div class="absolute inset-0 flex items-center justify-center bg-[color:var(--mcp-ui-surface,color-mix(in_oklab,var(--color-white,#fff)_80%,transparent))] dark:bg-[color:var(--mcp-ui-surface,color-mix(in_oklab,var(--color-gray-800,#1f2937)_80%,transparent))]">
|
|
1179
|
+
<div class="flex flex-col items-center gap-2">
|
|
1180
|
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-[color:var(--mcp-ui-accent-border,var(--color-blue-600))]" />
|
|
1181
|
+
<span class="text-sm text-[color:var(--mcp-ui-fg-muted,var(--color-gray-500,#6b7280))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]">
|
|
1182
|
+
{strings.chartLoading}
|
|
1183
|
+
</span>
|
|
1184
|
+
</div>
|
|
1185
|
+
</div>
|
|
1186
|
+
</Show>
|
|
814
1187
|
</div>
|
|
815
|
-
|
|
816
|
-
|
|
1188
|
+
|
|
1189
|
+
{/* Missing values (v6.24.0): how THIS chart type shows them, and which
|
|
1190
|
+
series have no data at all. Linked from the canvas, so a screen
|
|
1191
|
+
reader hears the convention with the chart. */}
|
|
1192
|
+
<Show when={notesShown()}>
|
|
1193
|
+
<div
|
|
1194
|
+
data-mcp-chart-notes=""
|
|
1195
|
+
id={notesId}
|
|
1196
|
+
class="mt-2 space-y-0.5 text-xs text-[color:var(--mcp-ui-fg-muted,var(--color-gray-600,#4b5563))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]"
|
|
1197
|
+
>
|
|
1198
|
+
<Show when={conventionNoteShown()}>
|
|
1199
|
+
<p data-mcp-chart-note={noteKind()}>
|
|
1200
|
+
{noteKind() === 'bars'
|
|
1201
|
+
? strings.chartMissingBars
|
|
1202
|
+
: noteKind() === 'omitted'
|
|
1203
|
+
? strings.chartMissingPoints
|
|
1204
|
+
: strings.chartMissingGaps}
|
|
1205
|
+
</p>
|
|
1206
|
+
</Show>
|
|
1207
|
+
<Show when={missingSummary().fullyMissing.some(Boolean)}>
|
|
1208
|
+
<p data-mcp-chart-note="no-data">
|
|
1209
|
+
{formatMCPUIString(strings.chartSeriesNoData, { series: noDataSeries() })}
|
|
1210
|
+
</p>
|
|
1211
|
+
</Show>
|
|
1212
|
+
</div>
|
|
1213
|
+
</Show>
|
|
817
1214
|
</div>
|
|
818
1215
|
)}
|
|
819
1216
|
/>
|