mviz 1.6.2 → 1.6.4
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/dist/core/export.d.ts +24 -0
- package/dist/core/export.js +262 -0
- package/dist/core/serializer.js +11 -1
- package/dist/core/themes.js +10 -5
- package/dist/layout/parser.js +10 -1
- package/dist/layout/templates.js +34 -19
- package/package.json +2 -2
|
@@ -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,262 @@
|
|
|
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: var(--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 getThemeFont() {
|
|
140
|
+
var fontFamily = getComputedStyle(document.body).fontFamily;
|
|
141
|
+
return fontFamily || '"Helvetica Neue", Helvetica, Arial, sans-serif';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function drawTitle(ctx, title, pixelRatio) {
|
|
145
|
+
ctx.fillStyle = isDarkTheme() ? CFG.TEXT_DARK : CFG.TEXT_LIGHT;
|
|
146
|
+
ctx.font = 'bold ' + Math.round(CFG.TITLE_FONT_SIZE * pixelRatio) + 'px ' + getThemeFont();
|
|
147
|
+
ctx.textAlign = 'left';
|
|
148
|
+
ctx.textBaseline = 'top';
|
|
149
|
+
ctx.fillText(
|
|
150
|
+
title.toUpperCase(),
|
|
151
|
+
Math.round(CFG.TITLE_PADDING_LEFT * pixelRatio),
|
|
152
|
+
Math.round(CFG.TITLE_PADDING_TOP * pixelRatio)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function triggerDownload(canvas, filename) {
|
|
157
|
+
var a = document.createElement('a');
|
|
158
|
+
a.href = canvas.toDataURL('image/png');
|
|
159
|
+
a.download = filename;
|
|
160
|
+
a.click();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function exportChart(chartId, presetKey) {
|
|
164
|
+
if (!window.chartInstances || !window.chartInstances[chartId]) return;
|
|
165
|
+
|
|
166
|
+
var preset = PRESETS[presetKey];
|
|
167
|
+
var targetW = preset.width;
|
|
168
|
+
var targetH = preset.height;
|
|
169
|
+
var pixelRatio = targetW / CFG.LOGICAL_BASE;
|
|
170
|
+
var logicalH = Math.round(targetH / pixelRatio);
|
|
171
|
+
|
|
172
|
+
// Reserve space for title
|
|
173
|
+
var chartEl = document.getElementById(chartId);
|
|
174
|
+
var title = getChartTitle(chartEl);
|
|
175
|
+
var titleLogicalH = title ? (CFG.TITLE_PADDING_TOP + CFG.TITLE_FONT_SIZE + CFG.TITLE_PADDING_BOTTOM) : 0;
|
|
176
|
+
|
|
177
|
+
// ECharts reflows to fill its container — give it the full space
|
|
178
|
+
var tmp = renderChart(chartId, CFG.LOGICAL_BASE, logicalH - titleLogicalH);
|
|
179
|
+
|
|
180
|
+
// Wait a frame for render, then composite
|
|
181
|
+
setTimeout(function() {
|
|
182
|
+
var chartDataUrl = tmp.chart.getDataURL({type: 'png', pixelRatio: pixelRatio, backgroundColor: 'transparent'});
|
|
183
|
+
var titlePxH = Math.round(titleLogicalH * pixelRatio);
|
|
184
|
+
|
|
185
|
+
var canvas = document.createElement('canvas');
|
|
186
|
+
canvas.width = targetW;
|
|
187
|
+
canvas.height = targetH;
|
|
188
|
+
var ctx = canvas.getContext('2d');
|
|
189
|
+
|
|
190
|
+
ctx.fillStyle = getChartBg();
|
|
191
|
+
ctx.fillRect(0, 0, targetW, targetH);
|
|
192
|
+
|
|
193
|
+
if (title) { drawTitle(ctx, title, pixelRatio); }
|
|
194
|
+
|
|
195
|
+
var img = new Image();
|
|
196
|
+
img.onload = function() {
|
|
197
|
+
ctx.drawImage(img, 0, titlePxH, targetW, targetH - titlePxH);
|
|
198
|
+
triggerDownload(canvas, slugify(title || 'chart') + '-' + presetKey + '.png');
|
|
199
|
+
tmp.chart.dispose();
|
|
200
|
+
document.body.removeChild(tmp.div);
|
|
201
|
+
};
|
|
202
|
+
img.src = chartDataUrl;
|
|
203
|
+
}, 100);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function showMenu(e, chartId) {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
removeMenu();
|
|
209
|
+
|
|
210
|
+
menu = document.createElement('div');
|
|
211
|
+
menu.className = 'mviz-export-menu';
|
|
212
|
+
|
|
213
|
+
var header = document.createElement('div');
|
|
214
|
+
header.className = 'mviz-export-menu-header';
|
|
215
|
+
header.textContent = 'Save as PNG';
|
|
216
|
+
menu.appendChild(header);
|
|
217
|
+
|
|
218
|
+
var keys = ['square', 'portrait', 'landscape'];
|
|
219
|
+
for (var i = 0; i < keys.length; i++) {
|
|
220
|
+
(function(key) {
|
|
221
|
+
var item = document.createElement('div');
|
|
222
|
+
item.className = 'mviz-export-menu-item';
|
|
223
|
+
item.textContent = PRESETS[key].label;
|
|
224
|
+
item.addEventListener('click', function() {
|
|
225
|
+
removeMenu();
|
|
226
|
+
exportChart(chartId, key);
|
|
227
|
+
});
|
|
228
|
+
menu.appendChild(item);
|
|
229
|
+
})(keys[i]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Position near cursor, clamped to viewport
|
|
233
|
+
var x = e.clientX;
|
|
234
|
+
var y = e.clientY;
|
|
235
|
+
document.body.appendChild(menu);
|
|
236
|
+
var mw = menu.offsetWidth;
|
|
237
|
+
var mh = menu.offsetHeight;
|
|
238
|
+
if (x + mw > window.innerWidth) x = window.innerWidth - mw - 4;
|
|
239
|
+
if (y + mh > window.innerHeight) y = window.innerHeight - mh - 4;
|
|
240
|
+
menu.style.left = x + 'px';
|
|
241
|
+
menu.style.top = y + 'px';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Dismiss on click elsewhere
|
|
245
|
+
document.addEventListener('click', removeMenu);
|
|
246
|
+
document.addEventListener('contextmenu', function(e) {
|
|
247
|
+
// Only show for chart containers that have an ECharts instance
|
|
248
|
+
var target = e.target;
|
|
249
|
+
var el = target.closest ? target.closest('[id]') : null;
|
|
250
|
+
while (el) {
|
|
251
|
+
if (window.chartInstances && window.chartInstances[el.id]) {
|
|
252
|
+
showMenu(e, el.id);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
el = el.parentElement ? el.parentElement.closest('[id]') : null;
|
|
256
|
+
}
|
|
257
|
+
// Not a chart — let default context menu through
|
|
258
|
+
removeMenu();
|
|
259
|
+
});
|
|
260
|
+
})();`;
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=export.js.map
|
package/dist/core/serializer.js
CHANGED
|
@@ -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>`;
|
package/dist/core/themes.js
CHANGED
|
@@ -193,11 +193,15 @@ export function getPalette(theme = 'light') {
|
|
|
193
193
|
* Custom theme can specify which base theme to extend.
|
|
194
194
|
*/
|
|
195
195
|
export function getThemeColorsWithCustom(theme = 'light', custom) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
// Only apply custom color overrides when the requested theme matches
|
|
197
|
+
// the theme the custom colors were designed for (custom.extends).
|
|
198
|
+
// For the opposite theme, use standard defaults so dark mode isn't
|
|
199
|
+
// broken by light-mode custom colors (and vice versa).
|
|
200
|
+
const customExtends = custom?.extends ?? 'light';
|
|
201
|
+
if (!custom?.colors || theme !== customExtends) {
|
|
202
|
+
return getThemeColors(theme);
|
|
200
203
|
}
|
|
204
|
+
const baseColors = getThemeColors(customExtends);
|
|
201
205
|
return { ...baseColors, ...custom.colors };
|
|
202
206
|
}
|
|
203
207
|
/**
|
|
@@ -249,10 +253,11 @@ export function getTooltipConfig(theme = 'light') {
|
|
|
249
253
|
export function getBaseOption(spec) {
|
|
250
254
|
const theme = spec.theme ?? 'light';
|
|
251
255
|
const colors = getThemeColors(theme);
|
|
256
|
+
const fonts = getFontsWithCustom(spec.customTheme);
|
|
252
257
|
return {
|
|
253
258
|
backgroundColor: 'transparent',
|
|
254
259
|
textStyle: {
|
|
255
|
-
fontFamily:
|
|
260
|
+
fontFamily: fonts.family,
|
|
256
261
|
color: colors.text,
|
|
257
262
|
},
|
|
258
263
|
title: spec.title
|
package/dist/layout/parser.js
CHANGED
|
@@ -12,7 +12,7 @@ import { getOptionsBuilder } from '../charts/index.js';
|
|
|
12
12
|
import { serializeOption } from '../core/serializer.js';
|
|
13
13
|
import { formatNumber, inferFormat, resolveCurrency } from '../core/formatting.js';
|
|
14
14
|
import { lintSpec } from '../core/linter.js';
|
|
15
|
-
import { COLORS, GRID_TOTAL_COLUMNS, DEFAULT_SIZES, autoSizeChart, getHeatmapColors, getThemeColors, getThemeColorsWithCustom, ALERT_ICONS, } from '../core/themes.js';
|
|
15
|
+
import { COLORS, GRID_TOTAL_COLUMNS, DEFAULT_SIZES, autoSizeChart, getHeatmapColors, getThemeColors, getThemeColorsWithCustom, getFontsWithCustom, ALERT_ICONS, } from '../core/themes.js';
|
|
16
16
|
import { renderMermaid, renderMermaidAscii } from 'beautiful-mermaid';
|
|
17
17
|
import { formatCell, computeHeatmapRanges, computeDumbbellRanges, } from '../components/table.js';
|
|
18
18
|
// Height per row unit (compact for print) - must match Python's ROW_HEIGHT_PX
|
|
@@ -139,16 +139,25 @@ function renderEchartsComponent(compType, spec, chartId, colSpan, itemHeight, an
|
|
|
139
139
|
}
|
|
140
140
|
const chartHeight = itemHeight - (title ? 24 : 0);
|
|
141
141
|
// Note: Time series validation is handled by the linter when specs are parsed
|
|
142
|
+
// Resolve custom font for ECharts textStyle injection
|
|
143
|
+
const fontFamily = getFontsWithCustom(customTheme).family;
|
|
144
|
+
function applyFont(option) {
|
|
145
|
+
option.textStyle = { ...(option.textStyle ?? {}), fontFamily };
|
|
146
|
+
}
|
|
142
147
|
// Generate light theme options
|
|
143
148
|
const specLight = { ...spec, theme: 'light', height: chartHeight, customTheme };
|
|
144
149
|
const optionLight = builder(specLight);
|
|
145
150
|
if (!optionLight) {
|
|
146
151
|
return { html: null, script: null };
|
|
147
152
|
}
|
|
153
|
+
applyFont(optionLight);
|
|
148
154
|
const optionJsonLight = serializeOption(optionLight);
|
|
149
155
|
// Generate dark theme options
|
|
150
156
|
const specDark = { ...spec, theme: 'dark', height: chartHeight, customTheme };
|
|
151
157
|
const optionDark = builder(specDark);
|
|
158
|
+
if (optionDark) {
|
|
159
|
+
applyFont(optionDark);
|
|
160
|
+
}
|
|
152
161
|
const optionJsonDark = optionDark ? serializeOption(optionDark) : null;
|
|
153
162
|
const anchorAttr = anchorId ? ` id="${anchorId}"` : '';
|
|
154
163
|
const html = `
|
package/dist/layout/templates.js
CHANGED
|
@@ -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
|
*/
|
|
@@ -102,7 +103,7 @@ export function generateDashboardCss(customTheme, orientation = 'portrait') {
|
|
|
102
103
|
margin-bottom: 8px;
|
|
103
104
|
}
|
|
104
105
|
.page-title {
|
|
105
|
-
font-family:
|
|
106
|
+
font-family: var(--font-family);
|
|
106
107
|
font-size: 18px;
|
|
107
108
|
font-weight: 900;
|
|
108
109
|
margin-bottom: 12px;
|
|
@@ -118,7 +119,7 @@ export function generateDashboardCss(customTheme, orientation = 'portrait') {
|
|
|
118
119
|
border-top: 1px solid var(--grid);
|
|
119
120
|
}
|
|
120
121
|
.section-title {
|
|
121
|
-
font-family:
|
|
122
|
+
font-family: var(--font-family);
|
|
122
123
|
font-size: 14px;
|
|
123
124
|
font-weight: 900;
|
|
124
125
|
letter-spacing: 0.05em;
|
|
@@ -127,7 +128,7 @@ export function generateDashboardCss(customTheme, orientation = 'portrait') {
|
|
|
127
128
|
margin-bottom: 16px;
|
|
128
129
|
}
|
|
129
130
|
.chart-title {
|
|
130
|
-
font-family:
|
|
131
|
+
font-family: var(--font-family); font-weight: 700;
|
|
131
132
|
font-size: 10px;
|
|
132
133
|
font-weight: bold;
|
|
133
134
|
color: var(--text);
|
|
@@ -156,6 +157,7 @@ export function generateDashboardCss(customTheme, orientation = 'portrait') {
|
|
|
156
157
|
.grid-item { break-inside: avoid; page-break-inside: avoid; }
|
|
157
158
|
.dashboard-section { break-inside: avoid; page-break-inside: avoid; }
|
|
158
159
|
.dashboard-section.page-break { page-break-before: always; }
|
|
160
|
+
.section-title { break-after: avoid; page-break-after: avoid; }
|
|
159
161
|
.data-table { break-inside: avoid; page-break-inside: avoid; }
|
|
160
162
|
.theme-toggle { display: none; }
|
|
161
163
|
.title-row { break-after: avoid; page-break-after: avoid; }
|
|
@@ -171,7 +173,7 @@ export function generateDashboardCss(customTheme, orientation = 'portrait') {
|
|
|
171
173
|
transition: background 0.3s;
|
|
172
174
|
}
|
|
173
175
|
.big-value {
|
|
174
|
-
font-family:
|
|
176
|
+
font-family: var(--font-family); font-weight: 700;
|
|
175
177
|
font-size: 28px;
|
|
176
178
|
font-weight: bold;
|
|
177
179
|
color: var(--text);
|
|
@@ -186,7 +188,7 @@ export function generateDashboardCss(customTheme, orientation = 'portrait') {
|
|
|
186
188
|
font-size: 16px;
|
|
187
189
|
}
|
|
188
190
|
.delta .value {
|
|
189
|
-
font-family:
|
|
191
|
+
font-family: var(--font-family);
|
|
190
192
|
font-size: 16px;
|
|
191
193
|
font-weight: bold;
|
|
192
194
|
}
|
|
@@ -243,7 +245,7 @@ export function generateDashboardCss(customTheme, orientation = 'portrait') {
|
|
|
243
245
|
width: 100%; border-collapse: collapse; font-size: 11px;
|
|
244
246
|
}
|
|
245
247
|
.data-table th {
|
|
246
|
-
font-family:
|
|
248
|
+
font-family: var(--font-family); font-size: 9px; font-weight: bold;
|
|
247
249
|
color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.03em;
|
|
248
250
|
padding: 6px 8px; border-bottom: 1px solid var(--text); text-align: left;
|
|
249
251
|
}
|
|
@@ -289,7 +291,7 @@ export function generateDashboardCss(customTheme, orientation = 'portrait') {
|
|
|
289
291
|
margin-bottom: 4px;
|
|
290
292
|
}
|
|
291
293
|
.inline-header-text {
|
|
292
|
-
font-family:
|
|
294
|
+
font-family: var(--font-family);
|
|
293
295
|
font-size: 11px;
|
|
294
296
|
font-weight: 700;
|
|
295
297
|
color: var(--text);
|
|
@@ -318,7 +320,7 @@ export function generateDashboardCss(customTheme, orientation = 'portrait') {
|
|
|
318
320
|
.pct-bar-value {
|
|
319
321
|
font-size: 11px;
|
|
320
322
|
color: var(--text);
|
|
321
|
-
font-family:
|
|
323
|
+
font-family: var(--font-family);
|
|
322
324
|
font-weight: 700;
|
|
323
325
|
min-width: 32px;
|
|
324
326
|
flex-shrink: 0;
|
|
@@ -339,7 +341,7 @@ export function generateDashboardCss(customTheme, orientation = 'portrait') {
|
|
|
339
341
|
padding: 4px 8px;
|
|
340
342
|
border-radius: 4px;
|
|
341
343
|
font-size: 11px;
|
|
342
|
-
font-family:
|
|
344
|
+
font-family: var(--font-family);
|
|
343
345
|
white-space: nowrap;
|
|
344
346
|
opacity: 0;
|
|
345
347
|
visibility: hidden;
|
|
@@ -360,6 +362,8 @@ export function generateDashboardHtml(pageTitle, componentsHtml, chartScripts, t
|
|
|
360
362
|
const initialThemeClass = theme === 'dark' ? 'theme-dark' : 'theme-light';
|
|
361
363
|
const continuousClass = continuous ? ' dashboard-continuous' : '';
|
|
362
364
|
const css = generateDashboardCss(customTheme, orientation);
|
|
365
|
+
const exportCss = generateExportCss();
|
|
366
|
+
const exportJs = generateExportJs();
|
|
363
367
|
return `<!DOCTYPE html>
|
|
364
368
|
<html lang="en">
|
|
365
369
|
<head>
|
|
@@ -369,6 +373,7 @@ export function generateDashboardHtml(pageTitle, componentsHtml, chartScripts, t
|
|
|
369
373
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
370
374
|
${extraScripts}
|
|
371
375
|
<style>${css}
|
|
376
|
+
${exportCss}
|
|
372
377
|
</style>
|
|
373
378
|
</head>
|
|
374
379
|
<body class="${initialThemeClass}">
|
|
@@ -416,6 +421,8 @@ export function generateDashboardHtml(pageTitle, componentsHtml, chartScripts, t
|
|
|
416
421
|
}
|
|
417
422
|
});
|
|
418
423
|
});
|
|
424
|
+
|
|
425
|
+
${exportJs}
|
|
419
426
|
</script>
|
|
420
427
|
</body>
|
|
421
428
|
</html>`;
|
|
@@ -441,6 +448,8 @@ export function generateTestHarnessCss() {
|
|
|
441
448
|
--grid: ${colorsLight.border};
|
|
442
449
|
--red: ${colorsLight.accent};
|
|
443
450
|
--link: ${linkColor};
|
|
451
|
+
--font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
452
|
+
--font-mono: 'Consolas', 'Monaco', monospace;
|
|
444
453
|
}
|
|
445
454
|
body.theme-dark {
|
|
446
455
|
--bg: ${colorsDark.background};
|
|
@@ -453,7 +462,7 @@ export function generateTestHarnessCss() {
|
|
|
453
462
|
}
|
|
454
463
|
|
|
455
464
|
body {
|
|
456
|
-
font-family:
|
|
465
|
+
font-family: var(--font-family);
|
|
457
466
|
background: var(--bg);
|
|
458
467
|
color: var(--text);
|
|
459
468
|
transition: background-color 0.3s, color 0.3s;
|
|
@@ -598,7 +607,7 @@ export function generateTestHarnessCss() {
|
|
|
598
607
|
margin-bottom: 40px;
|
|
599
608
|
}
|
|
600
609
|
.section-title {
|
|
601
|
-
font-family:
|
|
610
|
+
font-family: var(--font-family);
|
|
602
611
|
font-size: 14px;
|
|
603
612
|
font-weight: 900;
|
|
604
613
|
letter-spacing: 0.05em;
|
|
@@ -628,7 +637,7 @@ export function generateTestHarnessCss() {
|
|
|
628
637
|
margin-bottom: 8px;
|
|
629
638
|
}
|
|
630
639
|
.page-title {
|
|
631
|
-
font-family:
|
|
640
|
+
font-family: var(--font-family);
|
|
632
641
|
font-size: 18px;
|
|
633
642
|
font-weight: 900;
|
|
634
643
|
margin-bottom: 12px;
|
|
@@ -644,7 +653,7 @@ export function generateTestHarnessCss() {
|
|
|
644
653
|
border-top: 1px solid var(--grid);
|
|
645
654
|
}
|
|
646
655
|
.chart-title {
|
|
647
|
-
font-family:
|
|
656
|
+
font-family: var(--font-family); font-weight: 700;
|
|
648
657
|
font-size: 10px;
|
|
649
658
|
font-weight: bold;
|
|
650
659
|
color: var(--text);
|
|
@@ -682,7 +691,7 @@ export function generateTestHarnessCss() {
|
|
|
682
691
|
|
|
683
692
|
/* Component styles */
|
|
684
693
|
.big-value {
|
|
685
|
-
font-family:
|
|
694
|
+
font-family: var(--font-family); font-weight: 700;
|
|
686
695
|
font-size: 28px;
|
|
687
696
|
font-weight: bold;
|
|
688
697
|
color: var(--text);
|
|
@@ -697,7 +706,7 @@ export function generateTestHarnessCss() {
|
|
|
697
706
|
font-size: 16px;
|
|
698
707
|
}
|
|
699
708
|
.delta .value {
|
|
700
|
-
font-family:
|
|
709
|
+
font-family: var(--font-family);
|
|
701
710
|
font-size: 16px;
|
|
702
711
|
font-weight: bold;
|
|
703
712
|
}
|
|
@@ -751,7 +760,7 @@ export function generateTestHarnessCss() {
|
|
|
751
760
|
width: 100%; border-collapse: collapse; font-size: 11px;
|
|
752
761
|
}
|
|
753
762
|
.data-table th {
|
|
754
|
-
font-family:
|
|
763
|
+
font-family: var(--font-family); font-size: 9px; font-weight: bold;
|
|
755
764
|
color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.03em;
|
|
756
765
|
padding: 6px 8px; border-bottom: 1px solid var(--text); text-align: left;
|
|
757
766
|
}
|
|
@@ -778,7 +787,7 @@ export function generateTestHarnessCss() {
|
|
|
778
787
|
margin-bottom: 4px;
|
|
779
788
|
}
|
|
780
789
|
.inline-header-text {
|
|
781
|
-
font-family:
|
|
790
|
+
font-family: var(--font-family);
|
|
782
791
|
font-size: 11px;
|
|
783
792
|
font-weight: 700;
|
|
784
793
|
color: var(--text);
|
|
@@ -808,7 +817,7 @@ export function generateTestHarnessCss() {
|
|
|
808
817
|
.pct-bar-value {
|
|
809
818
|
font-size: 11px;
|
|
810
819
|
color: var(--text);
|
|
811
|
-
font-family:
|
|
820
|
+
font-family: var(--font-family);
|
|
812
821
|
font-weight: 700;
|
|
813
822
|
min-width: 32px;
|
|
814
823
|
flex-shrink: 0;
|
|
@@ -830,7 +839,7 @@ export function generateTestHarnessCss() {
|
|
|
830
839
|
padding: 4px 8px;
|
|
831
840
|
border-radius: 4px;
|
|
832
841
|
font-size: 11px;
|
|
833
|
-
font-family:
|
|
842
|
+
font-family: var(--font-family);
|
|
834
843
|
white-space: nowrap;
|
|
835
844
|
opacity: 0;
|
|
836
845
|
visibility: hidden;
|
|
@@ -874,6 +883,7 @@ export function generateTestHarnessCss() {
|
|
|
874
883
|
.grid-item { break-inside: avoid; page-break-inside: avoid; }
|
|
875
884
|
.dashboard-section { break-inside: avoid; page-break-inside: avoid; }
|
|
876
885
|
.dashboard-section.page-break { page-break-before: always; }
|
|
886
|
+
.section-title { break-after: avoid; page-break-after: avoid; }
|
|
877
887
|
.data-table { break-inside: avoid; page-break-inside: avoid; }
|
|
878
888
|
header { display: none; }
|
|
879
889
|
.annotation-bar { display: none; }
|
|
@@ -974,6 +984,8 @@ export function generateTestHarnessHtml(pageTitle, navItems, totalCharts, typesC
|
|
|
974
984
|
const initialThemeClass = theme === 'dark' ? 'theme-dark' : 'theme-light';
|
|
975
985
|
const css = generateTestHarnessCss();
|
|
976
986
|
const sparklineJs = generateSparklineTooltipJs();
|
|
987
|
+
const exportCss = generateExportCss();
|
|
988
|
+
const exportJs = generateExportJs();
|
|
977
989
|
return `<!DOCTYPE html>
|
|
978
990
|
<html lang="en">
|
|
979
991
|
<head>
|
|
@@ -983,6 +995,7 @@ export function generateTestHarnessHtml(pageTitle, navItems, totalCharts, typesC
|
|
|
983
995
|
<script src="${ECHARTS_CDN}"></script>
|
|
984
996
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
985
997
|
<style>${css}
|
|
998
|
+
${exportCss}
|
|
986
999
|
</style>
|
|
987
1000
|
</head>
|
|
988
1001
|
<body class="${initialThemeClass}">
|
|
@@ -1036,6 +1049,8 @@ export function generateTestHarnessHtml(pageTitle, navItems, totalCharts, typesC
|
|
|
1036
1049
|
${chartScripts}
|
|
1037
1050
|
|
|
1038
1051
|
${sparklineJs}
|
|
1052
|
+
|
|
1053
|
+
${exportJs}
|
|
1039
1054
|
</script>
|
|
1040
1055
|
</body>
|
|
1041
1056
|
</html>`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mviz",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
4
4
|
"description": "A chart & report builder designed for use by AI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -83,6 +83,6 @@
|
|
|
83
83
|
"dependencies": {
|
|
84
84
|
"ajv": "^8.17.1",
|
|
85
85
|
"ajv-formats": "^3.0.1",
|
|
86
|
-
"beautiful-mermaid": "^
|
|
86
|
+
"beautiful-mermaid": "^1.1.3"
|
|
87
87
|
}
|
|
88
88
|
}
|