mviz 1.6.6 → 1.7.0-pre.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/charts/area.js +8 -37
- package/dist/charts/bar.js +8 -27
- package/dist/charts/combo.js +17 -41
- package/dist/charts/line.js +6 -28
- package/dist/cli.js +15 -4
- package/dist/components/big_value.d.ts +23 -1
- package/dist/components/big_value.js +84 -25
- package/dist/components/delta.d.ts +24 -1
- package/dist/components/delta.js +63 -17
- package/dist/components/table-interactivity.d.ts +17 -1
- package/dist/components/table-interactivity.js +34 -1
- package/dist/components/table.js +1 -1
- package/dist/core/chart-helpers.d.ts +59 -5
- package/dist/core/chart-helpers.js +84 -5
- package/dist/core/formatting.d.ts +33 -0
- package/dist/core/formatting.js +135 -0
- package/dist/core/fragment.d.ts +28 -0
- package/dist/core/fragment.js +658 -0
- package/dist/core/linter.d.ts +3 -0
- package/dist/core/linter.js +110 -47
- package/dist/core/serializer.d.ts +3 -1
- package/dist/core/serializer.js +3 -2
- package/dist/layout/block-loader.d.ts +31 -0
- package/dist/layout/block-loader.js +143 -0
- package/dist/layout/dispatcher.d.ts +7 -2
- package/dist/layout/dispatcher.js +12 -16
- package/dist/layout/layout-resolver.d.ts +33 -0
- package/dist/layout/layout-resolver.js +73 -0
- package/dist/layout/markdown-parser.d.ts +34 -0
- package/dist/layout/markdown-parser.js +401 -0
- package/dist/layout/parser-types.d.ts +123 -0
- package/dist/layout/parser-types.js +11 -0
- package/dist/layout/parser.d.ts +31 -22
- package/dist/layout/parser.js +124 -1069
- package/dist/layout/renderer.d.ts +36 -0
- package/dist/layout/renderer.js +473 -0
- package/dist/layout/templates.d.ts +2 -2
- package/dist/layout/templates.js +74 -35
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/schema/mviz.v1.schema.json +359 -33
package/dist/charts/area.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Area chart generator
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { DEFAULT_CHART_HEIGHT, getThemeColors, getPaletteWithCustom, } from '../core/themes.js';
|
|
5
5
|
import { wrapHtml } from '../core/serializer.js';
|
|
6
6
|
import { registerChart, registerOptions } from './registry.js';
|
|
7
|
-
import {
|
|
7
|
+
import { inferAxisType } from '../core/formatting.js';
|
|
8
|
+
import { buildValueAxisFormatContext, buildXAxisLabelConfig, applyValueAxisRange, buildMultiSeriesLegend, } from '../core/chart-helpers.js';
|
|
8
9
|
/**
|
|
9
10
|
* Build ECharts options for an area chart
|
|
10
11
|
*/
|
|
@@ -19,30 +20,12 @@ export function buildAreaOptions(spec) {
|
|
|
19
20
|
const yKeys = Array.isArray(y) ? y : [y];
|
|
20
21
|
const categories = data.map((d) => d[x]);
|
|
21
22
|
const palette = getPaletteWithCustom(theme, spec.customTheme);
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
const sampleValue = data.length > 0 && data[0] ? data[0][firstYKey] ?? 0 : 0;
|
|
25
|
-
const valueFormat = spec.format ?? inferFormat(firstYKey, sampleValue);
|
|
26
|
-
const currency = resolveCurrency(spec.currency);
|
|
27
|
-
const pctMultiply = shouldPctMultiply(valueFormat, collectNumericFieldValues(data, yKeys));
|
|
28
|
-
const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale, pctMultiply);
|
|
23
|
+
// Resolve value-axis format/currency/formatters
|
|
24
|
+
const { axisFormatter } = buildValueAxisFormatContext(spec, data, yKeys);
|
|
29
25
|
// Detect x-axis type: time, value, or category
|
|
30
26
|
const xAxisType = spec.xAxisType ?? inferAxisType(categories);
|
|
31
27
|
// Get axis label config based on axis type
|
|
32
|
-
const xAxisLabelConfig = xAxisType
|
|
33
|
-
? { interval: 0, rotate: 45, overflow: 'truncate', ellipsis: '...', width: 100 }
|
|
34
|
-
: xAxisType === 'time'
|
|
35
|
-
? {
|
|
36
|
-
hideOverlap: true,
|
|
37
|
-
formatter: {
|
|
38
|
-
_js_: `function(value) {
|
|
39
|
-
var d = new Date(value);
|
|
40
|
-
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
41
|
-
return months[d.getMonth()] + ' ' + d.getDate();
|
|
42
|
-
}`,
|
|
43
|
-
},
|
|
44
|
-
}
|
|
45
|
-
: { hideOverlap: true };
|
|
28
|
+
const xAxisLabelConfig = buildXAxisLabelConfig(xAxisType);
|
|
46
29
|
const option = {
|
|
47
30
|
backgroundColor: 'transparent',
|
|
48
31
|
animation: false,
|
|
@@ -84,22 +67,10 @@ export function buildAreaOptions(spec) {
|
|
|
84
67
|
series: [],
|
|
85
68
|
};
|
|
86
69
|
// Apply yMin/yMax
|
|
87
|
-
|
|
88
|
-
option.yAxis.min = spec.yMin;
|
|
89
|
-
}
|
|
90
|
-
if (spec.yMax !== undefined) {
|
|
91
|
-
option.yAxis.max = spec.yMax;
|
|
92
|
-
}
|
|
70
|
+
applyValueAxisRange(option.yAxis, spec);
|
|
93
71
|
// Add legend for multi-series
|
|
94
72
|
if (yKeys.length > 1) {
|
|
95
|
-
option.legend =
|
|
96
|
-
data: yKeys,
|
|
97
|
-
top: 0,
|
|
98
|
-
textStyle: { color: colors.textSecondary, fontSize: FONT_SIZE_XXS },
|
|
99
|
-
itemWidth: LEGEND_ITEM_WIDTH,
|
|
100
|
-
itemHeight: LEGEND_ITEM_HEIGHT,
|
|
101
|
-
itemGap: LEGEND_ITEM_GAP,
|
|
102
|
-
};
|
|
73
|
+
option.legend = buildMultiSeriesLegend(yKeys, colors);
|
|
103
74
|
}
|
|
104
75
|
// Build series
|
|
105
76
|
for (let i = 0; i < yKeys.length; i++) {
|
package/dist/charts/bar.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bar chart generator
|
|
3
3
|
*/
|
|
4
|
-
import { BAR_MAX_WIDTH,
|
|
4
|
+
import { BAR_MAX_WIDTH, FONT_SIZE_TINY, FONT_SIZE_XXS, DEFAULT_CHART_HEIGHT, getThemeColors, getPaletteWithCustom, } from '../core/themes.js';
|
|
5
5
|
import { wrapHtml } from '../core/serializer.js';
|
|
6
6
|
import { registerChart, registerOptions } from './registry.js';
|
|
7
|
-
import {
|
|
7
|
+
import { inferAxisType } from '../core/formatting.js';
|
|
8
|
+
import { buildValueAxisFormatContext, buildXAxisLabelConfig, applyValueAxisRange, buildMultiSeriesLegend, } from '../core/chart-helpers.js';
|
|
8
9
|
/**
|
|
9
10
|
* Build ECharts options for a bar chart
|
|
10
11
|
*/
|
|
@@ -19,14 +20,8 @@ export function buildBarOptions(spec) {
|
|
|
19
20
|
const yKeys = Array.isArray(y) ? y : [y];
|
|
20
21
|
const categories = data.map((d) => d[x]);
|
|
21
22
|
const palette = getPaletteWithCustom(theme, spec.customTheme);
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
const sampleValue = data.length > 0 && data[0] ? data[0][firstYKey] ?? 0 : 0;
|
|
25
|
-
const valueFormat = spec.format ?? inferFormat(firstYKey, sampleValue);
|
|
26
|
-
const currency = resolveCurrency(spec.currency);
|
|
27
|
-
const pctMultiply = shouldPctMultiply(valueFormat, collectNumericFieldValues(data, yKeys));
|
|
28
|
-
const labelFormatter = getLabelFormatterJs(valueFormat, currency.symbol, currency.locale, pctMultiply);
|
|
29
|
-
const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale, pctMultiply);
|
|
23
|
+
// Resolve value-axis format/currency/formatters
|
|
24
|
+
const { axisFormatter, labelFormatter } = buildValueAxisFormatContext(spec, data, yKeys, { withLabelFormatter: true });
|
|
30
25
|
// Detect category axis type (this is x-axis for vertical, y-axis for horizontal)
|
|
31
26
|
const categoryAxisType = spec.xAxisType ?? inferAxisType(categories);
|
|
32
27
|
// For horizontal bars, x-axis is always 'value'; for vertical it's the category axis type
|
|
@@ -34,9 +29,7 @@ export function buildBarOptions(spec) {
|
|
|
34
29
|
// Determine if labels should be shown (based on category axis type, not x-axis for horizontal)
|
|
35
30
|
const showLabels = yKeys.length === 1 && !stacked && data.length <= 12 && categoryAxisType === 'category';
|
|
36
31
|
// Get axis label config based on axis type
|
|
37
|
-
const xAxisLabelConfig = xAxisType
|
|
38
|
-
? { interval: 0, rotate: 45, overflow: 'truncate', ellipsis: '...', width: 100 }
|
|
39
|
-
: { hideOverlap: true };
|
|
32
|
+
const xAxisLabelConfig = buildXAxisLabelConfig(xAxisType);
|
|
40
33
|
const option = {
|
|
41
34
|
backgroundColor: 'transparent',
|
|
42
35
|
animation: false,
|
|
@@ -111,12 +104,7 @@ export function buildBarOptions(spec) {
|
|
|
111
104
|
};
|
|
112
105
|
// Apply yMin/yMax to value axis
|
|
113
106
|
const valueAxis = horizontal ? 'xAxis' : 'yAxis';
|
|
114
|
-
|
|
115
|
-
option[valueAxis].min = spec.yMin;
|
|
116
|
-
}
|
|
117
|
-
if (spec.yMax !== undefined) {
|
|
118
|
-
option[valueAxis].max = spec.yMax;
|
|
119
|
-
}
|
|
107
|
+
applyValueAxisRange(option[valueAxis], spec);
|
|
120
108
|
// Adjust y-axis gridlines based on x-axis category label length
|
|
121
109
|
// Long category labels (rotated 45°) take up vertical space at bottom
|
|
122
110
|
const chartHeight = typeof spec.height === 'number' ? spec.height : DEFAULT_CHART_HEIGHT;
|
|
@@ -137,14 +125,7 @@ export function buildBarOptions(spec) {
|
|
|
137
125
|
}
|
|
138
126
|
// Add legend for multi-series
|
|
139
127
|
if (yKeys.length > 1) {
|
|
140
|
-
option.legend =
|
|
141
|
-
data: yKeys,
|
|
142
|
-
top: 0,
|
|
143
|
-
textStyle: { color: colors.textSecondary, fontSize: FONT_SIZE_XXS },
|
|
144
|
-
itemWidth: LEGEND_ITEM_WIDTH,
|
|
145
|
-
itemHeight: LEGEND_ITEM_HEIGHT,
|
|
146
|
-
itemGap: LEGEND_ITEM_GAP,
|
|
147
|
-
};
|
|
128
|
+
option.legend = buildMultiSeriesLegend(yKeys, colors);
|
|
148
129
|
}
|
|
149
130
|
// Use lighter label color for dark theme for better visibility
|
|
150
131
|
const labelColor = theme === 'dark' ? colors.text : colors.textSecondary;
|
package/dist/charts/combo.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Combo chart generator (bar + line with optional dual axis)
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { BAR_MAX_WIDTH, getThemeColors, getPaletteWithCustom, } from '../core/themes.js';
|
|
5
5
|
import { wrapHtml } from '../core/serializer.js';
|
|
6
|
-
import { inferAxisType, inferFormat
|
|
6
|
+
import { inferAxisType, inferFormat } from '../core/formatting.js';
|
|
7
|
+
import { buildValueAxisFormatContext, buildXAxisLabelConfig, applyValueAxisRange, buildMultiSeriesLegend, } from '../core/chart-helpers.js';
|
|
7
8
|
import { registerChart, registerOptions } from './registry.js';
|
|
8
9
|
/**
|
|
9
10
|
* Build ECharts options for a combo chart (bar + line with optional dual axis)
|
|
@@ -20,34 +21,21 @@ export function buildComboOptions(spec) {
|
|
|
20
21
|
const barKeys = Array.isArray(barFields) ? barFields : barFields ? [barFields] : [];
|
|
21
22
|
const lineKeys = Array.isArray(lineFields) ? lineFields : lineFields ? [lineFields] : [];
|
|
22
23
|
const categories = data.map((d) => d[x]);
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const primaryAxisFormatter =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const secondaryAxisFormatter =
|
|
24
|
+
// Resolve format/currency/formatters for primary y-axis (bars) and secondary (line).
|
|
25
|
+
//
|
|
26
|
+
// The secondary axis must NOT inherit spec.format (which is the primary/bar
|
|
27
|
+
// format), so we resolve it explicitly here from spec.secondaryFormat or by
|
|
28
|
+
// inferring from the line field, then pass it as an explicit override.
|
|
29
|
+
const { axisFormatter: primaryAxisFormatter } = buildValueAxisFormatContext(spec, data, barKeys);
|
|
30
|
+
const lineSample = data.length > 0 && data[0] && lineKeys[0]
|
|
31
|
+
? data[0][lineKeys[0]] ?? 0
|
|
32
|
+
: 0;
|
|
33
|
+
const resolvedSecondaryFormat = spec.secondaryFormat ?? inferFormat(lineKeys[0] ?? 'value', lineSample);
|
|
34
|
+
const { axisFormatter: secondaryAxisFormatter } = buildValueAxisFormatContext(spec, data, lineKeys, { formatOverride: resolvedSecondaryFormat });
|
|
34
35
|
// Detect x-axis type
|
|
35
36
|
const xAxisType = inferAxisType(categories);
|
|
36
37
|
// Get axis label config based on axis type
|
|
37
|
-
const xAxisLabelConfig = xAxisType
|
|
38
|
-
? { interval: 0, rotate: 45, overflow: 'truncate', ellipsis: '...', width: 100 }
|
|
39
|
-
: xAxisType === 'time'
|
|
40
|
-
? {
|
|
41
|
-
hideOverlap: true,
|
|
42
|
-
formatter: {
|
|
43
|
-
_js_: `function(value) {
|
|
44
|
-
var d = new Date(value);
|
|
45
|
-
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
46
|
-
return months[d.getMonth()] + ' ' + d.getDate();
|
|
47
|
-
}`,
|
|
48
|
-
},
|
|
49
|
-
}
|
|
50
|
-
: { hideOverlap: true };
|
|
38
|
+
const xAxisLabelConfig = buildXAxisLabelConfig(xAxisType);
|
|
51
39
|
const series = [];
|
|
52
40
|
// Add bar series
|
|
53
41
|
barKeys.forEach((key, i) => {
|
|
@@ -96,12 +84,7 @@ export function buildComboOptions(spec) {
|
|
|
96
84
|
},
|
|
97
85
|
];
|
|
98
86
|
// Apply yMin/yMax to primary y-axis
|
|
99
|
-
|
|
100
|
-
yAxis[0].min = spec.yMin;
|
|
101
|
-
}
|
|
102
|
-
if (spec.yMax !== undefined) {
|
|
103
|
-
yAxis[0].max = spec.yMax;
|
|
104
|
-
}
|
|
87
|
+
applyValueAxisRange(yAxis[0], spec);
|
|
105
88
|
if (dualAxis) {
|
|
106
89
|
yAxis.push({
|
|
107
90
|
type: 'value',
|
|
@@ -130,14 +113,7 @@ export function buildComboOptions(spec) {
|
|
|
130
113
|
bottom: '8%',
|
|
131
114
|
containLabel: true,
|
|
132
115
|
},
|
|
133
|
-
legend:
|
|
134
|
-
data: [...barKeys, ...lineKeys],
|
|
135
|
-
top: 0,
|
|
136
|
-
textStyle: { color: colors.textSecondary, fontSize: FONT_SIZE_XXS },
|
|
137
|
-
itemWidth: LEGEND_ITEM_WIDTH,
|
|
138
|
-
itemHeight: LEGEND_ITEM_HEIGHT,
|
|
139
|
-
itemGap: LEGEND_ITEM_GAP,
|
|
140
|
-
},
|
|
116
|
+
legend: buildMultiSeriesLegend([...barKeys, ...lineKeys], colors),
|
|
141
117
|
xAxis: {
|
|
142
118
|
type: xAxisType,
|
|
143
119
|
data: xAxisType === 'category' ? categories.map(String) : undefined,
|
package/dist/charts/line.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import { FONT_SIZE_TINY, DEFAULT_CHART_HEIGHT, getThemeColors, getPaletteWithCustom, } from '../core/themes.js';
|
|
5
5
|
import { wrapHtml } from '../core/serializer.js';
|
|
6
6
|
import { registerChart, registerOptions } from './registry.js';
|
|
7
|
-
import {
|
|
7
|
+
import { inferAxisType } from '../core/formatting.js';
|
|
8
|
+
import { buildValueAxisFormatContext, buildXAxisLabelConfig, applyValueAxisRange, } from '../core/chart-helpers.js';
|
|
8
9
|
/**
|
|
9
10
|
* Build ECharts options for a line chart
|
|
10
11
|
*/
|
|
@@ -18,33 +19,15 @@ export function buildLineOptions(spec) {
|
|
|
18
19
|
const yKeys = Array.isArray(y) ? y : [y];
|
|
19
20
|
const categories = data.map((d) => d[x]);
|
|
20
21
|
const palette = getPaletteWithCustom(theme, spec.customTheme);
|
|
21
|
-
//
|
|
22
|
-
const
|
|
23
|
-
const sampleValue = data.length > 0 && data[0] ? data[0][firstYKey] ?? 0 : 0;
|
|
24
|
-
const valueFormat = spec.format ?? inferFormat(firstYKey, sampleValue);
|
|
25
|
-
const currency = resolveCurrency(spec.currency);
|
|
26
|
-
const pctMultiply = shouldPctMultiply(valueFormat, collectNumericFieldValues(data, yKeys));
|
|
27
|
-
const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale, pctMultiply);
|
|
22
|
+
// Resolve value-axis format/currency/formatters
|
|
23
|
+
const { axisFormatter } = buildValueAxisFormatContext(spec, data, yKeys);
|
|
28
24
|
// Detect x-axis type: time, value, or category
|
|
29
25
|
// User can override with xAxisType in spec
|
|
30
26
|
const xAxisType = spec.xAxisType ?? inferAxisType(categories);
|
|
31
27
|
// More right margin needed for end labels on multi-series
|
|
32
28
|
const hasEndLabels = yKeys.length > 1;
|
|
33
29
|
// Get axis label config based on axis type
|
|
34
|
-
const xAxisLabelConfig = xAxisType
|
|
35
|
-
? { interval: 0, rotate: 45, overflow: 'truncate', ellipsis: '...', width: 100 }
|
|
36
|
-
: xAxisType === 'time'
|
|
37
|
-
? {
|
|
38
|
-
hideOverlap: true,
|
|
39
|
-
formatter: {
|
|
40
|
-
_js_: `function(value) {
|
|
41
|
-
var d = new Date(value);
|
|
42
|
-
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
43
|
-
return months[d.getMonth()] + ' ' + d.getDate();
|
|
44
|
-
}`,
|
|
45
|
-
},
|
|
46
|
-
}
|
|
47
|
-
: { hideOverlap: true };
|
|
30
|
+
const xAxisLabelConfig = buildXAxisLabelConfig(xAxisType);
|
|
48
31
|
const option = {
|
|
49
32
|
backgroundColor: 'transparent',
|
|
50
33
|
animation: false,
|
|
@@ -87,12 +70,7 @@ export function buildLineOptions(spec) {
|
|
|
87
70
|
series: [],
|
|
88
71
|
};
|
|
89
72
|
// Apply yMin/yMax
|
|
90
|
-
|
|
91
|
-
option.yAxis.min = spec.yMin;
|
|
92
|
-
}
|
|
93
|
-
if (spec.yMax !== undefined) {
|
|
94
|
-
option.yAxis.max = spec.yMax;
|
|
95
|
-
}
|
|
73
|
+
applyValueAxisRange(option.yAxis, spec);
|
|
96
74
|
// Build series
|
|
97
75
|
for (let i = 0; i < yKeys.length; i++) {
|
|
98
76
|
const key = yKeys[i];
|
package/dist/cli.js
CHANGED
|
@@ -110,6 +110,8 @@ async function main() {
|
|
|
110
110
|
console.log(' --lint, -l Validate input without generating output');
|
|
111
111
|
console.log(' --theme, -t <file> Load custom theme from YAML file');
|
|
112
112
|
console.log(' --allow-errors Continue generating output even with lint errors');
|
|
113
|
+
console.log(' --embed Strip page chrome (red bar, title, theme toggle) for embedding');
|
|
114
|
+
console.log(' --fragment Emit an HTML fragment (no DOCTYPE/html/head/body); implies --embed');
|
|
113
115
|
console.log(' --help, -h Show this help message');
|
|
114
116
|
console.log('');
|
|
115
117
|
console.log('Examples:');
|
|
@@ -123,9 +125,16 @@ async function main() {
|
|
|
123
125
|
// Parse flags
|
|
124
126
|
const lintOnly = args.includes('--lint') || args.includes('-l');
|
|
125
127
|
const allowErrors = args.includes('--allow-errors');
|
|
128
|
+
const fragmentFlag = args.includes('--fragment');
|
|
129
|
+
// Fragment mode implies embed — no chrome inside a fragment.
|
|
130
|
+
const embedFlag = fragmentFlag || args.includes('--embed');
|
|
126
131
|
const { themeFile, remainingArgs: afterTheme } = parseThemeFlag(args);
|
|
127
132
|
const { outputFile, remainingArgs } = parseOutputFlag(afterTheme);
|
|
128
|
-
const filteredArgs = remainingArgs.filter((a) => a !== '--lint' &&
|
|
133
|
+
const filteredArgs = remainingArgs.filter((a) => a !== '--lint' &&
|
|
134
|
+
a !== '-l' &&
|
|
135
|
+
a !== '--allow-errors' &&
|
|
136
|
+
a !== '--embed' &&
|
|
137
|
+
a !== '--fragment');
|
|
129
138
|
// Load custom theme if specified
|
|
130
139
|
let customTheme;
|
|
131
140
|
if (themeFile) {
|
|
@@ -168,6 +177,8 @@ async function main() {
|
|
|
168
177
|
console.error(' --lint, -l Validate input without generating output');
|
|
169
178
|
console.error(' --theme, -t <file> Load custom theme from YAML file');
|
|
170
179
|
console.error(' --allow-errors Continue generating output even with lint errors');
|
|
180
|
+
console.error(' --embed Strip page chrome (red bar, title, theme toggle) for embedding');
|
|
181
|
+
console.error(' --fragment Emit an HTML fragment (no DOCTYPE/html/head/body); implies --embed');
|
|
171
182
|
console.error(' --help, -h Show this help message');
|
|
172
183
|
process.exit(1);
|
|
173
184
|
}
|
|
@@ -182,11 +193,11 @@ async function main() {
|
|
|
182
193
|
// JSON input - lint then generate
|
|
183
194
|
const spec = JSON.parse(trimmed);
|
|
184
195
|
lintSpec(spec, lintMode);
|
|
185
|
-
html = await generateChartAsync(spec);
|
|
196
|
+
html = await generateChartAsync(spec, { fragment: fragmentFlag });
|
|
186
197
|
}
|
|
187
198
|
else if (trimmed.startsWith('---') || trimmed.includes('```')) {
|
|
188
199
|
// Markdown input - parser handles linting internally (async for mermaid)
|
|
189
|
-
const result = await parseMarkdownToDashboardAsync(input, 'light', baseDir, false, false, customTheme, lintMode);
|
|
200
|
+
const result = await parseMarkdownToDashboardAsync(input, 'light', baseDir, false, false, customTheme, lintMode, embedFlag, fragmentFlag);
|
|
190
201
|
html = result.html;
|
|
191
202
|
errors = result.errors;
|
|
192
203
|
}
|
|
@@ -194,7 +205,7 @@ async function main() {
|
|
|
194
205
|
// Try JSON anyway - lint then generate
|
|
195
206
|
const spec = JSON.parse(trimmed);
|
|
196
207
|
lintSpec(spec, lintMode);
|
|
197
|
-
html = await generateChartAsync(spec);
|
|
208
|
+
html = await generateChartAsync(spec, { fragment: fragmentFlag });
|
|
198
209
|
}
|
|
199
210
|
// Check for errors (unless --allow-errors is set)
|
|
200
211
|
if (errors.length > 0 && !allowErrors) {
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Big value display component
|
|
3
|
+
*
|
|
4
|
+
* Provides two rendering paths that share the same business rules:
|
|
5
|
+
* - generateBigValue(spec) – standalone HTML document
|
|
6
|
+
* - renderBigValue(spec, ctx) – dashboard grid-item fragment
|
|
7
|
+
*
|
|
8
|
+
* Validation (value must be numeric) and format inference live in the shared
|
|
9
|
+
* resolveBigValueState() helper so the two paths cannot drift.
|
|
3
10
|
*/
|
|
4
11
|
import type { ComponentSpec } from '../types.js';
|
|
5
12
|
/**
|
|
6
|
-
*
|
|
13
|
+
* Optional dashboard rendering context.
|
|
14
|
+
*/
|
|
15
|
+
export interface BigValueRenderContext {
|
|
16
|
+
colSpan?: number | undefined;
|
|
17
|
+
anchorId?: string | undefined;
|
|
18
|
+
/** Currency code from frontmatter; falls back to spec.currency */
|
|
19
|
+
currencyCode?: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Render a big_value as a dashboard grid-item fragment.
|
|
23
|
+
*
|
|
24
|
+
* Throws ValidationError on non-numeric `value` (matches generateBigValue).
|
|
25
|
+
*/
|
|
26
|
+
export declare function renderBigValue(spec: Record<string, unknown>, ctx?: BigValueRenderContext): string;
|
|
27
|
+
/**
|
|
28
|
+
* Generate a standalone big_value HTML document.
|
|
7
29
|
*/
|
|
8
30
|
declare function generateBigValue(spec: ComponentSpec): string;
|
|
9
31
|
export { generateBigValue };
|
|
@@ -1,54 +1,113 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Big value display component
|
|
3
|
+
*
|
|
4
|
+
* Provides two rendering paths that share the same business rules:
|
|
5
|
+
* - generateBigValue(spec) – standalone HTML document
|
|
6
|
+
* - renderBigValue(spec, ctx) – dashboard grid-item fragment
|
|
7
|
+
*
|
|
8
|
+
* Validation (value must be numeric) and format inference live in the shared
|
|
9
|
+
* resolveBigValueState() helper so the two paths cannot drift.
|
|
3
10
|
*/
|
|
4
11
|
import { COLORS, FONT_STACK, getThemeColors } from '../core/themes.js';
|
|
5
12
|
import { formatNumber, inferFormat, resolveCurrency } from '../core/formatting.js';
|
|
6
13
|
import { registerComponent } from './registry.js';
|
|
7
14
|
import { ValidationError } from '../core/exceptions.js';
|
|
8
15
|
/**
|
|
9
|
-
*
|
|
16
|
+
* Resolve the display state for a big_value spec.
|
|
17
|
+
*
|
|
18
|
+
* Throws ValidationError if `value` is not a number — this is intentional and
|
|
19
|
+
* matches the strict standalone behavior (the linter's `big-value-string` rule
|
|
20
|
+
* catches string values for spec authors; this throw is defensive for
|
|
21
|
+
* programmatic callers).
|
|
10
22
|
*/
|
|
11
|
-
function
|
|
23
|
+
function resolveBigValueState(spec, currencyCode) {
|
|
12
24
|
if (typeof spec.value !== 'number') {
|
|
13
25
|
throw new ValidationError('value', 'big_value', 'number', spec.value);
|
|
14
26
|
}
|
|
15
27
|
const value = spec.value;
|
|
16
28
|
const specLabel = spec.label;
|
|
17
29
|
const specTitle = spec.title;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// If only title is provided (no label), use title as the label below the number
|
|
21
|
-
// This matches user expectations that "title" describes what the number represents
|
|
30
|
+
// If only title is provided (no label), use title as the label below the number.
|
|
31
|
+
// If both provided, title becomes header and label goes below.
|
|
22
32
|
const label = specLabel ?? specTitle ?? '';
|
|
23
|
-
|
|
24
|
-
const showTitleHeader = specTitle && specLabel;
|
|
33
|
+
const showTitleHeader = Boolean(specTitle && specLabel);
|
|
25
34
|
const title = showTitleHeader ? specTitle : '';
|
|
26
|
-
// Auto-infer format from label if not specified
|
|
27
35
|
const fmt = spec.format ?? inferFormat(label, value);
|
|
28
|
-
const currency = resolveCurrency(spec.currency);
|
|
29
|
-
const colors = getThemeColors(theme);
|
|
36
|
+
const currency = resolveCurrency(currencyCode ?? spec.currency);
|
|
30
37
|
const displayValue = formatNumber(value, fmt, false, currency);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
38
|
+
const comparisonSpec = spec.comparison;
|
|
39
|
+
let comparison;
|
|
40
|
+
if (comparisonSpec) {
|
|
41
|
+
const compVal = comparisonSpec.value ?? 0;
|
|
35
42
|
const isPositive = compVal >= 0;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
comparison = {
|
|
44
|
+
arrow: isPositive ? '↑' : '↓',
|
|
45
|
+
color: isPositive ? COLORS.POSITIVE_GREEN : COLORS.ERROR_RED,
|
|
46
|
+
display: formatNumber(compVal, comparisonSpec.format, true, currency),
|
|
47
|
+
label: comparisonSpec.label ?? '',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return { value, label, title, showTitleHeader, displayValue, comparison };
|
|
51
|
+
}
|
|
52
|
+
function escapeHtml(text) {
|
|
53
|
+
return text
|
|
54
|
+
.replace(/&/g, '&')
|
|
55
|
+
.replace(/</g, '<')
|
|
56
|
+
.replace(/>/g, '>')
|
|
57
|
+
.replace(/"/g, '"')
|
|
58
|
+
.replace(/'/g, ''');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Render a big_value as a dashboard grid-item fragment.
|
|
62
|
+
*
|
|
63
|
+
* Throws ValidationError on non-numeric `value` (matches generateBigValue).
|
|
64
|
+
*/
|
|
65
|
+
export function renderBigValue(spec, ctx = {}) {
|
|
66
|
+
const state = resolveBigValueState(spec, ctx.currencyCode);
|
|
67
|
+
const colSpan = ctx.colSpan ?? 4;
|
|
68
|
+
const anchorAttr = ctx.anchorId ? ` id="${ctx.anchorId}"` : '';
|
|
69
|
+
let comparisonHtml = '';
|
|
70
|
+
if (state.comparison) {
|
|
71
|
+
const c = state.comparison;
|
|
72
|
+
comparisonHtml = `
|
|
73
|
+
<div style="display: flex; align-items: center; gap: 4px; margin-top: 4px;">
|
|
74
|
+
<span style="color: ${c.color}; font-size: 12px;">${c.arrow} ${c.display}</span>
|
|
75
|
+
<span style="color: var(--text-muted); font-size: 10px;">${escapeHtml(c.label)}</span>
|
|
76
|
+
</div>`;
|
|
77
|
+
}
|
|
78
|
+
const titleHtml = state.showTitleHeader
|
|
79
|
+
? `<h3 class="chart-title">${escapeHtml(state.title.toUpperCase())}</h3>`
|
|
80
|
+
: '';
|
|
81
|
+
return `
|
|
82
|
+
<div class="grid-item"${anchorAttr} style="--col-span: ${colSpan}; grid-column: span ${colSpan};">
|
|
83
|
+
${titleHtml}<div class="big-value">${escapeHtml(state.displayValue)}</div>
|
|
84
|
+
<div class="label">${escapeHtml(state.label)}</div>${comparisonHtml}
|
|
85
|
+
</div>`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Generate a standalone big_value HTML document.
|
|
89
|
+
*/
|
|
90
|
+
function generateBigValue(spec) {
|
|
91
|
+
const theme = (spec.theme ?? 'light');
|
|
92
|
+
const state = resolveBigValueState(spec);
|
|
93
|
+
const colors = getThemeColors(theme);
|
|
94
|
+
let comparisonHtml = '';
|
|
95
|
+
if (state.comparison) {
|
|
96
|
+
const c = state.comparison;
|
|
39
97
|
comparisonHtml = `
|
|
40
98
|
<div style="display: flex; align-items: center; gap: 4px; margin-top: 8px;">
|
|
41
|
-
<span style="color: ${
|
|
42
|
-
<span style="color: ${colors.textSecondary}; font-size: 12px;">${
|
|
99
|
+
<span style="color: ${c.color}; font-size: 14px;">${c.arrow} ${c.display}</span>
|
|
100
|
+
<span style="color: ${colors.textSecondary}; font-size: 12px;">${c.label}</span>
|
|
43
101
|
</div>`;
|
|
44
102
|
}
|
|
45
|
-
const
|
|
46
|
-
|
|
103
|
+
const titleHtml = state.showTitleHeader
|
|
104
|
+
? `<h2>${state.title.toUpperCase()}</h2>`
|
|
105
|
+
: '';
|
|
47
106
|
return `<!DOCTYPE html>
|
|
48
107
|
<html lang="en">
|
|
49
108
|
<head>
|
|
50
109
|
<meta charset="utf-8">
|
|
51
|
-
<title>${title}</title>
|
|
110
|
+
<title>${state.title}</title>
|
|
52
111
|
<style>
|
|
53
112
|
* { box-sizing: border-box; }
|
|
54
113
|
html, body {
|
|
@@ -70,8 +129,8 @@ function generateBigValue(spec) {
|
|
|
70
129
|
<body>
|
|
71
130
|
<div class="container">
|
|
72
131
|
${titleHtml}
|
|
73
|
-
<div class="big-value">${displayValue}</div>
|
|
74
|
-
<div class="label">${label}</div>
|
|
132
|
+
<div class="big-value">${state.displayValue}</div>
|
|
133
|
+
<div class="label">${state.label}</div>
|
|
75
134
|
${comparisonHtml}
|
|
76
135
|
</div>
|
|
77
136
|
</body>
|
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Delta/change indicator component
|
|
3
|
+
*
|
|
4
|
+
* Provides two rendering paths that share the same business rules:
|
|
5
|
+
* - generateDelta(spec) – standalone HTML document
|
|
6
|
+
* - renderDelta(spec, ctx) – dashboard grid-item fragment
|
|
7
|
+
*
|
|
8
|
+
* Direction/color resolution (including the neutral case) lives in the shared
|
|
9
|
+
* resolveDeltaState() helper so the two paths cannot drift.
|
|
3
10
|
*/
|
|
4
11
|
import type { ComponentSpec } from '../types.js';
|
|
5
12
|
/**
|
|
6
|
-
*
|
|
13
|
+
* Optional dashboard rendering context.
|
|
14
|
+
*/
|
|
15
|
+
export interface DeltaRenderContext {
|
|
16
|
+
colSpan?: number | undefined;
|
|
17
|
+
anchorId?: string | undefined;
|
|
18
|
+
/** Currency code from frontmatter; falls back to spec.currency */
|
|
19
|
+
currencyCode?: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Render a delta as a dashboard grid-item fragment.
|
|
23
|
+
*
|
|
24
|
+
* Uses the dashboard's CSS `--text-muted` token for the neutral color so the
|
|
25
|
+
* value blends with theme-aware muted text.
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderDelta(spec: Record<string, unknown>, ctx?: DeltaRenderContext): string;
|
|
28
|
+
/**
|
|
29
|
+
* Generate a standalone delta HTML document.
|
|
7
30
|
*/
|
|
8
31
|
declare function generateDelta(spec: ComponentSpec): string;
|
|
9
32
|
export { generateDelta };
|