mviz 1.5.3 → 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 +22 -7
- package/dist/charts/combo.js +4 -3
- package/dist/charts/dumbbell.js +126 -25
- 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 +29 -7
- 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/serializer.js +2 -1
- package/dist/core/theme-loader.js +1 -0
- package/dist/core/themes.d.ts +9 -1
- package/dist/core/themes.js +23 -5
- package/dist/generate_test_harness.d.ts +2 -1
- package/dist/generate_test_harness.js +14 -5
- package/dist/layout/parser.js +57 -18
- package/dist/layout/templates.d.ts +3 -3
- package/dist/layout/templates.js +11 -9
- package/dist/types.d.ts +17 -1
- package/package.json +2 -2
- package/schema/mviz.v1.schema.json +71 -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,8 @@
|
|
|
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
|
+
import { serializeOption } from '../core/serializer.js';
|
|
5
6
|
import { registerChart, registerOptions } from './registry.js';
|
|
6
7
|
/**
|
|
7
8
|
* Build ECharts options for a bubble chart
|
|
@@ -11,6 +12,7 @@ export function buildBubbleOptions(spec) {
|
|
|
11
12
|
const xField = spec.x ?? 'x';
|
|
12
13
|
const yField = spec.y ?? 'y';
|
|
13
14
|
const sizeField = spec.size ?? 'size';
|
|
15
|
+
const labelField = spec.label;
|
|
14
16
|
const theme = (spec.theme ?? 'light');
|
|
15
17
|
const colors = getThemeColors(theme);
|
|
16
18
|
const palette = getPaletteWithCustom(theme, spec.customTheme);
|
|
@@ -40,8 +42,22 @@ export function buildBubbleOptions(spec) {
|
|
|
40
42
|
const scaledSize = 10 + ((s - minSize) / sizeRange) * 40;
|
|
41
43
|
const xVal = xIsCategory ? xCategories.indexOf(String(d[xField] ?? '')) : (d[xField] ?? 0);
|
|
42
44
|
const yVal = yIsCategory ? yCategories.indexOf(String(d[yField] ?? '')) : (d[yField] ?? 0);
|
|
43
|
-
|
|
45
|
+
const point = [xVal, yVal, s, scaledSize];
|
|
46
|
+
if (labelField)
|
|
47
|
+
point.push(String(d[labelField] ?? ''));
|
|
48
|
+
return point;
|
|
44
49
|
});
|
|
50
|
+
// Build tooltip formatter with label support
|
|
51
|
+
const labelIdx = labelField ? 4 : -1;
|
|
52
|
+
const tooltipFormatterJs = `function(params) {
|
|
53
|
+
var d = params.value || params.data;
|
|
54
|
+
var result = '';
|
|
55
|
+
${labelIdx >= 0 ? "var label = d[" + labelIdx + "]; if (label) result += '<strong>' + label + '</strong><br/>';" : ''}
|
|
56
|
+
result += '${xField}' + ': ' + d[0] + '<br/>';
|
|
57
|
+
result += '${yField}' + ': ' + d[1] + '<br/>';
|
|
58
|
+
result += '${sizeField}' + ': ' + d[2];
|
|
59
|
+
return result;
|
|
60
|
+
}`;
|
|
45
61
|
return {
|
|
46
62
|
backgroundColor: 'transparent',
|
|
47
63
|
animation: false,
|
|
@@ -51,6 +67,7 @@ export function buildBubbleOptions(spec) {
|
|
|
51
67
|
backgroundColor: colors.paper,
|
|
52
68
|
borderColor: colors.border,
|
|
53
69
|
textStyle: { color: colors.text },
|
|
70
|
+
formatter: { _js_: tooltipFormatterJs },
|
|
54
71
|
},
|
|
55
72
|
grid: {
|
|
56
73
|
left: '6%',
|
|
@@ -89,7 +106,7 @@ export function buildBubbleOptions(spec) {
|
|
|
89
106
|
{
|
|
90
107
|
type: 'scatter',
|
|
91
108
|
data: bubbleData,
|
|
92
|
-
symbolSize: '
|
|
109
|
+
symbolSize: { _js_: 'function(val) { return val[3] || 20; }' },
|
|
93
110
|
itemStyle: { color: palette[0], opacity: 0.55 },
|
|
94
111
|
},
|
|
95
112
|
],
|
|
@@ -103,9 +120,7 @@ function generateBubble(spec) {
|
|
|
103
120
|
const title = spec.title ?? '';
|
|
104
121
|
const theme = (spec.theme ?? 'light');
|
|
105
122
|
const colors = getThemeColors(theme);
|
|
106
|
-
|
|
107
|
-
let optionJson = JSON.stringify(option, null, 2);
|
|
108
|
-
optionJson = optionJson.replace('"__SYMBOL_SIZE_FUNC__"', 'function(val) { return val[3] || 20; }');
|
|
123
|
+
const optionJson = serializeOption(option);
|
|
109
124
|
return `<!DOCTYPE html>
|
|
110
125
|
<html lang="en">
|
|
111
126
|
<head>
|
|
@@ -119,7 +134,7 @@ function generateBubble(spec) {
|
|
|
119
134
|
background-color: ${colors.background}; font-family: ${FONT_STACK};
|
|
120
135
|
}
|
|
121
136
|
.container { padding: 20px; width: 100%; height: 100%; display: flex; flex-direction: column; }
|
|
122
|
-
.red-line { width: 100%; height: 3px; background-color: ${
|
|
137
|
+
.red-line { width: 100%; height: 3px; background-color: ${colors.accent}; margin-bottom: 12px; }
|
|
123
138
|
h2 { font-family: ${FONT_STACK}; color: ${colors.text}; font-size: 18px; font-weight: 900; margin: 0 0 4px 0; }
|
|
124
139
|
#chart { width: 100%; flex: 1; min-height: 300px; }
|
|
125
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 } 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;
|
|
@@ -42,6 +42,121 @@ function getNiceStep(dataRange) {
|
|
|
42
42
|
return magnitude / 2;
|
|
43
43
|
return magnitude;
|
|
44
44
|
}
|
|
45
|
+
// Font sizes for dumbbell dot labels
|
|
46
|
+
const DUMBBELL_LABEL_FONT = FONT_SIZE_TINY - 2; // 8px - normal
|
|
47
|
+
const DUMBBELL_LABEL_FONT_SMALL = FONT_SIZE_TINY - 3; // 7px - for 5+ char labels
|
|
48
|
+
const COMPACT_LABEL_CHAR_THRESHOLD = 5;
|
|
49
|
+
/**
|
|
50
|
+
* Compute the compact dumbbell label string for a given value.
|
|
51
|
+
* Mirrors the JS formatter logic so we can measure character count at build time.
|
|
52
|
+
*/
|
|
53
|
+
function compactFormatLabel(value, fmt, currencySymbol = '$', locale = 'en-US') {
|
|
54
|
+
if (fmt === 'auto' || fmt === 'currency_auto') {
|
|
55
|
+
const abs = Math.abs(value);
|
|
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);
|
|
61
|
+
let r;
|
|
62
|
+
if (abs >= 1e9) {
|
|
63
|
+
const s = abs / 1e9;
|
|
64
|
+
r = p + (s >= 10 ? Math.round(s).toString() : lf(s, 1)) + 'b';
|
|
65
|
+
}
|
|
66
|
+
else if (abs >= 1e6) {
|
|
67
|
+
const s = abs / 1e6;
|
|
68
|
+
r = p + (s >= 10 ? Math.round(s).toString() : lf(s, 1)) + 'm';
|
|
69
|
+
}
|
|
70
|
+
else if (abs >= 1e3) {
|
|
71
|
+
const s = abs / 1e3;
|
|
72
|
+
r = p + (s >= 10 ? Math.round(s).toString() : lf(s, 1)) + 'k';
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
r = abs === Math.floor(abs) ? p + abs.toString() : p + lf(abs, 1);
|
|
76
|
+
}
|
|
77
|
+
return value < 0 ? '(' + r + ')' : r;
|
|
78
|
+
}
|
|
79
|
+
return formatNumber(value, fmt);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get the appropriate font size for a dumbbell dot label.
|
|
83
|
+
*/
|
|
84
|
+
function dumbbellLabelFontSize(value, fmt, currencySymbol = '$', locale = 'en-US') {
|
|
85
|
+
const label = compactFormatLabel(value, fmt, currencySymbol, locale);
|
|
86
|
+
return label.length >= COMPACT_LABEL_CHAR_THRESHOLD
|
|
87
|
+
? DUMBBELL_LABEL_FONT_SMALL
|
|
88
|
+
: DUMBBELL_LABEL_FONT;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Build a label formatter function for dumbbell dot labels.
|
|
92
|
+
* Dumbbell stores values as [value, categoryIndex], so we extract value from params.value[0].
|
|
93
|
+
* Uses compact formatting (~4-5 chars max) to fit inside small dot symbols.
|
|
94
|
+
*/
|
|
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); };`;
|
|
103
|
+
// Compact: max 1 decimal for scaled values to fit in dot labels
|
|
104
|
+
return `function(params) {
|
|
105
|
+
var v = params.value[0];
|
|
106
|
+
if (v == null) return '';
|
|
107
|
+
var abs = Math.abs(v);
|
|
108
|
+
var neg = v < 0;
|
|
109
|
+
var p = ${isCurrency} ? ${symbolStr} : '';
|
|
110
|
+
${lfHelper}
|
|
111
|
+
var r;
|
|
112
|
+
if (abs >= 1e9) {
|
|
113
|
+
var s = abs / 1e9;
|
|
114
|
+
r = p + (s >= 10 ? Math.round(s) : lf(s,1)) + 'b';
|
|
115
|
+
} else if (abs >= 1e6) {
|
|
116
|
+
var s = abs / 1e6;
|
|
117
|
+
r = p + (s >= 10 ? Math.round(s) : lf(s,1)) + 'm';
|
|
118
|
+
} else if (abs >= 1e3) {
|
|
119
|
+
var s = abs / 1e3;
|
|
120
|
+
r = p + (s >= 10 ? Math.round(s) : lf(s,1)) + 'k';
|
|
121
|
+
} else {
|
|
122
|
+
r = abs === Math.floor(abs) ? p + abs : p + lf(abs,1);
|
|
123
|
+
}
|
|
124
|
+
return neg ? '(' + r + ')' : r;
|
|
125
|
+
}`;
|
|
126
|
+
}
|
|
127
|
+
const expr = formatValue(fmt, currencySymbol, locale);
|
|
128
|
+
return `function(params) { var value = params.value[0]; if (value == null) return ''; return ${expr}; }`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Build a tooltip value formatting expression for dumbbell charts.
|
|
132
|
+
* Tooltip can be more verbose than dot labels, so uses full smart formatting.
|
|
133
|
+
*/
|
|
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); };`;
|
|
142
|
+
return `(function(v) {
|
|
143
|
+
var abs = Math.abs(v);
|
|
144
|
+
var neg = v < 0;
|
|
145
|
+
var p = ${isCurrency} ? ${symbolStr} : '';
|
|
146
|
+
${lfHelper}
|
|
147
|
+
var r;
|
|
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); }
|
|
153
|
+
return neg ? '(' + r + ')' : r;
|
|
154
|
+
})(val)`;
|
|
155
|
+
}
|
|
156
|
+
const expr = formatValue(fmt, currencySymbol, locale);
|
|
157
|
+
// formatValue uses 'value' as the variable name, replace with 'val' for tooltip context
|
|
158
|
+
return expr.replace(/\bvalue\b/g, 'val');
|
|
159
|
+
}
|
|
45
160
|
/**
|
|
46
161
|
* Build ECharts options for a dumbbell plot with directional coloring
|
|
47
162
|
*/
|
|
@@ -78,8 +193,8 @@ export function buildDumbbellOptions(spec) {
|
|
|
78
193
|
// Infer value format
|
|
79
194
|
const sampleValue = startValues[0] ?? 0;
|
|
80
195
|
const valueFormat = spec.format ?? inferFormat(startField, sampleValue);
|
|
81
|
-
const
|
|
82
|
-
const axisFormatter = getAxisFormatterJs(valueFormat);
|
|
196
|
+
const currency = resolveCurrency(spec.currency);
|
|
197
|
+
const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale);
|
|
83
198
|
// Calculate axis limits
|
|
84
199
|
const allValues = [...startValues, ...endValues];
|
|
85
200
|
let xMin = spec.xMin;
|
|
@@ -99,24 +214,8 @@ export function buildDumbbellOptions(spec) {
|
|
|
99
214
|
xMax = niceCeil(rawMax, step);
|
|
100
215
|
}
|
|
101
216
|
}
|
|
102
|
-
// Value formatter for labels inside dots
|
|
103
|
-
const valueFormatterJs =
|
|
104
|
-
var v = params.value[0];
|
|
105
|
-
var abs = Math.abs(v);
|
|
106
|
-
var isCurrency = ${isCurrency ? 'true' : 'false'};
|
|
107
|
-
var prefix = isCurrency ? '$' : '';
|
|
108
|
-
if (abs >= 1000000) {
|
|
109
|
-
var val = v / 1000000;
|
|
110
|
-
var formatted = isCurrency ? Math.round(val) : (Math.abs(val) >= 10 ? Math.round(val) : val.toFixed(1));
|
|
111
|
-
return prefix + formatted + 'm';
|
|
112
|
-
}
|
|
113
|
-
if (abs >= 1000) {
|
|
114
|
-
var val = v / 1000;
|
|
115
|
-
var formatted = isCurrency ? Math.round(val) : (Math.abs(val) >= 10 ? Math.round(val) : val.toFixed(1));
|
|
116
|
-
return prefix + formatted + 'k';
|
|
117
|
-
}
|
|
118
|
-
return isCurrency ? prefix + Math.round(v) : v;
|
|
119
|
-
}`;
|
|
217
|
+
// Value formatter for labels inside dots (uses formatValue for all format types)
|
|
218
|
+
const valueFormatterJs = buildDumbbellLabelFormatter(valueFormat, currency.symbol, currency.locale);
|
|
120
219
|
// Build line data for custom series
|
|
121
220
|
const lineData = startValues.map((s, i) => {
|
|
122
221
|
const e = endValues[i];
|
|
@@ -227,7 +326,7 @@ export function buildDumbbellOptions(spec) {
|
|
|
227
326
|
label: {
|
|
228
327
|
show: showValues,
|
|
229
328
|
position: 'inside',
|
|
230
|
-
fontSize:
|
|
329
|
+
fontSize: dumbbellLabelFontSize(v, valueFormat, currency.symbol, currency.locale),
|
|
231
330
|
fontWeight: 'bold',
|
|
232
331
|
color: rowColors[i],
|
|
233
332
|
formatter: { _js_: valueFormatterJs },
|
|
@@ -250,7 +349,7 @@ export function buildDumbbellOptions(spec) {
|
|
|
250
349
|
label: {
|
|
251
350
|
show: showValues,
|
|
252
351
|
position: 'inside',
|
|
253
|
-
fontSize:
|
|
352
|
+
fontSize: dumbbellLabelFontSize(v, valueFormat, currency.symbol, currency.locale),
|
|
254
353
|
fontWeight: 'bold',
|
|
255
354
|
color: '#ffffff',
|
|
256
355
|
formatter: { _js_: valueFormatterJs },
|
|
@@ -263,12 +362,14 @@ export function buildDumbbellOptions(spec) {
|
|
|
263
362
|
symbolSize: DUMBBELL_SYMBOL_SIZE,
|
|
264
363
|
z: 10,
|
|
265
364
|
});
|
|
266
|
-
// Tooltip formatter
|
|
365
|
+
// Tooltip formatter with value formatting
|
|
366
|
+
const tooltipValueExpr = buildDumbbellTooltipExpr(valueFormat, currency.symbol, currency.locale);
|
|
267
367
|
const tooltipFormatterJs = `function(params) {
|
|
268
368
|
if (params.seriesName === 'range') return '';
|
|
269
369
|
var cat = ${JSON.stringify(categories)}[params.data.value ? params.data.value[1] : params.data[1]];
|
|
270
370
|
var val = params.data.value ? params.data.value[0] : params.data[0];
|
|
271
|
-
|
|
371
|
+
var formatted = ${tooltipValueExpr};
|
|
372
|
+
return '<strong>' + cat + '</strong><br/>' + params.seriesName + ': ' + formatted;
|
|
272
373
|
}`;
|
|
273
374
|
return {
|
|
274
375
|
backgroundColor: 'transparent',
|
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 } 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
|
*/
|
|
@@ -13,6 +13,7 @@ export function buildScatterOptions(spec) {
|
|
|
13
13
|
const xField = spec.x ?? 'x';
|
|
14
14
|
const yField = spec.y ?? 'y';
|
|
15
15
|
const seriesField = spec.series;
|
|
16
|
+
const labelField = spec.label;
|
|
16
17
|
const theme = (spec.theme ?? 'light');
|
|
17
18
|
const colors = getThemeColors(theme);
|
|
18
19
|
const palette = getPaletteWithCustom(theme, spec.customTheme);
|
|
@@ -21,8 +22,9 @@ export function buildScatterOptions(spec) {
|
|
|
21
22
|
const ySample = data.length > 0 && data[0] ? data[0][yField] ?? 0 : 0;
|
|
22
23
|
const xFormat = spec.xFormat ?? inferFormat(xField, xSample);
|
|
23
24
|
const yFormat = spec.yFormat ?? inferFormat(yField, ySample);
|
|
24
|
-
const
|
|
25
|
-
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);
|
|
26
28
|
// Build series - either single series or grouped by seriesField
|
|
27
29
|
let seriesList;
|
|
28
30
|
let legendData = [];
|
|
@@ -34,7 +36,10 @@ export function buildScatterOptions(spec) {
|
|
|
34
36
|
if (!groups.has(groupName)) {
|
|
35
37
|
groups.set(groupName, []);
|
|
36
38
|
}
|
|
37
|
-
|
|
39
|
+
const point = [d[xField] ?? 0, d[yField] ?? 0];
|
|
40
|
+
if (labelField)
|
|
41
|
+
point.push(String(d[labelField] ?? ''));
|
|
42
|
+
groups.get(groupName).push(point);
|
|
38
43
|
}
|
|
39
44
|
// Create a series for each group
|
|
40
45
|
legendData = [...groups.keys()];
|
|
@@ -47,8 +52,13 @@ export function buildScatterOptions(spec) {
|
|
|
47
52
|
}));
|
|
48
53
|
}
|
|
49
54
|
else {
|
|
50
|
-
// Single series
|
|
51
|
-
const scatterData = data.map((d) =>
|
|
55
|
+
// Single series
|
|
56
|
+
const scatterData = data.map((d) => {
|
|
57
|
+
const point = [d[xField] ?? 0, d[yField] ?? 0];
|
|
58
|
+
if (labelField)
|
|
59
|
+
point.push(String(d[labelField] ?? ''));
|
|
60
|
+
return point;
|
|
61
|
+
});
|
|
52
62
|
seriesList = [
|
|
53
63
|
{
|
|
54
64
|
type: 'scatter',
|
|
@@ -58,6 +68,17 @@ export function buildScatterOptions(spec) {
|
|
|
58
68
|
},
|
|
59
69
|
];
|
|
60
70
|
}
|
|
71
|
+
// Build tooltip formatter with label support and value formatting
|
|
72
|
+
const xFormatExpr = formatValue(xFormat, currency.symbol, currency.locale);
|
|
73
|
+
const yFormatExpr = formatValue(yFormat, currency.symbol, currency.locale);
|
|
74
|
+
const tooltipFormatterJs = `function(params) {
|
|
75
|
+
var d = params.value || params.data;
|
|
76
|
+
var result = '';
|
|
77
|
+
${labelField ? "var label = d[2]; if (label) result += '<strong>' + label + '</strong><br/>';" : ''}
|
|
78
|
+
var value = d[0]; result += ${JSON.stringify(xField)} + ': ' + ${xFormatExpr} + '<br/>';
|
|
79
|
+
var value = d[1]; result += ${JSON.stringify(yField)} + ': ' + ${yFormatExpr};
|
|
80
|
+
return result;
|
|
81
|
+
}`;
|
|
61
82
|
const option = {
|
|
62
83
|
backgroundColor: 'transparent',
|
|
63
84
|
animation: false,
|
|
@@ -67,6 +88,7 @@ export function buildScatterOptions(spec) {
|
|
|
67
88
|
backgroundColor: colors.paper,
|
|
68
89
|
borderColor: colors.border,
|
|
69
90
|
textStyle: { color: colors.text },
|
|
91
|
+
formatter: { _js_: tooltipFormatterJs },
|
|
70
92
|
},
|
|
71
93
|
grid: {
|
|
72
94
|
left: '2%',
|
|
@@ -93,7 +115,7 @@ export function buildScatterOptions(spec) {
|
|
|
93
115
|
type: 'value',
|
|
94
116
|
name: yField,
|
|
95
117
|
nameLocation: 'middle',
|
|
96
|
-
nameGap:
|
|
118
|
+
nameGap: 40,
|
|
97
119
|
nameTextStyle: { color: colors.textSecondary, fontSize: FONT_SIZE_TINY },
|
|
98
120
|
axisLine: { show: false },
|
|
99
121
|
axisTick: { show: false },
|