mviz 1.6.2 → 1.6.3

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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Client-side PNG export for ECharts charts.
3
+ *
4
+ * Generates JavaScript that adds a right-click context menu to each chart,
5
+ * allowing users to save high-resolution PNGs in three presets:
6
+ * - Square: 1080 x 1080
7
+ * - Portrait: 1080 x 1350 (4:5)
8
+ * - Landscape: 1920 x 1080 (16:9)
9
+ *
10
+ * The chart reflows to fill the full target canvas, with the title
11
+ * drawn above. Text scales proportionally via pixelRatio.
12
+ */
13
+ /**
14
+ * Return the CSS for the export context menu.
15
+ */
16
+ export declare function generateExportCss(): string;
17
+ /**
18
+ * Return the JavaScript that powers chart PNG export.
19
+ *
20
+ * Injected once into the page. Relies on `window.chartInstances`
21
+ * and `window.chartOptions` being populated by the renderer.
22
+ */
23
+ export declare function generateExportJs(): string;
24
+ //# sourceMappingURL=export.d.ts.map
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Client-side PNG export for ECharts charts.
3
+ *
4
+ * Generates JavaScript that adds a right-click context menu to each chart,
5
+ * allowing users to save high-resolution PNGs in three presets:
6
+ * - Square: 1080 x 1080
7
+ * - Portrait: 1080 x 1350 (4:5)
8
+ * - Landscape: 1920 x 1080 (16:9)
9
+ *
10
+ * The chart reflows to fill the full target canvas, with the title
11
+ * drawn above. Text scales proportionally via pixelRatio.
12
+ */
13
+ import { COLORS } from './themes.js';
14
+ /** Logical base width for export — matches typical mobile screen width. */
15
+ const LOGICAL_BASE = 360;
16
+ /** Title layout in logical pixels (scale with pixelRatio for output). */
17
+ const TITLE_FONT_SIZE = 14;
18
+ const TITLE_PADDING_TOP = 12;
19
+ const TITLE_PADDING_BOTTOM = 8;
20
+ const TITLE_PADDING_LEFT = 12;
21
+ const EXPORT_PRESETS = {
22
+ square: { width: 1080, height: 1080, label: 'Square (1080 × 1080)' },
23
+ portrait: { width: 1080, height: 1350, label: 'Portrait (1080 × 1350)' },
24
+ landscape: { width: 1920, height: 1080, label: 'Landscape (1920 × 1080)' },
25
+ };
26
+ /**
27
+ * Return the CSS for the export context menu.
28
+ */
29
+ export function generateExportCss() {
30
+ return `
31
+ .mviz-export-menu {
32
+ position: fixed;
33
+ z-index: 10000;
34
+ background: ${COLORS.WHITE};
35
+ border: 1px solid ${COLORS.GRAY_300};
36
+ border-radius: 4px;
37
+ box-shadow: 0 2px 8px rgba(0,0,0,0.18);
38
+ padding: 4px 0;
39
+ min-width: 200px;
40
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
41
+ font-size: 12px;
42
+ color: ${COLORS.TEXT_DARK};
43
+ }
44
+ body.theme-dark .mviz-export-menu {
45
+ background: ${COLORS.GRAY_800};
46
+ border-color: ${COLORS.GRAY_600};
47
+ color: ${COLORS.GRAY_200};
48
+ }
49
+ .mviz-export-menu-header {
50
+ padding: 6px 12px 4px;
51
+ font-size: 10px;
52
+ font-weight: 700;
53
+ text-transform: uppercase;
54
+ letter-spacing: 0.05em;
55
+ color: ${COLORS.GRAY_500};
56
+ }
57
+ body.theme-dark .mviz-export-menu-header { color: ${COLORS.GRAY_600}; }
58
+ .mviz-export-menu-item {
59
+ padding: 6px 12px;
60
+ cursor: pointer;
61
+ }
62
+ .mviz-export-menu-item:hover {
63
+ background: ${COLORS.GRAY_100};
64
+ }
65
+ body.theme-dark .mviz-export-menu-item:hover {
66
+ background: ${COLORS.GRID_DARK};
67
+ }
68
+ @media print {
69
+ .mviz-export-menu { display: none; }
70
+ }`;
71
+ }
72
+ /**
73
+ * Return the JavaScript that powers chart PNG export.
74
+ *
75
+ * Injected once into the page. Relies on `window.chartInstances`
76
+ * and `window.chartOptions` being populated by the renderer.
77
+ */
78
+ export function generateExportJs() {
79
+ const presets = JSON.stringify(EXPORT_PRESETS);
80
+ // Inject TypeScript constants into the JS runtime
81
+ const config = JSON.stringify({
82
+ LOGICAL_BASE,
83
+ TITLE_FONT_SIZE,
84
+ TITLE_PADDING_TOP,
85
+ TITLE_PADDING_BOTTOM,
86
+ TITLE_PADDING_LEFT,
87
+ BG_LIGHT: COLORS.BG_LIGHT,
88
+ BG_DARK: COLORS.DARK,
89
+ TEXT_LIGHT: COLORS.TEXT_DARK,
90
+ TEXT_DARK: COLORS.GRAY_200,
91
+ });
92
+ return `
93
+ // --- mviz PNG export ---
94
+ (function() {
95
+ var PRESETS = ${presets};
96
+ var CFG = ${config};
97
+ var menu = null;
98
+
99
+ function removeMenu() {
100
+ if (menu && menu.parentNode) { menu.parentNode.removeChild(menu); menu = null; }
101
+ }
102
+
103
+ function isDarkTheme() {
104
+ return document.body.classList.contains('theme-dark');
105
+ }
106
+
107
+ function getChartBg() {
108
+ var bg = getComputedStyle(document.body).backgroundColor;
109
+ return (bg && bg !== 'rgba(0, 0, 0, 0)') ? bg : (isDarkTheme() ? CFG.BG_DARK : CFG.BG_LIGHT);
110
+ }
111
+
112
+ function getChartTitle(chartEl) {
113
+ var gridItem = chartEl.closest('.grid-item');
114
+ if (gridItem) {
115
+ var h3 = gridItem.querySelector('.chart-title');
116
+ if (h3) return h3.textContent.trim();
117
+ }
118
+ return '';
119
+ }
120
+
121
+ function slugify(text) {
122
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'chart';
123
+ }
124
+
125
+ function renderChart(chartId, drawW, drawH) {
126
+ var tmpDiv = document.createElement('div');
127
+ tmpDiv.style.cssText = 'position:absolute;left:-9999px;top:-9999px;width:' + drawW + 'px;height:' + drawH + 'px;';
128
+ document.body.appendChild(tmpDiv);
129
+
130
+ var tmpChart = echarts.init(tmpDiv, null, {renderer: 'canvas', width: drawW, height: drawH});
131
+ var currentTheme = isDarkTheme() ? 'dark' : 'light';
132
+ var opts = window.chartOptions && window.chartOptions[chartId];
133
+ var instance = window.chartInstances[chartId];
134
+ tmpChart.setOption(opts ? opts[currentTheme] : instance.getOption());
135
+
136
+ return { div: tmpDiv, chart: tmpChart };
137
+ }
138
+
139
+ function drawTitle(ctx, title, pixelRatio) {
140
+ ctx.fillStyle = isDarkTheme() ? CFG.TEXT_DARK : CFG.TEXT_LIGHT;
141
+ ctx.font = 'bold ' + Math.round(CFG.TITLE_FONT_SIZE * pixelRatio) + 'px "Helvetica Neue", Helvetica, Arial, sans-serif';
142
+ ctx.textAlign = 'left';
143
+ ctx.textBaseline = 'top';
144
+ ctx.fillText(
145
+ title.toUpperCase(),
146
+ Math.round(CFG.TITLE_PADDING_LEFT * pixelRatio),
147
+ Math.round(CFG.TITLE_PADDING_TOP * pixelRatio)
148
+ );
149
+ }
150
+
151
+ function triggerDownload(canvas, filename) {
152
+ var a = document.createElement('a');
153
+ a.href = canvas.toDataURL('image/png');
154
+ a.download = filename;
155
+ a.click();
156
+ }
157
+
158
+ function exportChart(chartId, presetKey) {
159
+ if (!window.chartInstances || !window.chartInstances[chartId]) return;
160
+
161
+ var preset = PRESETS[presetKey];
162
+ var targetW = preset.width;
163
+ var targetH = preset.height;
164
+ var pixelRatio = targetW / CFG.LOGICAL_BASE;
165
+ var logicalH = Math.round(targetH / pixelRatio);
166
+
167
+ // Reserve space for title
168
+ var chartEl = document.getElementById(chartId);
169
+ var title = getChartTitle(chartEl);
170
+ var titleLogicalH = title ? (CFG.TITLE_PADDING_TOP + CFG.TITLE_FONT_SIZE + CFG.TITLE_PADDING_BOTTOM) : 0;
171
+
172
+ // ECharts reflows to fill its container — give it the full space
173
+ var tmp = renderChart(chartId, CFG.LOGICAL_BASE, logicalH - titleLogicalH);
174
+
175
+ // Wait a frame for render, then composite
176
+ setTimeout(function() {
177
+ var chartDataUrl = tmp.chart.getDataURL({type: 'png', pixelRatio: pixelRatio, backgroundColor: 'transparent'});
178
+ var titlePxH = Math.round(titleLogicalH * pixelRatio);
179
+
180
+ var canvas = document.createElement('canvas');
181
+ canvas.width = targetW;
182
+ canvas.height = targetH;
183
+ var ctx = canvas.getContext('2d');
184
+
185
+ ctx.fillStyle = getChartBg();
186
+ ctx.fillRect(0, 0, targetW, targetH);
187
+
188
+ if (title) { drawTitle(ctx, title, pixelRatio); }
189
+
190
+ var img = new Image();
191
+ img.onload = function() {
192
+ ctx.drawImage(img, 0, titlePxH, targetW, targetH - titlePxH);
193
+ triggerDownload(canvas, slugify(title || 'chart') + '-' + presetKey + '.png');
194
+ tmp.chart.dispose();
195
+ document.body.removeChild(tmp.div);
196
+ };
197
+ img.src = chartDataUrl;
198
+ }, 100);
199
+ }
200
+
201
+ function showMenu(e, chartId) {
202
+ e.preventDefault();
203
+ removeMenu();
204
+
205
+ menu = document.createElement('div');
206
+ menu.className = 'mviz-export-menu';
207
+
208
+ var header = document.createElement('div');
209
+ header.className = 'mviz-export-menu-header';
210
+ header.textContent = 'Save as PNG';
211
+ menu.appendChild(header);
212
+
213
+ var keys = ['square', 'portrait', 'landscape'];
214
+ for (var i = 0; i < keys.length; i++) {
215
+ (function(key) {
216
+ var item = document.createElement('div');
217
+ item.className = 'mviz-export-menu-item';
218
+ item.textContent = PRESETS[key].label;
219
+ item.addEventListener('click', function() {
220
+ removeMenu();
221
+ exportChart(chartId, key);
222
+ });
223
+ menu.appendChild(item);
224
+ })(keys[i]);
225
+ }
226
+
227
+ // Position near cursor, clamped to viewport
228
+ var x = e.clientX;
229
+ var y = e.clientY;
230
+ document.body.appendChild(menu);
231
+ var mw = menu.offsetWidth;
232
+ var mh = menu.offsetHeight;
233
+ if (x + mw > window.innerWidth) x = window.innerWidth - mw - 4;
234
+ if (y + mh > window.innerHeight) y = window.innerHeight - mh - 4;
235
+ menu.style.left = x + 'px';
236
+ menu.style.top = y + 'px';
237
+ }
238
+
239
+ // Dismiss on click elsewhere
240
+ document.addEventListener('click', removeMenu);
241
+ document.addEventListener('contextmenu', function(e) {
242
+ // Only show for chart containers that have an ECharts instance
243
+ var target = e.target;
244
+ var el = target.closest ? target.closest('[id]') : null;
245
+ while (el) {
246
+ if (window.chartInstances && window.chartInstances[el.id]) {
247
+ showMenu(e, el.id);
248
+ return;
249
+ }
250
+ el = el.parentElement ? el.parentElement.closest('[id]') : null;
251
+ }
252
+ // Not a chart — let default context menu through
253
+ removeMenu();
254
+ });
255
+ })();`;
256
+ }
257
+ //# sourceMappingURL=export.js.map
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * JSON serialization with JavaScript function embedding for ECharts
3
3
  */
4
+ import { generateExportCss, generateExportJs } from './export.js';
4
5
  /**
5
6
  * Special markers for JS functions that need to be embedded in JSON
6
7
  */
@@ -61,6 +62,8 @@ export function createJsFunction(code) {
61
62
  */
62
63
  export function wrapHtml(chartId, option, css, width, height) {
63
64
  const optionJson = serializeOption(option);
65
+ const exportCss = generateExportCss();
66
+ const exportJs = generateExportJs();
64
67
  return `<!DOCTYPE html>
65
68
  <html lang="en">
66
69
  <head>
@@ -69,15 +72,22 @@ export function wrapHtml(chartId, option, css, width, height) {
69
72
  <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
70
73
  <style>
71
74
  ${css}
75
+ ${exportCss}
72
76
  </style>
73
77
  </head>
74
78
  <body>
75
79
  <div id="${chartId}" style="width: ${width}; height: ${height}px;"></div>
76
80
  <script>
77
- var chart = echarts.init(document.getElementById('${chartId}'), null, {renderer: 'svg'});
78
81
  var option = ${optionJson};
82
+ var chart = echarts.init(document.getElementById('${chartId}'), null, {renderer: 'svg'});
83
+ window.chartInstances = window.chartInstances || {};
84
+ window.chartOptions = window.chartOptions || {};
85
+ window.chartInstances['${chartId}'] = chart;
86
+ window.chartOptions['${chartId}'] = { light: option, dark: option };
79
87
  chart.setOption(option);
80
88
  window.addEventListener('resize', function() { chart.resize(); });
89
+
90
+ ${exportJs}
81
91
  </script>
82
92
  </body>
83
93
  </html>`;
@@ -2,6 +2,7 @@
2
2
  * HTML templates for dashboard generation.
3
3
  */
4
4
  import { ECHARTS_CDN, PALETTE, COLORS, getThemeColors, getThemeColorsWithCustom, getPaletteWithCustom, getFontsWithCustom, getLinkColor, PRINT_PORTRAIT_WIDTH, PRINT_LANDSCAPE_WIDTH, } from '../core/themes.js';
5
+ import { generateExportCss, generateExportJs } from '../core/export.js';
5
6
  /**
6
7
  * Escape HTML special characters
7
8
  */
@@ -360,6 +361,8 @@ export function generateDashboardHtml(pageTitle, componentsHtml, chartScripts, t
360
361
  const initialThemeClass = theme === 'dark' ? 'theme-dark' : 'theme-light';
361
362
  const continuousClass = continuous ? ' dashboard-continuous' : '';
362
363
  const css = generateDashboardCss(customTheme, orientation);
364
+ const exportCss = generateExportCss();
365
+ const exportJs = generateExportJs();
363
366
  return `<!DOCTYPE html>
364
367
  <html lang="en">
365
368
  <head>
@@ -369,6 +372,7 @@ export function generateDashboardHtml(pageTitle, componentsHtml, chartScripts, t
369
372
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
370
373
  ${extraScripts}
371
374
  <style>${css}
375
+ ${exportCss}
372
376
  </style>
373
377
  </head>
374
378
  <body class="${initialThemeClass}">
@@ -416,6 +420,8 @@ export function generateDashboardHtml(pageTitle, componentsHtml, chartScripts, t
416
420
  }
417
421
  });
418
422
  });
423
+
424
+ ${exportJs}
419
425
  </script>
420
426
  </body>
421
427
  </html>`;
@@ -974,6 +980,8 @@ export function generateTestHarnessHtml(pageTitle, navItems, totalCharts, typesC
974
980
  const initialThemeClass = theme === 'dark' ? 'theme-dark' : 'theme-light';
975
981
  const css = generateTestHarnessCss();
976
982
  const sparklineJs = generateSparklineTooltipJs();
983
+ const exportCss = generateExportCss();
984
+ const exportJs = generateExportJs();
977
985
  return `<!DOCTYPE html>
978
986
  <html lang="en">
979
987
  <head>
@@ -983,6 +991,7 @@ export function generateTestHarnessHtml(pageTitle, navItems, totalCharts, typesC
983
991
  <script src="${ECHARTS_CDN}"></script>
984
992
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
985
993
  <style>${css}
994
+ ${exportCss}
986
995
  </style>
987
996
  </head>
988
997
  <body class="${initialThemeClass}">
@@ -1036,6 +1045,8 @@ export function generateTestHarnessHtml(pageTitle, navItems, totalCharts, typesC
1036
1045
  ${chartScripts}
1037
1046
 
1038
1047
  ${sparklineJs}
1048
+
1049
+ ${exportJs}
1039
1050
  </script>
1040
1051
  </body>
1041
1052
  </html>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mviz",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "A chart & report builder designed for use by AI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",