mviz 1.4.3 → 1.4.5
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/README.md +1 -1
- package/dist/charts/area.js +9 -0
- package/dist/charts/bar.js +11 -0
- package/dist/charts/line.js +9 -0
- package/dist/core/themes.d.ts +1 -0
- package/dist/core/themes.js +2 -0
- package/dist/core/validation.d.ts +22 -0
- package/dist/core/validation.js +120 -0
- package/dist/layout/parser.js +29 -2
- package/dist/layout/templates.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,7 +166,7 @@ mviz is available as a Claude skill for use in Claude.ai, Claude Code, and the C
|
|
|
166
166
|
Build the skill bundle:
|
|
167
167
|
```bash
|
|
168
168
|
npm run build
|
|
169
|
-
python3
|
|
169
|
+
python3 build_skill_compact.py -o mviz.skill
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
## License
|
package/dist/charts/area.js
CHANGED
|
@@ -5,6 +5,7 @@ import { FONT_SIZE_XXS, LEGEND_ITEM_WIDTH, LEGEND_ITEM_HEIGHT, LEGEND_ITEM_GAP,
|
|
|
5
5
|
import { wrapHtml } from '../core/serializer.js';
|
|
6
6
|
import { registerChart, registerOptions } from './registry.js';
|
|
7
7
|
import { inferFormat, getAxisFormatterJs, inferAxisType } from '../core/formatting.js';
|
|
8
|
+
import { validateTimeSeriesSorted, generateErrorHtml } from '../core/validation.js';
|
|
8
9
|
/**
|
|
9
10
|
* Build ECharts options for an area chart
|
|
10
11
|
*/
|
|
@@ -127,7 +128,15 @@ export function buildAreaOptions(spec) {
|
|
|
127
128
|
* Generate an area chart
|
|
128
129
|
*/
|
|
129
130
|
function generateAreaChart(spec) {
|
|
131
|
+
const data = (spec.data ?? []);
|
|
132
|
+
const x = spec.x ?? 'name';
|
|
133
|
+
const theme = (spec.theme ?? 'light');
|
|
130
134
|
const height = typeof spec.height === 'number' ? spec.height : DEFAULT_CHART_HEIGHT;
|
|
135
|
+
// Validate time series data is sorted
|
|
136
|
+
const validation = validateTimeSeriesSorted(data, x);
|
|
137
|
+
if (!validation.isValid) {
|
|
138
|
+
return generateErrorHtml(validation.errorMessage, theme, '100%', height);
|
|
139
|
+
}
|
|
131
140
|
return wrapHtml('chart', buildAreaOptions(spec), '', '100%', height);
|
|
132
141
|
}
|
|
133
142
|
// Register the chart
|
package/dist/charts/bar.js
CHANGED
|
@@ -5,6 +5,7 @@ import { BAR_MAX_WIDTH, LEGEND_ITEM_WIDTH, LEGEND_ITEM_HEIGHT, LEGEND_ITEM_GAP,
|
|
|
5
5
|
import { wrapHtml } from '../core/serializer.js';
|
|
6
6
|
import { registerChart, registerOptions } from './registry.js';
|
|
7
7
|
import { inferFormat, getLabelFormatterJs, getAxisFormatterJs, inferAxisType } from '../core/formatting.js';
|
|
8
|
+
import { validateTimeSeriesSorted, generateErrorHtml } from '../core/validation.js';
|
|
8
9
|
/**
|
|
9
10
|
* Build ECharts options for a bar chart
|
|
10
11
|
*/
|
|
@@ -181,7 +182,17 @@ export function buildBarOptions(spec) {
|
|
|
181
182
|
* Generate a bar chart
|
|
182
183
|
*/
|
|
183
184
|
function generateBarChart(spec) {
|
|
185
|
+
const data = (spec.data ?? []);
|
|
186
|
+
const x = spec.x ?? 'name';
|
|
187
|
+
const theme = (spec.theme ?? 'light');
|
|
184
188
|
const height = typeof spec.height === 'number' ? spec.height : DEFAULT_CHART_HEIGHT;
|
|
189
|
+
// Validate time series data is sorted (only for non-horizontal bars)
|
|
190
|
+
if (!spec.horizontal) {
|
|
191
|
+
const validation = validateTimeSeriesSorted(data, x);
|
|
192
|
+
if (!validation.isValid) {
|
|
193
|
+
return generateErrorHtml(validation.errorMessage, theme, '100%', height);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
185
196
|
return wrapHtml('chart', buildBarOptions(spec), '', '100%', height);
|
|
186
197
|
}
|
|
187
198
|
// Register the chart
|
package/dist/charts/line.js
CHANGED
|
@@ -5,6 +5,7 @@ import { FONT_SIZE_TINY, DEFAULT_CHART_HEIGHT, getThemeColors, getPalette, } fro
|
|
|
5
5
|
import { wrapHtml } from '../core/serializer.js';
|
|
6
6
|
import { registerChart, registerOptions } from './registry.js';
|
|
7
7
|
import { inferFormat, getAxisFormatterJs, inferAxisType } from '../core/formatting.js';
|
|
8
|
+
import { validateTimeSeriesSorted, generateErrorHtml } from '../core/validation.js';
|
|
8
9
|
/**
|
|
9
10
|
* Build ECharts options for a line chart
|
|
10
11
|
*/
|
|
@@ -124,7 +125,15 @@ export function buildLineOptions(spec) {
|
|
|
124
125
|
* Generate a line chart
|
|
125
126
|
*/
|
|
126
127
|
function generateLineChart(spec) {
|
|
128
|
+
const data = (spec.data ?? []);
|
|
129
|
+
const x = spec.x ?? 'name';
|
|
130
|
+
const theme = (spec.theme ?? 'light');
|
|
127
131
|
const height = typeof spec.height === 'number' ? spec.height : DEFAULT_CHART_HEIGHT;
|
|
132
|
+
// Validate time series data is sorted
|
|
133
|
+
const validation = validateTimeSeriesSorted(data, x);
|
|
134
|
+
if (!validation.isValid) {
|
|
135
|
+
return generateErrorHtml(validation.errorMessage, theme, '100%', height);
|
|
136
|
+
}
|
|
128
137
|
return wrapHtml('chart', buildLineOptions(spec), '', '100%', height);
|
|
129
138
|
}
|
|
130
139
|
// Register the chart
|
package/dist/core/themes.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Theme definitions and utilities for mviz
|
|
3
3
|
*/
|
|
4
4
|
import type { Theme, ThemeColors, ChartSpec } from '../types.js';
|
|
5
|
+
export declare const ERROR_RED = "#bc1200";
|
|
5
6
|
export declare const COLORS: {
|
|
6
7
|
readonly PRIMARY_BLUE: "#0777b3";
|
|
7
8
|
readonly SECONDARY_ORANGE: "#bd4e35";
|
package/dist/core/themes.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data validation utilities for mviz charts
|
|
3
|
+
*/
|
|
4
|
+
import type { DataPoint, Theme } from '../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Result of time series validation
|
|
7
|
+
*/
|
|
8
|
+
export interface TimeSeriesValidation {
|
|
9
|
+
isValid: boolean;
|
|
10
|
+
errorMessage?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Check if time series data is sorted in ascending order.
|
|
14
|
+
* Returns validation result with error message if unsorted.
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateTimeSeriesSorted(data: DataPoint[], xField: string): TimeSeriesValidation;
|
|
17
|
+
/**
|
|
18
|
+
* Generate an error HTML component to display instead of a chart
|
|
19
|
+
* when validation fails.
|
|
20
|
+
*/
|
|
21
|
+
export declare function generateErrorHtml(errorMessage: string, theme: Theme, width: string, height: number): string;
|
|
22
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data validation utilities for mviz charts
|
|
3
|
+
*/
|
|
4
|
+
import { FONT_STACK, getThemeColors, ERROR_RED } from './themes.js';
|
|
5
|
+
import { inferAxisType } from './formatting.js';
|
|
6
|
+
/**
|
|
7
|
+
* Check if time series data is sorted in ascending order.
|
|
8
|
+
* Returns validation result with error message if unsorted.
|
|
9
|
+
*/
|
|
10
|
+
export function validateTimeSeriesSorted(data, xField) {
|
|
11
|
+
if (data.length < 2) {
|
|
12
|
+
return { isValid: true };
|
|
13
|
+
}
|
|
14
|
+
const xValues = data.map((d) => d[xField]);
|
|
15
|
+
const axisType = inferAxisType(xValues);
|
|
16
|
+
// Only validate if this is a time axis
|
|
17
|
+
if (axisType !== 'time') {
|
|
18
|
+
return { isValid: true };
|
|
19
|
+
}
|
|
20
|
+
// Convert values to timestamps for comparison
|
|
21
|
+
const timestamps = xValues.map((v) => {
|
|
22
|
+
if (typeof v === 'number')
|
|
23
|
+
return v;
|
|
24
|
+
if (typeof v === 'string') {
|
|
25
|
+
const parsed = Date.parse(v);
|
|
26
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
27
|
+
}
|
|
28
|
+
if (v instanceof Date)
|
|
29
|
+
return v.getTime();
|
|
30
|
+
return 0;
|
|
31
|
+
});
|
|
32
|
+
// Check if sorted in ascending order
|
|
33
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
34
|
+
if (timestamps[i] < timestamps[i - 1]) {
|
|
35
|
+
return {
|
|
36
|
+
isValid: false,
|
|
37
|
+
errorMessage: `Time series axis is not sorted chronologically. Sort your data before charting.`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { isValid: true };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Generate an error HTML component to display instead of a chart
|
|
45
|
+
* when validation fails.
|
|
46
|
+
*/
|
|
47
|
+
export function generateErrorHtml(errorMessage, theme, width, height) {
|
|
48
|
+
const colors = getThemeColors(theme);
|
|
49
|
+
return `<!DOCTYPE html>
|
|
50
|
+
<html lang="en">
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="UTF-8">
|
|
53
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
54
|
+
<style>
|
|
55
|
+
* { box-sizing: border-box; }
|
|
56
|
+
html, body {
|
|
57
|
+
margin: 0;
|
|
58
|
+
padding: 0;
|
|
59
|
+
width: 100%;
|
|
60
|
+
height: 100%;
|
|
61
|
+
background-color: ${colors.background};
|
|
62
|
+
font-family: ${FONT_STACK};
|
|
63
|
+
}
|
|
64
|
+
.error-container {
|
|
65
|
+
width: ${width};
|
|
66
|
+
height: ${height}px;
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
padding: 24px;
|
|
71
|
+
}
|
|
72
|
+
.error-box {
|
|
73
|
+
border: 2px solid ${ERROR_RED};
|
|
74
|
+
border-radius: 8px;
|
|
75
|
+
padding: 20px 24px;
|
|
76
|
+
background-color: ${theme === 'light' ? '#fff5f5' : '#2d1f1f'};
|
|
77
|
+
max-width: 500px;
|
|
78
|
+
}
|
|
79
|
+
.error-title {
|
|
80
|
+
color: ${ERROR_RED};
|
|
81
|
+
font-size: 14px;
|
|
82
|
+
font-weight: 700;
|
|
83
|
+
margin: 0 0 8px 0;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: 8px;
|
|
87
|
+
}
|
|
88
|
+
.error-icon {
|
|
89
|
+
font-size: 18px;
|
|
90
|
+
}
|
|
91
|
+
.error-message {
|
|
92
|
+
color: ${colors.text};
|
|
93
|
+
font-size: 13px;
|
|
94
|
+
line-height: 1.5;
|
|
95
|
+
margin: 0;
|
|
96
|
+
}
|
|
97
|
+
</style>
|
|
98
|
+
</head>
|
|
99
|
+
<body>
|
|
100
|
+
<div class="error-container">
|
|
101
|
+
<div class="error-box">
|
|
102
|
+
<p class="error-title"><span class="error-icon">⚠</span> Chart Error</p>
|
|
103
|
+
<p class="error-message">${escapeHtml(errorMessage)}</p>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</body>
|
|
107
|
+
</html>`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Escape HTML special characters
|
|
111
|
+
*/
|
|
112
|
+
function escapeHtml(text) {
|
|
113
|
+
return text
|
|
114
|
+
.replace(/&/g, '&')
|
|
115
|
+
.replace(/</g, '<')
|
|
116
|
+
.replace(/>/g, '>')
|
|
117
|
+
.replace(/"/g, '"')
|
|
118
|
+
.replace(/'/g, ''');
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=validation.js.map
|
package/dist/layout/parser.js
CHANGED
|
@@ -11,6 +11,7 @@ import { parseCsv } from './csv.js';
|
|
|
11
11
|
import { getOptionsBuilder } from '../charts/index.js';
|
|
12
12
|
import { serializeOption } from '../core/serializer.js';
|
|
13
13
|
import { formatNumber, inferFormat } from '../core/formatting.js';
|
|
14
|
+
import { validateTimeSeriesSorted } from '../core/validation.js';
|
|
14
15
|
import { COLORS, GRID_TOTAL_COLUMNS, DEFAULT_SIZES, autoSizeChart, getHeatmapColors, getThemeColors, ALERT_ICONS, } from '../core/themes.js';
|
|
15
16
|
import { formatCell, computeHeatmapRanges, computeDumbbellRanges, } from '../components/table.js';
|
|
16
17
|
// Height per row unit (compact for print) - must match Python's ROW_HEIGHT_PX
|
|
@@ -122,6 +123,30 @@ function renderEchartsComponent(compType, spec, chartId, colSpan, itemHeight, an
|
|
|
122
123
|
return { html: null, script: null };
|
|
123
124
|
}
|
|
124
125
|
const chartHeight = itemHeight - (title ? 24 : 0);
|
|
126
|
+
// Validate time series data for line, bar (non-horizontal), and area charts
|
|
127
|
+
const timeSeriesTypes = ['line', 'area'];
|
|
128
|
+
const isBarNonHorizontal = compType === 'bar' && !spec.horizontal;
|
|
129
|
+
if (timeSeriesTypes.includes(compType) || isBarNonHorizontal) {
|
|
130
|
+
const data = (spec.data ?? []);
|
|
131
|
+
const xField = spec.x ?? 'name';
|
|
132
|
+
const validation = validateTimeSeriesSorted(data, xField);
|
|
133
|
+
if (!validation.isValid) {
|
|
134
|
+
const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
|
|
135
|
+
const errorContent = `
|
|
136
|
+
<div style="border: 2px solid #bc1200; border-radius: 8px; padding: 20px 24px; background-color: #fff5f5; max-width: 500px; margin: auto;">
|
|
137
|
+
<p style="color: #bc1200; font-size: 14px; font-weight: 700; margin: 0 0 8px 0;">⚠ Chart Not Rendered</p>
|
|
138
|
+
<p style="color: #231f20; font-size: 13px; line-height: 1.5; margin: 0;">Time series axis is not sorted chronologically. Sort your data before charting.</p>
|
|
139
|
+
</div>`;
|
|
140
|
+
const html = `
|
|
141
|
+
<div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
|
|
142
|
+
${titleHtml}
|
|
143
|
+
<div style="width: 100%; height: ${chartHeight}px; display: flex; align-items: center; justify-content: center;">
|
|
144
|
+
${errorContent}
|
|
145
|
+
</div>
|
|
146
|
+
</div>`;
|
|
147
|
+
return { html, script: null };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
125
150
|
// Generate light theme options
|
|
126
151
|
const specLight = { ...spec, theme: 'light', height: chartHeight };
|
|
127
152
|
const optionLight = builder(specLight);
|
|
@@ -785,9 +810,11 @@ export function parseMarkdownToDashboard(markdown, baseTheme = 'light', _baseDir
|
|
|
785
810
|
const builder = getOptionsBuilder(compType);
|
|
786
811
|
if (builder) {
|
|
787
812
|
const { html, script } = renderEchartsComponent(compType, spec, chartId, colSpan, itemHeight, anchorId);
|
|
788
|
-
if (html
|
|
813
|
+
if (html) {
|
|
789
814
|
rowItems.push(html);
|
|
790
|
-
|
|
815
|
+
if (script) {
|
|
816
|
+
chartScripts.push(script);
|
|
817
|
+
}
|
|
791
818
|
}
|
|
792
819
|
else {
|
|
793
820
|
rowItems.push(`<div class="grid-item"><p>Error rendering ${compType}</p></div>`);
|
package/dist/layout/templates.js
CHANGED
|
@@ -55,6 +55,7 @@ export function generateDashboardCss() {
|
|
|
55
55
|
margin: 0 auto;
|
|
56
56
|
padding: 16px;
|
|
57
57
|
position: relative;
|
|
58
|
+
overflow-x: hidden;
|
|
58
59
|
}
|
|
59
60
|
.title-row {
|
|
60
61
|
display: flex;
|
|
@@ -144,6 +145,7 @@ export function generateDashboardCss() {
|
|
|
144
145
|
@media (max-width: 480px) {
|
|
145
146
|
.row { grid-template-columns: 1fr; }
|
|
146
147
|
.row > * { grid-column: span 1; }
|
|
148
|
+
.grid-item { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
147
149
|
}
|
|
148
150
|
@media print {
|
|
149
151
|
.dashboard { max-width: 100%; padding: 0; }
|
|
@@ -600,6 +602,7 @@ export function generateTestHarnessCss() {
|
|
|
600
602
|
max-width: 720px;
|
|
601
603
|
margin: 0 auto;
|
|
602
604
|
padding: 16px;
|
|
605
|
+
overflow-x: hidden;
|
|
603
606
|
}
|
|
604
607
|
.red-line {
|
|
605
608
|
width: 100%;
|
|
@@ -644,6 +647,7 @@ export function generateTestHarnessCss() {
|
|
|
644
647
|
@media (max-width: 480px) {
|
|
645
648
|
.row { grid-template-columns: 1fr; }
|
|
646
649
|
.row > * { grid-column: span 1; }
|
|
650
|
+
.grid-item { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
647
651
|
}
|
|
648
652
|
|
|
649
653
|
/* Grid items */
|