mviz 1.5.4 → 1.6.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/dist/charts/area.js +3 -2
- package/dist/charts/bar.js +4 -3
- package/dist/charts/bubble.d.ts +2 -2
- package/dist/charts/bubble.js +172 -130
- package/dist/charts/combo.js +4 -3
- package/dist/charts/dumbbell.js +51 -34
- package/dist/charts/funnel.js +34 -15
- package/dist/charts/heatmap.js +25 -14
- package/dist/charts/line.js +3 -2
- package/dist/charts/scatter.js +50 -46
- package/dist/charts/waterfall.js +19 -13
- package/dist/components/alert.js +1 -1
- package/dist/components/big_value.js +4 -3
- package/dist/components/delta.js +3 -2
- package/dist/components/table.d.ts +2 -2
- package/dist/components/table.js +9 -5
- package/dist/core/chart-helpers.d.ts +21 -0
- package/dist/core/chart-helpers.js +46 -0
- package/dist/core/formatting.d.ts +27 -6
- package/dist/core/formatting.js +131 -72
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/lint-rules/rules/big-value-string.js +1 -1
- package/dist/core/theme-loader.js +1 -0
- package/dist/core/themes.d.ts +3 -1
- package/dist/core/themes.js +16 -5
- package/dist/layout/parser.js +27 -16
- package/dist/layout/templates.js +4 -4
- package/dist/types.d.ts +13 -1
- package/package.json +1 -1
- package/schema/mviz.v1.schema.json +75 -7
package/dist/charts/funnel.js
CHANGED
|
@@ -3,49 +3,67 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { FONT_SIZE_XXS, getThemeColors, getPaletteWithCustom } from '../core/themes.js';
|
|
5
5
|
import { wrapHtml } from '../core/serializer.js';
|
|
6
|
-
import { inferFormat } from '../core/formatting.js';
|
|
6
|
+
import { inferFormat, resolveCurrency } from '../core/formatting.js';
|
|
7
7
|
import { registerChart, registerOptions } from './registry.js';
|
|
8
8
|
/**
|
|
9
9
|
* Get JavaScript formatter for funnel chart labels/tooltips
|
|
10
|
+
* @param currencySymbol - Symbol to use for currency formats
|
|
11
|
+
* @param locale - Locale for number formatting (defaults to 'en-US')
|
|
10
12
|
*/
|
|
11
|
-
function getFunnelFormatterJs(fmt) {
|
|
13
|
+
function getFunnelFormatterJs(fmt, currencySymbol = '$', locale = 'en-US') {
|
|
12
14
|
// Smart auto-formatting based on magnitude
|
|
13
|
-
if (fmt === 'auto' || fmt === '
|
|
14
|
-
const isCurrency = fmt === '
|
|
15
|
+
if (fmt === 'auto' || fmt === 'currency_auto' || fmt === null || fmt === undefined) {
|
|
16
|
+
const isCurrency = fmt === 'currency_auto' ? 'true' : 'false';
|
|
17
|
+
const symbolStr = JSON.stringify(currencySymbol);
|
|
18
|
+
const locStr = JSON.stringify(locale);
|
|
19
|
+
const lfHelper = fmt === 'currency_auto'
|
|
20
|
+
? `var loc = ${locStr}; var lf = function(v, d) { return v.toLocaleString(loc, {minimumFractionDigits:d, maximumFractionDigits:d}); };`
|
|
21
|
+
: `var lf = function(v, d) { return v.toFixed(d); };`;
|
|
15
22
|
return {
|
|
16
23
|
_js_: `function(params) {
|
|
17
24
|
var v = params.value;
|
|
18
25
|
if (v == null) return params.name;
|
|
19
26
|
var abs = Math.abs(v);
|
|
20
27
|
var neg = v < 0;
|
|
21
|
-
var p = ${isCurrency} ?
|
|
28
|
+
var p = ${isCurrency} ? ${symbolStr} : '';
|
|
29
|
+
${lfHelper}
|
|
22
30
|
var r;
|
|
23
31
|
if (abs >= 1e9) {
|
|
24
32
|
var s = abs / 1e9;
|
|
25
|
-
r = s >= 100 ? p + s
|
|
33
|
+
r = s >= 100 ? p + lf(s,1) + 'b' : s >= 10 ? p + lf(s,2) + 'b' : p + lf(s,3) + 'b';
|
|
26
34
|
} else if (abs >= 1e6) {
|
|
27
35
|
var s = abs / 1e6;
|
|
28
|
-
r = s >= 100 ? p + s
|
|
36
|
+
r = s >= 100 ? p + lf(s,1) + 'm' : s >= 10 ? p + lf(s,2) + 'm' : p + lf(s,3) + 'm';
|
|
29
37
|
} else if (abs >= 1e4) {
|
|
30
38
|
var s = abs / 1e3;
|
|
31
|
-
r = s >= 100 ? p + s
|
|
39
|
+
r = s >= 100 ? p + lf(s,1) + 'k' : p + lf(s,2) + 'k';
|
|
32
40
|
} else if (abs >= 1e3) {
|
|
33
|
-
r = p + abs.toLocaleString(
|
|
41
|
+
r = p + abs.toLocaleString(${locStr}, {maximumFractionDigits: 0});
|
|
34
42
|
} else {
|
|
35
|
-
r = abs === Math.floor(abs) ? p + abs : p + abs
|
|
43
|
+
r = abs === Math.floor(abs) ? p + abs : p + lf(abs,2);
|
|
36
44
|
}
|
|
37
45
|
var formatted = neg ? '(' + r + ')' : r;
|
|
38
46
|
return params.name + ': ' + formatted;
|
|
39
47
|
}`,
|
|
40
48
|
};
|
|
41
49
|
}
|
|
50
|
+
const sym = JSON.stringify(currencySymbol);
|
|
51
|
+
const locStr = JSON.stringify(locale);
|
|
42
52
|
// Specific format handlers
|
|
43
53
|
switch (fmt) {
|
|
44
|
-
case '
|
|
45
|
-
return { _js_:
|
|
46
|
-
case '
|
|
54
|
+
case 'currency':
|
|
55
|
+
return { _js_: `function(params) { return params.name + ': ' + ${sym} + params.value.toLocaleString(${locStr}); }` };
|
|
56
|
+
case 'currency0k':
|
|
47
57
|
return {
|
|
48
|
-
_js_:
|
|
58
|
+
_js_: `function(params) { return params.name + ': ' + ${sym} + (params.value/1000).toFixed(0) + 'k'; }`,
|
|
59
|
+
};
|
|
60
|
+
case 'currency0m':
|
|
61
|
+
return {
|
|
62
|
+
_js_: `function(params) { return params.name + ': ' + ${sym} + (params.value/1000000).toLocaleString(${locStr}, {minimumFractionDigits:1, maximumFractionDigits:1}) + 'm'; }`,
|
|
63
|
+
};
|
|
64
|
+
case 'currency0b':
|
|
65
|
+
return {
|
|
66
|
+
_js_: `function(params) { return params.name + ': ' + ${sym} + (params.value/1000000000).toLocaleString(${locStr}, {minimumFractionDigits:1, maximumFractionDigits:1}) + 'b'; }`,
|
|
49
67
|
};
|
|
50
68
|
case 'pct':
|
|
51
69
|
case 'pct1':
|
|
@@ -88,8 +106,9 @@ export function buildFunnelOptions(spec) {
|
|
|
88
106
|
// Infer format from value field name or use explicit format
|
|
89
107
|
const sampleValue = funnelData[0]?.value ?? 0;
|
|
90
108
|
const valueFormat = spec.format ?? inferFormat(valueField, sampleValue);
|
|
109
|
+
const currency = resolveCurrency(spec.currency);
|
|
91
110
|
// Get JavaScript formatter
|
|
92
|
-
const formatter = getFunnelFormatterJs(valueFormat);
|
|
111
|
+
const formatter = getFunnelFormatterJs(valueFormat, currency.symbol, currency.locale);
|
|
93
112
|
return {
|
|
94
113
|
backgroundColor: 'transparent',
|
|
95
114
|
animation: false,
|
package/dist/charts/heatmap.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { FONT_SIZE_XXS, getThemeColors, getPaletteWithCustom } from '../core/themes.js';
|
|
5
5
|
import { wrapHtml } from '../core/serializer.js';
|
|
6
6
|
import { registerChart, registerOptions } from './registry.js';
|
|
7
|
-
import { inferFormat } from '../core/formatting.js';
|
|
7
|
+
import { inferFormat, resolveCurrency } from '../core/formatting.js';
|
|
8
8
|
/**
|
|
9
9
|
* Parse a formatted string value like "1.9k" or "$2.5m" to a number.
|
|
10
10
|
* Returns the original value if parsing fails.
|
|
@@ -35,45 +35,55 @@ function parseFormattedValue(value) {
|
|
|
35
35
|
/**
|
|
36
36
|
* Generate a label formatter for heatmap cells.
|
|
37
37
|
* Heatmap data is [x, y, value], so we access params.value[2].
|
|
38
|
+
* @param currencySymbol - Symbol to use for currency formats
|
|
39
|
+
* @param locale - Locale for number formatting (defaults to 'en-US')
|
|
38
40
|
*/
|
|
39
|
-
function getHeatmapLabelFormatter(fmt) {
|
|
41
|
+
function getHeatmapLabelFormatter(fmt, currencySymbol = '$', locale = 'en-US') {
|
|
40
42
|
if (fmt === null || fmt === undefined) {
|
|
41
43
|
return null;
|
|
42
44
|
}
|
|
43
45
|
// Smart auto-formatting for heatmap labels
|
|
44
|
-
if (fmt === 'auto' || fmt === '
|
|
45
|
-
const isCurrency = fmt === '
|
|
46
|
+
if (fmt === 'auto' || fmt === 'currency_auto') {
|
|
47
|
+
const isCurrency = fmt === 'currency_auto' ? 'true' : 'false';
|
|
48
|
+
const symbolStr = JSON.stringify(currencySymbol);
|
|
49
|
+
const locStr = JSON.stringify(locale);
|
|
50
|
+
const lfHelper = fmt === 'currency_auto'
|
|
51
|
+
? `var loc = ${locStr}; var lf = function(v, d) { return v.toLocaleString(loc, {minimumFractionDigits:d, maximumFractionDigits:d}); };`
|
|
52
|
+
: `var lf = function(v, d) { return v.toFixed(d); };`;
|
|
46
53
|
return {
|
|
47
54
|
_js_: `function(params) {
|
|
48
55
|
var v = params.value[2];
|
|
49
56
|
if (v == null) return '';
|
|
50
57
|
var abs = Math.abs(v);
|
|
51
58
|
var neg = v < 0;
|
|
52
|
-
var p = ${isCurrency} ?
|
|
59
|
+
var p = ${isCurrency} ? ${symbolStr} : '';
|
|
60
|
+
${lfHelper}
|
|
53
61
|
var r;
|
|
54
62
|
if (abs >= 1e9) {
|
|
55
63
|
var s = abs / 1e9;
|
|
56
|
-
r = s >= 100 ? p + s
|
|
64
|
+
r = s >= 100 ? p + lf(s,1) + 'b' : s >= 10 ? p + lf(s,2) + 'b' : p + lf(s,3) + 'b';
|
|
57
65
|
} else if (abs >= 1e6) {
|
|
58
66
|
var s = abs / 1e6;
|
|
59
|
-
r = s >= 100 ? p + s
|
|
67
|
+
r = s >= 100 ? p + lf(s,1) + 'm' : s >= 10 ? p + lf(s,2) + 'm' : p + lf(s,3) + 'm';
|
|
60
68
|
} else if (abs >= 1e4) {
|
|
61
69
|
var s = abs / 1e3;
|
|
62
|
-
r = s >= 100 ? p + s
|
|
70
|
+
r = s >= 100 ? p + lf(s,1) + 'k' : p + lf(s,2) + 'k';
|
|
63
71
|
} else if (abs >= 1e3) {
|
|
64
|
-
r = p + abs.toLocaleString(
|
|
72
|
+
r = p + abs.toLocaleString(${locStr}, {maximumFractionDigits: 0});
|
|
65
73
|
} else {
|
|
66
|
-
r = abs === Math.floor(abs) ? p + abs : p + abs
|
|
74
|
+
r = abs === Math.floor(abs) ? p + abs : p + lf(abs,2);
|
|
67
75
|
}
|
|
68
76
|
return neg ? '(' + r + ')' : r;
|
|
69
77
|
}`,
|
|
70
78
|
};
|
|
71
79
|
}
|
|
72
80
|
// Format-specific formatters (with null checks)
|
|
81
|
+
const sym = JSON.stringify(currencySymbol);
|
|
82
|
+
const locStr = JSON.stringify(locale);
|
|
73
83
|
const formatExprs = {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
currency: `${sym} + v.toLocaleString(${locStr})`,
|
|
85
|
+
currency0k: `${sym} + (v/1000).toFixed(0) + 'k'`,
|
|
86
|
+
currency0m: `${sym} + (v/1000000).toLocaleString(${locStr}, {minimumFractionDigits:1, maximumFractionDigits:1}) + 'm'`,
|
|
77
87
|
pct: "(v * 100).toFixed(1) + '%'",
|
|
78
88
|
pct0: "(v * 100).toFixed(0) + '%'",
|
|
79
89
|
pct1: "(v * 100).toFixed(1) + '%'",
|
|
@@ -149,7 +159,8 @@ export function buildHeatmapOptions(spec) {
|
|
|
149
159
|
// Infer format for labels
|
|
150
160
|
const sampleValue = values[0] ?? 0;
|
|
151
161
|
const valueFormat = spec.format ?? inferFormat('value', sampleValue);
|
|
152
|
-
const
|
|
162
|
+
const currency = resolveCurrency(spec.currency);
|
|
163
|
+
const labelFormatter = getHeatmapLabelFormatter(valueFormat, currency.symbol, currency.locale);
|
|
153
164
|
// Build label config
|
|
154
165
|
const labelConfig = { show: true, color: colors.text };
|
|
155
166
|
if (labelFormatter) {
|
package/dist/charts/line.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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 { inferFormat, getAxisFormatterJs, inferAxisType } from '../core/formatting.js';
|
|
7
|
+
import { inferFormat, getAxisFormatterJs, inferAxisType, resolveCurrency } from '../core/formatting.js';
|
|
8
8
|
/**
|
|
9
9
|
* Build ECharts options for a line chart
|
|
10
10
|
*/
|
|
@@ -22,7 +22,8 @@ export function buildLineOptions(spec) {
|
|
|
22
22
|
const firstYKey = yKeys[0] ?? 'value';
|
|
23
23
|
const sampleValue = data.length > 0 && data[0] ? data[0][firstYKey] ?? 0 : 0;
|
|
24
24
|
const valueFormat = spec.format ?? inferFormat(firstYKey, sampleValue);
|
|
25
|
-
const
|
|
25
|
+
const currency = resolveCurrency(spec.currency);
|
|
26
|
+
const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale);
|
|
26
27
|
// Detect x-axis type: time, value, or category
|
|
27
28
|
// User can override with xAxisType in spec
|
|
28
29
|
const xAxisType = spec.xAxisType ?? inferAxisType(categories);
|
package/dist/charts/scatter.js
CHANGED
|
@@ -4,7 +4,21 @@
|
|
|
4
4
|
import { SCATTER_SYMBOL_SIZE, 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 { inferFormat, getAxisFormatterJs, formatValue } from '../core/formatting.js';
|
|
7
|
+
import { inferFormat, getAxisFormatterJs, formatValue, resolveCurrency } from '../core/formatting.js';
|
|
8
|
+
import { buildPointLabelConfig, groupDataBySeries, buildSeriesLegend } from '../core/chart-helpers.js';
|
|
9
|
+
/** Scatter point opacity */
|
|
10
|
+
const SCATTER_OPACITY = 0.75;
|
|
11
|
+
/** Label sits at index 2 in scatter point arrays: [x, y, label?] */
|
|
12
|
+
const SCATTER_LABEL_INDEX = 2;
|
|
13
|
+
/**
|
|
14
|
+
* Build a scatter data point: [x, y, label?]
|
|
15
|
+
*/
|
|
16
|
+
function buildScatterPoint(d, xField, yField, labelField) {
|
|
17
|
+
const point = [d[xField] ?? 0, d[yField] ?? 0];
|
|
18
|
+
if (labelField)
|
|
19
|
+
point.push(String(d[labelField] ?? ''));
|
|
20
|
+
return point;
|
|
21
|
+
}
|
|
8
22
|
/**
|
|
9
23
|
* Build ECharts options for a scatter plot
|
|
10
24
|
*/
|
|
@@ -14,6 +28,7 @@ export function buildScatterOptions(spec) {
|
|
|
14
28
|
const yField = spec.y ?? 'y';
|
|
15
29
|
const seriesField = spec.series;
|
|
16
30
|
const labelField = spec.label;
|
|
31
|
+
const showLabels = spec.showLabels;
|
|
17
32
|
const theme = (spec.theme ?? 'light');
|
|
18
33
|
const colors = getThemeColors(theme);
|
|
19
34
|
const palette = getPaletteWithCustom(theme, spec.customTheme);
|
|
@@ -22,54 +37,48 @@ export function buildScatterOptions(spec) {
|
|
|
22
37
|
const ySample = data.length > 0 && data[0] ? data[0][yField] ?? 0 : 0;
|
|
23
38
|
const xFormat = spec.xFormat ?? inferFormat(xField, xSample);
|
|
24
39
|
const yFormat = spec.yFormat ?? inferFormat(yField, ySample);
|
|
25
|
-
const
|
|
26
|
-
const
|
|
40
|
+
const currency = resolveCurrency(spec.currency);
|
|
41
|
+
const xAxisFormatter = getAxisFormatterJs(xFormat, currency.symbol, currency.locale);
|
|
42
|
+
const yAxisFormatter = getAxisFormatterJs(yFormat, currency.symbol, currency.locale);
|
|
43
|
+
// Label config for persistent point labels
|
|
44
|
+
const labelConfig = showLabels && labelField
|
|
45
|
+
? buildPointLabelConfig(SCATTER_LABEL_INDEX, colors)
|
|
46
|
+
: undefined;
|
|
27
47
|
// Build series - either single series or grouped by seriesField
|
|
28
48
|
let seriesList;
|
|
29
49
|
let legendData = [];
|
|
50
|
+
const pointBuilder = (d) => buildScatterPoint(d, xField, yField, labelField);
|
|
30
51
|
if (seriesField) {
|
|
31
|
-
|
|
32
|
-
const groups = new Map();
|
|
33
|
-
for (const d of data) {
|
|
34
|
-
const groupName = String(d[seriesField] ?? 'Unknown');
|
|
35
|
-
if (!groups.has(groupName)) {
|
|
36
|
-
groups.set(groupName, []);
|
|
37
|
-
}
|
|
38
|
-
const point = [d[xField] ?? 0, d[yField] ?? 0];
|
|
39
|
-
if (labelField)
|
|
40
|
-
point.push(String(d[labelField] ?? ''));
|
|
41
|
-
groups.get(groupName).push(point);
|
|
42
|
-
}
|
|
43
|
-
// Create a series for each group
|
|
52
|
+
const groups = groupDataBySeries(data, seriesField, pointBuilder);
|
|
44
53
|
legendData = [...groups.keys()];
|
|
45
|
-
seriesList = legendData.map((name, idx) =>
|
|
46
|
-
|
|
47
|
-
name,
|
|
48
|
-
data: groups.get(name),
|
|
49
|
-
symbolSize: SCATTER_SYMBOL_SIZE,
|
|
50
|
-
itemStyle: { color: palette[idx % palette.length], opacity: 0.75 },
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
// Single series
|
|
55
|
-
const scatterData = data.map((d) => {
|
|
56
|
-
const point = [d[xField] ?? 0, d[yField] ?? 0];
|
|
57
|
-
if (labelField)
|
|
58
|
-
point.push(String(d[labelField] ?? ''));
|
|
59
|
-
return point;
|
|
60
|
-
});
|
|
61
|
-
seriesList = [
|
|
62
|
-
{
|
|
54
|
+
seriesList = legendData.map((name, idx) => {
|
|
55
|
+
const seriesObj = {
|
|
63
56
|
type: 'scatter',
|
|
64
|
-
|
|
57
|
+
name,
|
|
58
|
+
data: groups.get(name),
|
|
65
59
|
symbolSize: SCATTER_SYMBOL_SIZE,
|
|
66
|
-
itemStyle: { color: palette[
|
|
67
|
-
}
|
|
68
|
-
|
|
60
|
+
itemStyle: { color: palette[idx % palette.length], opacity: SCATTER_OPACITY },
|
|
61
|
+
};
|
|
62
|
+
if (labelConfig)
|
|
63
|
+
seriesObj.label = labelConfig;
|
|
64
|
+
return seriesObj;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const scatterData = data.map(pointBuilder);
|
|
69
|
+
const seriesObj = {
|
|
70
|
+
type: 'scatter',
|
|
71
|
+
data: scatterData,
|
|
72
|
+
symbolSize: SCATTER_SYMBOL_SIZE,
|
|
73
|
+
itemStyle: { color: palette[0], opacity: SCATTER_OPACITY },
|
|
74
|
+
};
|
|
75
|
+
if (labelConfig)
|
|
76
|
+
seriesObj.label = labelConfig;
|
|
77
|
+
seriesList = [seriesObj];
|
|
69
78
|
}
|
|
70
79
|
// Build tooltip formatter with label support and value formatting
|
|
71
|
-
const xFormatExpr = formatValue(xFormat);
|
|
72
|
-
const yFormatExpr = formatValue(yFormat);
|
|
80
|
+
const xFormatExpr = formatValue(xFormat, currency.symbol, currency.locale);
|
|
81
|
+
const yFormatExpr = formatValue(yFormat, currency.symbol, currency.locale);
|
|
73
82
|
const tooltipFormatterJs = `function(params) {
|
|
74
83
|
var d = params.value || params.data;
|
|
75
84
|
var result = '';
|
|
@@ -128,12 +137,7 @@ export function buildScatterOptions(spec) {
|
|
|
128
137
|
};
|
|
129
138
|
// Add legend for multi-series
|
|
130
139
|
if (seriesField && legendData.length > 1) {
|
|
131
|
-
option.legend =
|
|
132
|
-
show: true,
|
|
133
|
-
data: legendData,
|
|
134
|
-
top: 0,
|
|
135
|
-
textStyle: { color: colors.textSecondary, fontSize: FONT_SIZE_TINY },
|
|
136
|
-
};
|
|
140
|
+
option.legend = buildSeriesLegend(legendData, colors);
|
|
137
141
|
}
|
|
138
142
|
return option;
|
|
139
143
|
}
|
package/dist/charts/waterfall.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { FONT_SIZE_TINY, FONT_SIZE_XXS, BAR_MAX_WIDTH, getThemeColors, getPaletteWithCustom, getWaterfallColors, getAxisLabelFormatterJs, } from '../core/themes.js';
|
|
5
5
|
import { wrapHtml } from '../core/serializer.js';
|
|
6
|
-
import { inferFormat } from '../core/formatting.js';
|
|
6
|
+
import { inferFormat, resolveCurrency } from '../core/formatting.js';
|
|
7
7
|
import { registerChart, registerOptions } from './registry.js';
|
|
8
8
|
/**
|
|
9
9
|
* Build ECharts options for a waterfall chart showing cumulative effect
|
|
@@ -20,7 +20,8 @@ export function buildWaterfallOptions(spec) {
|
|
|
20
20
|
// Infer format for y-axis
|
|
21
21
|
const sampleValue = data[0]?.[valueField] ?? 0;
|
|
22
22
|
const valueFormat = spec.format ?? inferFormat(valueField, sampleValue);
|
|
23
|
-
const
|
|
23
|
+
const currency = resolveCurrency(spec.currency);
|
|
24
|
+
const isCurrency = valueFormat && (valueFormat.includes('currency') || valueFormat === 'currency_auto');
|
|
24
25
|
const categories = [];
|
|
25
26
|
const invisibleData = [];
|
|
26
27
|
const increaseData = [];
|
|
@@ -64,30 +65,35 @@ export function buildWaterfallOptions(spec) {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
|
-
// Label formatter JS function
|
|
68
|
+
// Label formatter JS function - use locale-aware formatting for currency
|
|
69
|
+
const locStr = JSON.stringify(currency.locale);
|
|
70
|
+
const lfHelper = isCurrency
|
|
71
|
+
? `var loc = ${locStr}; var lf = function(v, d) { return v.toLocaleString(loc, {minimumFractionDigits:d, maximumFractionDigits:d}); };`
|
|
72
|
+
: `var lf = function(v, d) { return v.toFixed(d); };`;
|
|
68
73
|
const labelFormatter = {
|
|
69
74
|
_js_: `function(params) {
|
|
70
75
|
var value = params.value;
|
|
71
76
|
if (value === '-' || value === null || value === undefined) return '';
|
|
72
77
|
var absVal = Math.abs(value);
|
|
73
78
|
var isNeg = value < 0;
|
|
79
|
+
${lfHelper}
|
|
74
80
|
var formatted;
|
|
75
81
|
if (absVal >= 1000000000) {
|
|
76
82
|
var scaled = absVal / 1000000000;
|
|
77
|
-
if (scaled >= 100) formatted = scaled
|
|
78
|
-
else if (scaled >= 10) formatted = scaled
|
|
79
|
-
else formatted = scaled
|
|
83
|
+
if (scaled >= 100) formatted = lf(scaled,1) + 'b';
|
|
84
|
+
else if (scaled >= 10) formatted = lf(scaled,2) + 'b';
|
|
85
|
+
else formatted = lf(scaled,3) + 'b';
|
|
80
86
|
} else if (absVal >= 1000000) {
|
|
81
87
|
var scaled = absVal / 1000000;
|
|
82
|
-
if (scaled >= 100) formatted = scaled
|
|
83
|
-
else if (scaled >= 10) formatted = scaled
|
|
84
|
-
else formatted = scaled
|
|
88
|
+
if (scaled >= 100) formatted = lf(scaled,1) + 'm';
|
|
89
|
+
else if (scaled >= 10) formatted = lf(scaled,2) + 'm';
|
|
90
|
+
else formatted = lf(scaled,3) + 'm';
|
|
85
91
|
} else if (absVal >= 10000) {
|
|
86
92
|
var scaled = absVal / 1000;
|
|
87
|
-
if (scaled >= 100) formatted = scaled
|
|
88
|
-
else formatted = scaled
|
|
93
|
+
if (scaled >= 100) formatted = lf(scaled,1) + 'k';
|
|
94
|
+
else formatted = lf(scaled,2) + 'k';
|
|
89
95
|
} else if (absVal >= 1000) {
|
|
90
|
-
formatted = absVal.toLocaleString();
|
|
96
|
+
formatted = absVal.toLocaleString(${locStr});
|
|
91
97
|
} else {
|
|
92
98
|
formatted = absVal.toString();
|
|
93
99
|
}
|
|
@@ -155,7 +161,7 @@ export function buildWaterfallOptions(spec) {
|
|
|
155
161
|
axisLabel: {
|
|
156
162
|
color: colors.textSecondary,
|
|
157
163
|
fontSize: FONT_SIZE_TINY,
|
|
158
|
-
formatter: getAxisLabelFormatterJs(!!isCurrency),
|
|
164
|
+
formatter: getAxisLabelFormatterJs(!!isCurrency, currency.symbol, currency.locale),
|
|
159
165
|
},
|
|
160
166
|
},
|
|
161
167
|
series: [
|
package/dist/components/alert.js
CHANGED
|
@@ -7,7 +7,7 @@ import { registerComponent } from './registry.js';
|
|
|
7
7
|
* Generate an alert/banner component
|
|
8
8
|
*/
|
|
9
9
|
function generateAlert(spec) {
|
|
10
|
-
const message = spec.message ?? '';
|
|
10
|
+
const message = spec.message ?? spec.content ?? '';
|
|
11
11
|
const alertType = (spec.alertType ?? 'info');
|
|
12
12
|
const title = spec.title ?? '';
|
|
13
13
|
const theme = (spec.theme ?? 'light');
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Big value display component
|
|
3
3
|
*/
|
|
4
4
|
import { COLORS, FONT_STACK, getThemeColors } from '../core/themes.js';
|
|
5
|
-
import { formatNumber, inferFormat } from '../core/formatting.js';
|
|
5
|
+
import { formatNumber, inferFormat, resolveCurrency } from '../core/formatting.js';
|
|
6
6
|
import { registerComponent } from './registry.js';
|
|
7
7
|
import { ValidationError } from '../core/exceptions.js';
|
|
8
8
|
/**
|
|
@@ -25,8 +25,9 @@ function generateBigValue(spec) {
|
|
|
25
25
|
const title = showTitleHeader ? specTitle : '';
|
|
26
26
|
// Auto-infer format from label if not specified
|
|
27
27
|
const fmt = spec.format ?? inferFormat(label, value);
|
|
28
|
+
const currency = resolveCurrency(spec.currency);
|
|
28
29
|
const colors = getThemeColors(theme);
|
|
29
|
-
const displayValue = formatNumber(value, fmt);
|
|
30
|
+
const displayValue = formatNumber(value, fmt, false, currency);
|
|
30
31
|
let comparisonHtml = '';
|
|
31
32
|
if (comparison) {
|
|
32
33
|
const compVal = comparison.value ?? 0;
|
|
@@ -34,7 +35,7 @@ function generateBigValue(spec) {
|
|
|
34
35
|
const isPositive = compVal >= 0;
|
|
35
36
|
const arrow = isPositive ? '↑' : '↓';
|
|
36
37
|
const compColor = isPositive ? COLORS.POSITIVE_GREEN : COLORS.ERROR_RED;
|
|
37
|
-
const compDisplay = formatNumber(compVal, comparison.format, true);
|
|
38
|
+
const compDisplay = formatNumber(compVal, comparison.format, true, currency);
|
|
38
39
|
comparisonHtml = `
|
|
39
40
|
<div style="display: flex; align-items: center; gap: 4px; margin-top: 8px;">
|
|
40
41
|
<span style="color: ${compColor}; font-size: 14px;">${arrow} ${compDisplay}</span>
|
package/dist/components/delta.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Delta/change indicator component
|
|
3
3
|
*/
|
|
4
4
|
import { COLORS, FONT_STACK, getThemeColors } from '../core/themes.js';
|
|
5
|
-
import { formatNumber, inferFormat } from '../core/formatting.js';
|
|
5
|
+
import { formatNumber, inferFormat, resolveCurrency } from '../core/formatting.js';
|
|
6
6
|
import { registerComponent } from './registry.js';
|
|
7
7
|
/**
|
|
8
8
|
* Generate a delta/change indicator
|
|
@@ -21,6 +21,7 @@ function generateDelta(spec) {
|
|
|
21
21
|
const title = showTitleHeader ? specTitle : '';
|
|
22
22
|
// Auto-infer format from label if not specified
|
|
23
23
|
const fmt = spec.format ?? inferFormat(label, value);
|
|
24
|
+
const currency = resolveCurrency(spec.currency);
|
|
24
25
|
const colors = getThemeColors(theme);
|
|
25
26
|
// Determine direction and color
|
|
26
27
|
const isPositive = value > neutralIs;
|
|
@@ -40,7 +41,7 @@ function generateDelta(spec) {
|
|
|
40
41
|
color = COLORS.ERROR_RED;
|
|
41
42
|
arrow = isPositive ? '↑' : '↓';
|
|
42
43
|
}
|
|
43
|
-
const displayValue = formatNumber(value, fmt, true);
|
|
44
|
+
const displayValue = formatNumber(value, fmt, true, currency);
|
|
44
45
|
const titleUpper = title.toUpperCase();
|
|
45
46
|
const titleHtml = title ? `<h2>${titleUpper}</h2>` : '';
|
|
46
47
|
return `<!DOCTYPE html>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ComponentSpec } from '../types.js';
|
|
5
5
|
import { getThemeColors } from '../core/themes.js';
|
|
6
|
-
import { type FormatType } from '../core/formatting.js';
|
|
6
|
+
import { type FormatType, type CurrencyConfig } from '../core/formatting.js';
|
|
7
7
|
/**
|
|
8
8
|
* Column definition for table configuration
|
|
9
9
|
*/
|
|
@@ -81,7 +81,7 @@ export declare function formatCell(value: unknown, colDef: ColumnDef, themeColor
|
|
|
81
81
|
}>, dumbbellRanges: Map<string, {
|
|
82
82
|
min: number;
|
|
83
83
|
max: number;
|
|
84
|
-
}>, defaultHeatmapColors: string[], customPalette?: readonly string[]): CellResult;
|
|
84
|
+
}>, defaultHeatmapColors: string[], customPalette?: readonly string[], currency?: CurrencyConfig): CellResult;
|
|
85
85
|
/**
|
|
86
86
|
* Generate a styled data table
|
|
87
87
|
*/
|
package/dist/components/table.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Data table component
|
|
3
3
|
*/
|
|
4
4
|
import { COLORS, PALETTE, FONT_STACK, SPARKLINE_WIDTH, SPARKLINE_HEIGHT, DUMBBELL_DOT_RADIUS, getThemeColors, getHeatmapColors, getSparklineColors, } from '../core/themes.js';
|
|
5
|
-
import { formatNumber, inferFormat } from '../core/formatting.js';
|
|
5
|
+
import { formatNumber, inferFormat, resolveCurrency } from '../core/formatting.js';
|
|
6
6
|
import { calculateHeatmapColorWithContrast } from '../core/colors.js';
|
|
7
7
|
import { registerComponent } from './registry.js';
|
|
8
8
|
/**
|
|
@@ -334,7 +334,7 @@ function formatSparklineCell(rawValue, colDef, colId, themeColors, dumbbellRange
|
|
|
334
334
|
/**
|
|
335
335
|
* Format a cell value and return content with optional styling
|
|
336
336
|
*/
|
|
337
|
-
export function formatCell(value, colDef, themeColors, heatmapRanges, dumbbellRanges, defaultHeatmapColors, customPalette) {
|
|
337
|
+
export function formatCell(value, colDef, themeColors, heatmapRanges, dumbbellRanges, defaultHeatmapColors, customPalette, currency) {
|
|
338
338
|
const colType = colDef.type;
|
|
339
339
|
const colId = colDef.id;
|
|
340
340
|
const palette = customPalette ?? PALETTE;
|
|
@@ -350,7 +350,10 @@ export function formatCell(value, colDef, themeColors, heatmapRanges, dumbbellRa
|
|
|
350
350
|
if (colType === 'heatmap') {
|
|
351
351
|
const range = heatmapRanges.get(colId);
|
|
352
352
|
if (typeof rawValue === 'number' && range) {
|
|
353
|
-
const
|
|
353
|
+
const baseColors = colDef.heatmapColors ?? defaultHeatmapColors;
|
|
354
|
+
const heatmapColors = colDef.higherIsBetter === false
|
|
355
|
+
? [...baseColors].reverse()
|
|
356
|
+
: baseColors;
|
|
354
357
|
const colorResult = calculateHeatmapColorWithContrast(rawValue, range.min, range.max, heatmapColors);
|
|
355
358
|
bgColor = colorResult.bgColor;
|
|
356
359
|
textColor = colorResult.textColor;
|
|
@@ -367,7 +370,7 @@ export function formatCell(value, colDef, themeColors, heatmapRanges, dumbbellRa
|
|
|
367
370
|
formatted = rawValue;
|
|
368
371
|
}
|
|
369
372
|
else {
|
|
370
|
-
formatted = formatNumber(rawValue, fmt);
|
|
373
|
+
formatted = formatNumber(rawValue, fmt, false, currency);
|
|
371
374
|
}
|
|
372
375
|
// Apply bold/italic styling
|
|
373
376
|
if (styles.bold) {
|
|
@@ -404,6 +407,7 @@ function parseTableConfig(spec) {
|
|
|
404
407
|
heatmapRanges: computeHeatmapRanges(columns, data),
|
|
405
408
|
dumbbellRanges: computeDumbbellRanges(columns, data),
|
|
406
409
|
defaultHeatmapColors: getHeatmapColors(theme),
|
|
410
|
+
currency: resolveCurrency(spec.currency),
|
|
407
411
|
};
|
|
408
412
|
}
|
|
409
413
|
/**
|
|
@@ -460,7 +464,7 @@ function buildDataRows(cfg, inlineStyles = false) {
|
|
|
460
464
|
for (const col of cfg.columns) {
|
|
461
465
|
const value = rowData[col.id];
|
|
462
466
|
const align = col.align ?? 'left';
|
|
463
|
-
const cellResult = formatCell(value, col, cfg.colors, cfg.heatmapRanges, cfg.dumbbellRanges, cfg.defaultHeatmapColors);
|
|
467
|
+
const cellResult = formatCell(value, col, cfg.colors, cfg.heatmapRanges, cfg.dumbbellRanges, cfg.defaultHeatmapColors, undefined, cfg.currency);
|
|
464
468
|
if (inlineStyles) {
|
|
465
469
|
const border = isLast ? `border-bottom: 1px solid ${cfg.colors.text};` : '';
|
|
466
470
|
const cellBg = cellResult.bgColor ?? rowBg;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for scatter-family charts (scatter, bubble).
|
|
3
|
+
*
|
|
4
|
+
* Consolidates series-grouping, label config, and legend patterns
|
|
5
|
+
* that were duplicated across scatter.ts and bubble.ts.
|
|
6
|
+
*/
|
|
7
|
+
import type { DataPoint, ThemeColors } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Build ECharts label config for persistent point labels.
|
|
10
|
+
* @param labelIndex - Index of the label value in the point array
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildPointLabelConfig(labelIndex: number, colors: ThemeColors): Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Group data points by a series field, applying a point builder to each.
|
|
15
|
+
*/
|
|
16
|
+
export declare function groupDataBySeries<T>(data: DataPoint[], seriesField: string, buildPoint: (d: DataPoint) => T): Map<string, T[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Build ECharts legend config for multi-series charts.
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildSeriesLegend(legendData: string[], colors: ThemeColors): Record<string, unknown>;
|
|
21
|
+
//# sourceMappingURL=chart-helpers.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for scatter-family charts (scatter, bubble).
|
|
3
|
+
*
|
|
4
|
+
* Consolidates series-grouping, label config, and legend patterns
|
|
5
|
+
* that were duplicated across scatter.ts and bubble.ts.
|
|
6
|
+
*/
|
|
7
|
+
import { FONT_SIZE_TINY } from './themes.js';
|
|
8
|
+
/**
|
|
9
|
+
* Build ECharts label config for persistent point labels.
|
|
10
|
+
* @param labelIndex - Index of the label value in the point array
|
|
11
|
+
*/
|
|
12
|
+
export function buildPointLabelConfig(labelIndex, colors) {
|
|
13
|
+
return {
|
|
14
|
+
show: true,
|
|
15
|
+
formatter: { _js_: `function(params) { return params.value[${labelIndex}] || ""; }` },
|
|
16
|
+
position: 'right',
|
|
17
|
+
fontSize: FONT_SIZE_TINY,
|
|
18
|
+
color: colors.textSecondary,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Group data points by a series field, applying a point builder to each.
|
|
23
|
+
*/
|
|
24
|
+
export function groupDataBySeries(data, seriesField, buildPoint) {
|
|
25
|
+
const groups = new Map();
|
|
26
|
+
for (const d of data) {
|
|
27
|
+
const groupName = String(d[seriesField] ?? 'Unknown');
|
|
28
|
+
if (!groups.has(groupName)) {
|
|
29
|
+
groups.set(groupName, []);
|
|
30
|
+
}
|
|
31
|
+
groups.get(groupName).push(buildPoint(d));
|
|
32
|
+
}
|
|
33
|
+
return groups;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build ECharts legend config for multi-series charts.
|
|
37
|
+
*/
|
|
38
|
+
export function buildSeriesLegend(legendData, colors) {
|
|
39
|
+
return {
|
|
40
|
+
show: true,
|
|
41
|
+
data: legendData,
|
|
42
|
+
top: 0,
|
|
43
|
+
textStyle: { color: colors.textSecondary, fontSize: FONT_SIZE_TINY },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=chart-helpers.js.map
|