mviz 1.5.4 → 1.6.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 +3 -2
- package/dist/charts/bar.js +4 -3
- package/dist/charts/bubble.js +2 -2
- 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 +6 -5
- 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 +5 -4
- package/dist/core/formatting.d.ts +27 -6
- package/dist/core/formatting.js +131 -72
- 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 +63 -7
package/dist/charts/area.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { FONT_SIZE_XXS, LEGEND_ITEM_WIDTH, LEGEND_ITEM_HEIGHT, LEGEND_ITEM_GAP, 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 an area chart
|
|
10
10
|
*/
|
|
@@ -23,7 +23,8 @@ export function buildAreaOptions(spec) {
|
|
|
23
23
|
const firstYKey = yKeys[0] ?? 'value';
|
|
24
24
|
const sampleValue = data.length > 0 && data[0] ? data[0][firstYKey] ?? 0 : 0;
|
|
25
25
|
const valueFormat = spec.format ?? inferFormat(firstYKey, sampleValue);
|
|
26
|
-
const
|
|
26
|
+
const currency = resolveCurrency(spec.currency);
|
|
27
|
+
const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale);
|
|
27
28
|
// Detect x-axis type: time, value, or category
|
|
28
29
|
const xAxisType = spec.xAxisType ?? inferAxisType(categories);
|
|
29
30
|
// Get axis label config based on axis type
|
package/dist/charts/bar.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { BAR_MAX_WIDTH, LEGEND_ITEM_WIDTH, LEGEND_ITEM_HEIGHT, LEGEND_ITEM_GAP, 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 { inferFormat, getLabelFormatterJs, getAxisFormatterJs, inferAxisType } from '../core/formatting.js';
|
|
7
|
+
import { inferFormat, getLabelFormatterJs, getAxisFormatterJs, inferAxisType, resolveCurrency } from '../core/formatting.js';
|
|
8
8
|
/**
|
|
9
9
|
* Build ECharts options for a bar chart
|
|
10
10
|
*/
|
|
@@ -23,8 +23,9 @@ export function buildBarOptions(spec) {
|
|
|
23
23
|
const firstYKey = yKeys[0] ?? 'value';
|
|
24
24
|
const sampleValue = data.length > 0 && data[0] ? data[0][firstYKey] ?? 0 : 0;
|
|
25
25
|
const valueFormat = spec.format ?? inferFormat(firstYKey, sampleValue);
|
|
26
|
-
const
|
|
27
|
-
const
|
|
26
|
+
const currency = resolveCurrency(spec.currency);
|
|
27
|
+
const labelFormatter = getLabelFormatterJs(valueFormat, currency.symbol, currency.locale);
|
|
28
|
+
const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale);
|
|
28
29
|
// Detect category axis type (this is x-axis for vertical, y-axis for horizontal)
|
|
29
30
|
const categoryAxisType = spec.xAxisType ?? inferAxisType(categories);
|
|
30
31
|
// For horizontal bars, x-axis is always 'value'; for vertical it's the category axis type
|
package/dist/charts/bubble.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bubble chart generator
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { FONT_SIZE_TINY, ECHARTS_CDN, FONT_STACK, getThemeColors, getPaletteWithCustom } from '../core/themes.js';
|
|
5
5
|
import { serializeOption } from '../core/serializer.js';
|
|
6
6
|
import { registerChart, registerOptions } from './registry.js';
|
|
7
7
|
/**
|
|
@@ -134,7 +134,7 @@ function generateBubble(spec) {
|
|
|
134
134
|
background-color: ${colors.background}; font-family: ${FONT_STACK};
|
|
135
135
|
}
|
|
136
136
|
.container { padding: 20px; width: 100%; height: 100%; display: flex; flex-direction: column; }
|
|
137
|
-
.red-line { width: 100%; height: 3px; background-color: ${
|
|
137
|
+
.red-line { width: 100%; height: 3px; background-color: ${colors.accent}; margin-bottom: 12px; }
|
|
138
138
|
h2 { font-family: ${FONT_STACK}; color: ${colors.text}; font-size: 18px; font-weight: 900; margin: 0 0 4px 0; }
|
|
139
139
|
#chart { width: 100%; flex: 1; min-height: 300px; }
|
|
140
140
|
</style>
|
package/dist/charts/combo.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { FONT_SIZE_XXS, BAR_MAX_WIDTH, LEGEND_ITEM_WIDTH, LEGEND_ITEM_HEIGHT, LEGEND_ITEM_GAP, getThemeColors, getPaletteWithCustom, } from '../core/themes.js';
|
|
5
5
|
import { wrapHtml } from '../core/serializer.js';
|
|
6
|
-
import { inferAxisType, inferFormat, getAxisFormatterJs } from '../core/formatting.js';
|
|
6
|
+
import { inferAxisType, inferFormat, getAxisFormatterJs, resolveCurrency } from '../core/formatting.js';
|
|
7
7
|
import { registerChart, registerOptions } from './registry.js';
|
|
8
8
|
/**
|
|
9
9
|
* Build ECharts options for a combo chart (bar + line with optional dual axis)
|
|
@@ -23,11 +23,12 @@ export function buildComboOptions(spec) {
|
|
|
23
23
|
// Infer format for primary y-axis (bars)
|
|
24
24
|
const barSample = data.length > 0 && data[0] && barKeys[0] ? data[0][barKeys[0]] ?? 0 : 0;
|
|
25
25
|
const primaryFormat = spec.format ?? inferFormat(barKeys[0] ?? 'value', barSample);
|
|
26
|
-
const
|
|
26
|
+
const currency = resolveCurrency(spec.currency);
|
|
27
|
+
const primaryAxisFormatter = getAxisFormatterJs(primaryFormat, currency.symbol, currency.locale);
|
|
27
28
|
// Infer format for secondary y-axis (line if dual axis)
|
|
28
29
|
const lineSample = data.length > 0 && data[0] && lineKeys[0] ? data[0][lineKeys[0]] ?? 0 : 0;
|
|
29
30
|
const secondaryFormat = spec.secondaryFormat ?? inferFormat(lineKeys[0] ?? 'value', lineSample);
|
|
30
|
-
const secondaryAxisFormatter = getAxisFormatterJs(secondaryFormat);
|
|
31
|
+
const secondaryAxisFormatter = getAxisFormatterJs(secondaryFormat, currency.symbol, currency.locale);
|
|
31
32
|
// Detect x-axis type
|
|
32
33
|
const xAxisType = inferAxisType(categories);
|
|
33
34
|
// Get axis label config based on axis type
|
package/dist/charts/dumbbell.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { COLORS, FONT_SIZE_TINY, DUMBBELL_SYMBOL_SIZE, DEFAULT_CHART_HEIGHT, getThemeColors, } from '../core/themes.js';
|
|
8
8
|
import { wrapHtml } from '../core/serializer.js';
|
|
9
|
-
import { inferFormat, getAxisFormatterJs, formatValue, formatNumber } from '../core/formatting.js';
|
|
9
|
+
import { inferFormat, getAxisFormatterJs, formatValue, formatNumber, resolveCurrency, localeFixed } from '../core/formatting.js';
|
|
10
10
|
import { registerChart, registerOptions } from './registry.js';
|
|
11
11
|
// Directional colors
|
|
12
12
|
const POSITIVE_COLOR = COLORS.POSITIVE_GREEN;
|
|
@@ -50,25 +50,29 @@ const COMPACT_LABEL_CHAR_THRESHOLD = 5;
|
|
|
50
50
|
* Compute the compact dumbbell label string for a given value.
|
|
51
51
|
* Mirrors the JS formatter logic so we can measure character count at build time.
|
|
52
52
|
*/
|
|
53
|
-
function compactFormatLabel(value, fmt) {
|
|
54
|
-
if (fmt === 'auto' || fmt === '
|
|
53
|
+
function compactFormatLabel(value, fmt, currencySymbol = '$', locale = 'en-US') {
|
|
54
|
+
if (fmt === 'auto' || fmt === 'currency_auto') {
|
|
55
55
|
const abs = Math.abs(value);
|
|
56
|
-
const p = fmt === '
|
|
56
|
+
const p = fmt === 'currency_auto' ? currencySymbol : '';
|
|
57
|
+
const isCurrency = fmt === 'currency_auto';
|
|
58
|
+
const lf = isCurrency
|
|
59
|
+
? (v, d) => localeFixed(v, d, locale)
|
|
60
|
+
: (v, d) => v.toFixed(d);
|
|
57
61
|
let r;
|
|
58
62
|
if (abs >= 1e9) {
|
|
59
63
|
const s = abs / 1e9;
|
|
60
|
-
r = p + (s >= 10 ? Math.round(s).toString() : s
|
|
64
|
+
r = p + (s >= 10 ? Math.round(s).toString() : lf(s, 1)) + 'b';
|
|
61
65
|
}
|
|
62
66
|
else if (abs >= 1e6) {
|
|
63
67
|
const s = abs / 1e6;
|
|
64
|
-
r = p + (s >= 10 ? Math.round(s).toString() : s
|
|
68
|
+
r = p + (s >= 10 ? Math.round(s).toString() : lf(s, 1)) + 'm';
|
|
65
69
|
}
|
|
66
70
|
else if (abs >= 1e3) {
|
|
67
71
|
const s = abs / 1e3;
|
|
68
|
-
r = p + (s >= 10 ? Math.round(s).toString() : s
|
|
72
|
+
r = p + (s >= 10 ? Math.round(s).toString() : lf(s, 1)) + 'k';
|
|
69
73
|
}
|
|
70
74
|
else {
|
|
71
|
-
r = abs === Math.floor(abs) ? p + abs.toString() : p + abs
|
|
75
|
+
r = abs === Math.floor(abs) ? p + abs.toString() : p + lf(abs, 1);
|
|
72
76
|
}
|
|
73
77
|
return value < 0 ? '(' + r + ')' : r;
|
|
74
78
|
}
|
|
@@ -77,8 +81,8 @@ function compactFormatLabel(value, fmt) {
|
|
|
77
81
|
/**
|
|
78
82
|
* Get the appropriate font size for a dumbbell dot label.
|
|
79
83
|
*/
|
|
80
|
-
function dumbbellLabelFontSize(value, fmt) {
|
|
81
|
-
const label = compactFormatLabel(value, fmt);
|
|
84
|
+
function dumbbellLabelFontSize(value, fmt, currencySymbol = '$', locale = 'en-US') {
|
|
85
|
+
const label = compactFormatLabel(value, fmt, currencySymbol, locale);
|
|
82
86
|
return label.length >= COMPACT_LABEL_CHAR_THRESHOLD
|
|
83
87
|
? DUMBBELL_LABEL_FONT_SMALL
|
|
84
88
|
: DUMBBELL_LABEL_FONT;
|
|
@@ -88,56 +92,68 @@ function dumbbellLabelFontSize(value, fmt) {
|
|
|
88
92
|
* Dumbbell stores values as [value, categoryIndex], so we extract value from params.value[0].
|
|
89
93
|
* Uses compact formatting (~4-5 chars max) to fit inside small dot symbols.
|
|
90
94
|
*/
|
|
91
|
-
function buildDumbbellLabelFormatter(fmt) {
|
|
92
|
-
if (fmt === 'auto' || fmt === '
|
|
93
|
-
const isCurrency = fmt === '
|
|
95
|
+
function buildDumbbellLabelFormatter(fmt, currencySymbol = '$', locale = 'en-US') {
|
|
96
|
+
if (fmt === 'auto' || fmt === 'currency_auto') {
|
|
97
|
+
const isCurrency = fmt === 'currency_auto' ? 'true' : 'false';
|
|
98
|
+
const symbolStr = JSON.stringify(currencySymbol);
|
|
99
|
+
const locStr = JSON.stringify(locale);
|
|
100
|
+
const lfHelper = fmt === 'currency_auto'
|
|
101
|
+
? `var loc = ${locStr}; var lf = function(v, d) { return v.toLocaleString(loc, {minimumFractionDigits:d, maximumFractionDigits:d}); };`
|
|
102
|
+
: `var lf = function(v, d) { return v.toFixed(d); };`;
|
|
94
103
|
// Compact: max 1 decimal for scaled values to fit in dot labels
|
|
95
104
|
return `function(params) {
|
|
96
105
|
var v = params.value[0];
|
|
97
106
|
if (v == null) return '';
|
|
98
107
|
var abs = Math.abs(v);
|
|
99
108
|
var neg = v < 0;
|
|
100
|
-
var p = ${isCurrency} ?
|
|
109
|
+
var p = ${isCurrency} ? ${symbolStr} : '';
|
|
110
|
+
${lfHelper}
|
|
101
111
|
var r;
|
|
102
112
|
if (abs >= 1e9) {
|
|
103
113
|
var s = abs / 1e9;
|
|
104
|
-
r = p + (s >= 10 ? Math.round(s) : s
|
|
114
|
+
r = p + (s >= 10 ? Math.round(s) : lf(s,1)) + 'b';
|
|
105
115
|
} else if (abs >= 1e6) {
|
|
106
116
|
var s = abs / 1e6;
|
|
107
|
-
r = p + (s >= 10 ? Math.round(s) : s
|
|
117
|
+
r = p + (s >= 10 ? Math.round(s) : lf(s,1)) + 'm';
|
|
108
118
|
} else if (abs >= 1e3) {
|
|
109
119
|
var s = abs / 1e3;
|
|
110
|
-
r = p + (s >= 10 ? Math.round(s) : s
|
|
120
|
+
r = p + (s >= 10 ? Math.round(s) : lf(s,1)) + 'k';
|
|
111
121
|
} else {
|
|
112
|
-
r = abs === Math.floor(abs) ? p + abs : p + abs
|
|
122
|
+
r = abs === Math.floor(abs) ? p + abs : p + lf(abs,1);
|
|
113
123
|
}
|
|
114
124
|
return neg ? '(' + r + ')' : r;
|
|
115
125
|
}`;
|
|
116
126
|
}
|
|
117
|
-
const expr = formatValue(fmt);
|
|
127
|
+
const expr = formatValue(fmt, currencySymbol, locale);
|
|
118
128
|
return `function(params) { var value = params.value[0]; if (value == null) return ''; return ${expr}; }`;
|
|
119
129
|
}
|
|
120
130
|
/**
|
|
121
131
|
* Build a tooltip value formatting expression for dumbbell charts.
|
|
122
132
|
* Tooltip can be more verbose than dot labels, so uses full smart formatting.
|
|
123
133
|
*/
|
|
124
|
-
function buildDumbbellTooltipExpr(fmt) {
|
|
125
|
-
if (fmt === 'auto' || fmt === '
|
|
126
|
-
const isCurrency = fmt === '
|
|
134
|
+
function buildDumbbellTooltipExpr(fmt, currencySymbol = '$', locale = 'en-US') {
|
|
135
|
+
if (fmt === 'auto' || fmt === 'currency_auto') {
|
|
136
|
+
const isCurrency = fmt === 'currency_auto' ? 'true' : 'false';
|
|
137
|
+
const symbolStr = JSON.stringify(currencySymbol);
|
|
138
|
+
const locStr = JSON.stringify(locale);
|
|
139
|
+
const lfHelper = fmt === 'currency_auto'
|
|
140
|
+
? `var loc = ${locStr}; var lf = function(v, d) { return v.toLocaleString(loc, {minimumFractionDigits:d, maximumFractionDigits:d}); };`
|
|
141
|
+
: `var lf = function(v, d) { return v.toFixed(d); };`;
|
|
127
142
|
return `(function(v) {
|
|
128
143
|
var abs = Math.abs(v);
|
|
129
144
|
var neg = v < 0;
|
|
130
|
-
var p = ${isCurrency} ?
|
|
145
|
+
var p = ${isCurrency} ? ${symbolStr} : '';
|
|
146
|
+
${lfHelper}
|
|
131
147
|
var r;
|
|
132
|
-
if (abs >= 1e9) { var s = abs / 1e9; r = s >= 100 ? p + s
|
|
133
|
-
else if (abs >= 1e6) { var s = abs / 1e6; r = s >= 100 ? p + s
|
|
134
|
-
else if (abs >= 1e4) { var s = abs / 1e3; r = s >= 100 ? p + s
|
|
135
|
-
else if (abs >= 1e3) { r = p + abs.toLocaleString(
|
|
136
|
-
else { r = abs === Math.floor(abs) ? p + abs : p + abs
|
|
148
|
+
if (abs >= 1e9) { var s = abs / 1e9; r = s >= 100 ? p + lf(s,1) + 'b' : s >= 10 ? p + lf(s,2) + 'b' : p + lf(s,3) + 'b'; }
|
|
149
|
+
else if (abs >= 1e6) { var s = abs / 1e6; r = s >= 100 ? p + lf(s,1) + 'm' : s >= 10 ? p + lf(s,2) + 'm' : p + lf(s,3) + 'm'; }
|
|
150
|
+
else if (abs >= 1e4) { var s = abs / 1e3; r = s >= 100 ? p + lf(s,1) + 'k' : p + lf(s,2) + 'k'; }
|
|
151
|
+
else if (abs >= 1e3) { r = p + abs.toLocaleString(${locStr}, {maximumFractionDigits: 0}); }
|
|
152
|
+
else { r = abs === Math.floor(abs) ? p + abs : p + lf(abs,2); }
|
|
137
153
|
return neg ? '(' + r + ')' : r;
|
|
138
154
|
})(val)`;
|
|
139
155
|
}
|
|
140
|
-
const expr = formatValue(fmt);
|
|
156
|
+
const expr = formatValue(fmt, currencySymbol, locale);
|
|
141
157
|
// formatValue uses 'value' as the variable name, replace with 'val' for tooltip context
|
|
142
158
|
return expr.replace(/\bvalue\b/g, 'val');
|
|
143
159
|
}
|
|
@@ -177,7 +193,8 @@ export function buildDumbbellOptions(spec) {
|
|
|
177
193
|
// Infer value format
|
|
178
194
|
const sampleValue = startValues[0] ?? 0;
|
|
179
195
|
const valueFormat = spec.format ?? inferFormat(startField, sampleValue);
|
|
180
|
-
const
|
|
196
|
+
const currency = resolveCurrency(spec.currency);
|
|
197
|
+
const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale);
|
|
181
198
|
// Calculate axis limits
|
|
182
199
|
const allValues = [...startValues, ...endValues];
|
|
183
200
|
let xMin = spec.xMin;
|
|
@@ -198,7 +215,7 @@ export function buildDumbbellOptions(spec) {
|
|
|
198
215
|
}
|
|
199
216
|
}
|
|
200
217
|
// Value formatter for labels inside dots (uses formatValue for all format types)
|
|
201
|
-
const valueFormatterJs = buildDumbbellLabelFormatter(valueFormat);
|
|
218
|
+
const valueFormatterJs = buildDumbbellLabelFormatter(valueFormat, currency.symbol, currency.locale);
|
|
202
219
|
// Build line data for custom series
|
|
203
220
|
const lineData = startValues.map((s, i) => {
|
|
204
221
|
const e = endValues[i];
|
|
@@ -309,7 +326,7 @@ export function buildDumbbellOptions(spec) {
|
|
|
309
326
|
label: {
|
|
310
327
|
show: showValues,
|
|
311
328
|
position: 'inside',
|
|
312
|
-
fontSize: dumbbellLabelFontSize(v, valueFormat),
|
|
329
|
+
fontSize: dumbbellLabelFontSize(v, valueFormat, currency.symbol, currency.locale),
|
|
313
330
|
fontWeight: 'bold',
|
|
314
331
|
color: rowColors[i],
|
|
315
332
|
formatter: { _js_: valueFormatterJs },
|
|
@@ -332,7 +349,7 @@ export function buildDumbbellOptions(spec) {
|
|
|
332
349
|
label: {
|
|
333
350
|
show: showValues,
|
|
334
351
|
position: 'inside',
|
|
335
|
-
fontSize: dumbbellLabelFontSize(v, valueFormat),
|
|
352
|
+
fontSize: dumbbellLabelFontSize(v, valueFormat, currency.symbol, currency.locale),
|
|
336
353
|
fontWeight: 'bold',
|
|
337
354
|
color: '#ffffff',
|
|
338
355
|
formatter: { _js_: valueFormatterJs },
|
|
@@ -346,7 +363,7 @@ export function buildDumbbellOptions(spec) {
|
|
|
346
363
|
z: 10,
|
|
347
364
|
});
|
|
348
365
|
// Tooltip formatter with value formatting
|
|
349
|
-
const tooltipValueExpr = buildDumbbellTooltipExpr(valueFormat);
|
|
366
|
+
const tooltipValueExpr = buildDumbbellTooltipExpr(valueFormat, currency.symbol, currency.locale);
|
|
350
367
|
const tooltipFormatterJs = `function(params) {
|
|
351
368
|
if (params.seriesName === 'range') return '';
|
|
352
369
|
var cat = ${JSON.stringify(categories)}[params.data.value ? params.data.value[1] : params.data[1]];
|
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,7 @@
|
|
|
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
8
|
/**
|
|
9
9
|
* Build ECharts options for a scatter plot
|
|
10
10
|
*/
|
|
@@ -22,8 +22,9 @@ export function buildScatterOptions(spec) {
|
|
|
22
22
|
const ySample = data.length > 0 && data[0] ? data[0][yField] ?? 0 : 0;
|
|
23
23
|
const xFormat = spec.xFormat ?? inferFormat(xField, xSample);
|
|
24
24
|
const yFormat = spec.yFormat ?? inferFormat(yField, ySample);
|
|
25
|
-
const
|
|
26
|
-
const
|
|
25
|
+
const currency = resolveCurrency(spec.currency);
|
|
26
|
+
const xAxisFormatter = getAxisFormatterJs(xFormat, currency.symbol, currency.locale);
|
|
27
|
+
const yAxisFormatter = getAxisFormatterJs(yFormat, currency.symbol, currency.locale);
|
|
27
28
|
// Build series - either single series or grouped by seriesField
|
|
28
29
|
let seriesList;
|
|
29
30
|
let legendData = [];
|
|
@@ -68,8 +69,8 @@ export function buildScatterOptions(spec) {
|
|
|
68
69
|
];
|
|
69
70
|
}
|
|
70
71
|
// Build tooltip formatter with label support and value formatting
|
|
71
|
-
const xFormatExpr = formatValue(xFormat);
|
|
72
|
-
const yFormatExpr = formatValue(yFormat);
|
|
72
|
+
const xFormatExpr = formatValue(xFormat, currency.symbol, currency.locale);
|
|
73
|
+
const yFormatExpr = formatValue(yFormat, currency.symbol, currency.locale);
|
|
73
74
|
const tooltipFormatterJs = `function(params) {
|
|
74
75
|
var d = params.value || params.data;
|
|
75
76
|
var result = '';
|
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>
|