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/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.d.ts
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ChartSpec } from '../types.js';
|
|
5
5
|
/**
|
|
6
|
-
* Build ECharts options for a bubble chart
|
|
6
|
+
* Build ECharts options for a bubble chart.
|
|
7
7
|
*/
|
|
8
8
|
export declare function buildBubbleOptions(spec: ChartSpec): Record<string, unknown>;
|
|
9
9
|
/**
|
|
10
|
-
* Generate a bubble chart
|
|
10
|
+
* Generate a bubble chart.
|
|
11
11
|
*/
|
|
12
12
|
declare function generateBubble(spec: ChartSpec): string;
|
|
13
13
|
export { generateBubble };
|
package/dist/charts/bubble.js
CHANGED
|
@@ -1,158 +1,200 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bubble chart generator
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { FONT_SIZE_TINY, DEFAULT_CHART_HEIGHT, getThemeColors, getPaletteWithCustom, } from '../core/themes.js';
|
|
5
|
+
import { wrapHtml } from '../core/serializer.js';
|
|
6
6
|
import { registerChart, registerOptions } from './registry.js';
|
|
7
|
+
import { inferFormat, getAxisFormatterJs, formatValue, resolveCurrency } from '../core/formatting.js';
|
|
8
|
+
import { buildPointLabelConfig, groupDataBySeries, buildSeriesLegend } from '../core/chart-helpers.js';
|
|
9
|
+
/** Min/max bubble pixel sizes */
|
|
10
|
+
const BUBBLE_SIZE_MIN = 10;
|
|
11
|
+
const BUBBLE_SIZE_MAX = 50;
|
|
12
|
+
/** Bubble opacity */
|
|
13
|
+
const BUBBLE_OPACITY = 0.55;
|
|
14
|
+
/** Label sits at index 4 in bubble point arrays: [x, y, rawSize, scaledSize, label?] */
|
|
15
|
+
const BUBBLE_LABEL_INDEX = 4;
|
|
7
16
|
/**
|
|
8
|
-
*
|
|
17
|
+
* Scale a raw size value to a pixel diameter.
|
|
18
|
+
*/
|
|
19
|
+
function scaleBubbleSize(raw, minSize, sizeRange) {
|
|
20
|
+
return BUBBLE_SIZE_MIN + ((raw - minSize) / sizeRange) * (BUBBLE_SIZE_MAX - BUBBLE_SIZE_MIN);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Compute global min/max/range for size field across all data.
|
|
24
|
+
*/
|
|
25
|
+
function computeSizeRange(data, sizeField) {
|
|
26
|
+
const sizes = data.map((d) => (typeof d[sizeField] === 'number' ? d[sizeField] : 1));
|
|
27
|
+
const min = sizes.length > 0 ? Math.min(...sizes) : 1;
|
|
28
|
+
const max = sizes.length > 0 ? Math.max(...sizes) : 1;
|
|
29
|
+
return { min, range: max - min || 1 };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Detect whether a field contains category (string) data and extract unique values.
|
|
33
|
+
*/
|
|
34
|
+
function detectAxisCategories(data, field) {
|
|
35
|
+
const first = data.length > 0 ? data[0][field] : null;
|
|
36
|
+
const isCategory = typeof first === 'string';
|
|
37
|
+
const categories = isCategory
|
|
38
|
+
? [...new Set(data.map((d) => String(d[field] ?? '')))]
|
|
39
|
+
: [];
|
|
40
|
+
return { isCategory, categories };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build a single bubble data point array: [x, y, rawSize, scaledSize, label?]
|
|
44
|
+
*/
|
|
45
|
+
function buildBubblePoint(d, ctx) {
|
|
46
|
+
const s = typeof d[ctx.sizeField] === 'number' ? d[ctx.sizeField] : 1;
|
|
47
|
+
const scaledSize = scaleBubbleSize(s, ctx.minSize, ctx.sizeRange);
|
|
48
|
+
const xVal = ctx.xIsCategory ? ctx.xCategories.indexOf(String(d[ctx.xField] ?? '')) : (d[ctx.xField] ?? 0);
|
|
49
|
+
const yVal = ctx.yIsCategory ? ctx.yCategories.indexOf(String(d[ctx.yField] ?? '')) : (d[ctx.yField] ?? 0);
|
|
50
|
+
const point = [xVal, yVal, s, scaledSize];
|
|
51
|
+
if (ctx.labelField)
|
|
52
|
+
point.push(String(d[ctx.labelField] ?? ''));
|
|
53
|
+
return point;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build series list and legend data for bubble chart.
|
|
57
|
+
*/
|
|
58
|
+
function buildBubbleSeries(data, ctx, seriesField, showLabels, palette, colors) {
|
|
59
|
+
const labelConfig = showLabels && ctx.labelField
|
|
60
|
+
? buildPointLabelConfig(BUBBLE_LABEL_INDEX, colors)
|
|
61
|
+
: undefined;
|
|
62
|
+
const pointBuilder = (d) => buildBubblePoint(d, ctx);
|
|
63
|
+
if (seriesField) {
|
|
64
|
+
const groups = groupDataBySeries(data, seriesField, pointBuilder);
|
|
65
|
+
const legendData = [...groups.keys()];
|
|
66
|
+
const seriesList = legendData.map((name, idx) => {
|
|
67
|
+
const obj = {
|
|
68
|
+
type: 'scatter',
|
|
69
|
+
name,
|
|
70
|
+
data: groups.get(name),
|
|
71
|
+
symbolSize: { _js_: 'function(val) { return val[3] || 20; }' },
|
|
72
|
+
itemStyle: { color: palette[idx % palette.length], opacity: BUBBLE_OPACITY },
|
|
73
|
+
};
|
|
74
|
+
if (labelConfig)
|
|
75
|
+
obj.label = labelConfig;
|
|
76
|
+
return obj;
|
|
77
|
+
});
|
|
78
|
+
return { seriesList, legendData };
|
|
79
|
+
}
|
|
80
|
+
const bubbleData = data.map(pointBuilder);
|
|
81
|
+
const obj = {
|
|
82
|
+
type: 'scatter',
|
|
83
|
+
data: bubbleData,
|
|
84
|
+
symbolSize: { _js_: 'function(val) { return val[3] || 20; }' },
|
|
85
|
+
itemStyle: { color: palette[0], opacity: BUBBLE_OPACITY },
|
|
86
|
+
};
|
|
87
|
+
if (labelConfig)
|
|
88
|
+
obj.label = labelConfig;
|
|
89
|
+
return { seriesList: [obj], legendData: [] };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build tooltip config for bubble chart.
|
|
93
|
+
*/
|
|
94
|
+
function buildBubbleTooltip(ctx, seriesField, xFormat, yFormat, currency, colors) {
|
|
95
|
+
const labelIdx = ctx.labelField ? BUBBLE_LABEL_INDEX : -1;
|
|
96
|
+
const xFormatExpr = formatValue(xFormat, currency.symbol, currency.locale);
|
|
97
|
+
const yFormatExpr = formatValue(yFormat, currency.symbol, currency.locale);
|
|
98
|
+
const seriesNameLine = seriesField
|
|
99
|
+
? "if (params.seriesName) result += '<strong>' + params.seriesName + '</strong><br/>';"
|
|
100
|
+
: '';
|
|
101
|
+
const formatterJs = `function(params) {
|
|
102
|
+
var d = params.value || params.data;
|
|
103
|
+
var result = '';
|
|
104
|
+
${seriesNameLine}
|
|
105
|
+
${labelIdx >= 0 ? "var label = d[" + labelIdx + "]; if (label) result += '<strong>' + label + '</strong><br/>';" : ''}
|
|
106
|
+
var value = d[0]; result += ${JSON.stringify(ctx.xField)} + ': ' + ${xFormatExpr} + '<br/>';
|
|
107
|
+
var value = d[1]; result += ${JSON.stringify(ctx.yField)} + ': ' + ${yFormatExpr} + '<br/>';
|
|
108
|
+
result += ${JSON.stringify(ctx.sizeField)} + ': ' + d[2];
|
|
109
|
+
return result;
|
|
110
|
+
}`;
|
|
111
|
+
return {
|
|
112
|
+
trigger: 'item',
|
|
113
|
+
backgroundColor: colors.paper,
|
|
114
|
+
borderColor: colors.border,
|
|
115
|
+
textStyle: { color: colors.text },
|
|
116
|
+
formatter: { _js_: formatterJs },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Build a single axis config for bubble chart.
|
|
121
|
+
*/
|
|
122
|
+
function buildBubbleAxis(field, isCategory, categories, formatter, nameGap, colors) {
|
|
123
|
+
return {
|
|
124
|
+
type: isCategory ? 'category' : 'value',
|
|
125
|
+
data: isCategory ? categories : undefined,
|
|
126
|
+
name: field,
|
|
127
|
+
nameLocation: 'middle',
|
|
128
|
+
nameGap,
|
|
129
|
+
nameTextStyle: { color: colors.textSecondary, fontSize: FONT_SIZE_TINY },
|
|
130
|
+
scale: !isCategory,
|
|
131
|
+
axisLine: { show: false },
|
|
132
|
+
axisTick: { show: false },
|
|
133
|
+
axisLabel: {
|
|
134
|
+
color: colors.textSecondary,
|
|
135
|
+
...(formatter && { formatter }),
|
|
136
|
+
},
|
|
137
|
+
splitLine: { lineStyle: { color: colors.border, type: [2, 3], opacity: 0.6 } },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Build ECharts options for a bubble chart.
|
|
9
142
|
*/
|
|
10
143
|
export function buildBubbleOptions(spec) {
|
|
11
144
|
const data = (spec.data ?? []);
|
|
12
145
|
const xField = spec.x ?? 'x';
|
|
13
146
|
const yField = spec.y ?? 'y';
|
|
14
147
|
const sizeField = spec.size ?? 'size';
|
|
148
|
+
const seriesField = spec.series;
|
|
15
149
|
const labelField = spec.label;
|
|
150
|
+
const showLabels = spec.showLabels;
|
|
16
151
|
const theme = (spec.theme ?? 'light');
|
|
17
152
|
const colors = getThemeColors(theme);
|
|
18
153
|
const palette = getPaletteWithCustom(theme, spec.customTheme);
|
|
19
|
-
//
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
// For category axes, convert string values to indices
|
|
40
|
-
const bubbleData = data.map((d) => {
|
|
41
|
-
const s = typeof d[sizeField] === 'number' ? d[sizeField] : 1;
|
|
42
|
-
const scaledSize = 10 + ((s - minSize) / sizeRange) * 40;
|
|
43
|
-
const xVal = xIsCategory ? xCategories.indexOf(String(d[xField] ?? '')) : (d[xField] ?? 0);
|
|
44
|
-
const yVal = yIsCategory ? yCategories.indexOf(String(d[yField] ?? '')) : (d[yField] ?? 0);
|
|
45
|
-
const point = [xVal, yVal, s, scaledSize];
|
|
46
|
-
if (labelField)
|
|
47
|
-
point.push(String(d[labelField] ?? ''));
|
|
48
|
-
return point;
|
|
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
|
-
}`;
|
|
61
|
-
return {
|
|
154
|
+
// Infer axis formats
|
|
155
|
+
const xSample = data.length > 0 && data[0] ? data[0][xField] ?? 0 : 0;
|
|
156
|
+
const ySample = data.length > 0 && data[0] ? data[0][yField] ?? 0 : 0;
|
|
157
|
+
const xFormat = spec.xFormat ?? inferFormat(xField, xSample);
|
|
158
|
+
const yFormat = spec.yFormat ?? inferFormat(yField, ySample);
|
|
159
|
+
const currency = resolveCurrency(spec.currency);
|
|
160
|
+
// Build context for point construction
|
|
161
|
+
const xAxis = detectAxisCategories(data, xField);
|
|
162
|
+
const yAxis = detectAxisCategories(data, yField);
|
|
163
|
+
const { min: minSize, range: sizeRange } = computeSizeRange(data, sizeField);
|
|
164
|
+
const ctx = {
|
|
165
|
+
xField, yField, sizeField, labelField,
|
|
166
|
+
xIsCategory: xAxis.isCategory, yIsCategory: yAxis.isCategory,
|
|
167
|
+
xCategories: xAxis.categories, yCategories: yAxis.categories,
|
|
168
|
+
minSize, sizeRange,
|
|
169
|
+
};
|
|
170
|
+
// Build series and legend
|
|
171
|
+
const { seriesList, legendData } = buildBubbleSeries(data, ctx, seriesField, showLabels, palette, colors);
|
|
172
|
+
const hasLegend = seriesField && legendData.length > 1;
|
|
173
|
+
const option = {
|
|
62
174
|
backgroundColor: 'transparent',
|
|
63
175
|
animation: false,
|
|
64
176
|
color: palette,
|
|
65
|
-
tooltip:
|
|
66
|
-
trigger: 'item',
|
|
67
|
-
backgroundColor: colors.paper,
|
|
68
|
-
borderColor: colors.border,
|
|
69
|
-
textStyle: { color: colors.text },
|
|
70
|
-
formatter: { _js_: tooltipFormatterJs },
|
|
71
|
-
},
|
|
177
|
+
tooltip: buildBubbleTooltip(ctx, seriesField, xFormat, yFormat, currency, colors),
|
|
72
178
|
grid: {
|
|
73
|
-
left: '6%',
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
bottom: '14%',
|
|
77
|
-
containLabel: true,
|
|
78
|
-
},
|
|
79
|
-
xAxis: {
|
|
80
|
-
type: xIsCategory ? 'category' : 'value',
|
|
81
|
-
data: xIsCategory ? xCategories : undefined,
|
|
82
|
-
name: xField,
|
|
83
|
-
nameLocation: 'middle',
|
|
84
|
-
nameGap: 24,
|
|
85
|
-
nameTextStyle: { color: colors.textSecondary, fontSize: FONT_SIZE_TINY },
|
|
86
|
-
scale: !xIsCategory,
|
|
87
|
-
axisLine: { show: false },
|
|
88
|
-
axisTick: { show: false },
|
|
89
|
-
axisLabel: { color: colors.textSecondary },
|
|
90
|
-
splitLine: { lineStyle: { color: colors.border, type: [2, 3], opacity: 0.6 } },
|
|
91
|
-
},
|
|
92
|
-
yAxis: {
|
|
93
|
-
type: yIsCategory ? 'category' : 'value',
|
|
94
|
-
data: yIsCategory ? yCategories : undefined,
|
|
95
|
-
name: yField,
|
|
96
|
-
nameLocation: 'middle',
|
|
97
|
-
nameGap: 32,
|
|
98
|
-
nameTextStyle: { color: colors.textSecondary, fontSize: FONT_SIZE_TINY },
|
|
99
|
-
scale: !yIsCategory,
|
|
100
|
-
axisLine: { show: false },
|
|
101
|
-
axisTick: { show: false },
|
|
102
|
-
axisLabel: { color: colors.textSecondary },
|
|
103
|
-
splitLine: { lineStyle: { color: colors.border, type: [2, 3], opacity: 0.6 } },
|
|
179
|
+
left: '6%', right: '8%',
|
|
180
|
+
top: hasLegend ? '14%' : '12%',
|
|
181
|
+
bottom: '14%', containLabel: true,
|
|
104
182
|
},
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
data: bubbleData,
|
|
109
|
-
symbolSize: { _js_: 'function(val) { return val[3] || 20; }' },
|
|
110
|
-
itemStyle: { color: palette[0], opacity: 0.55 },
|
|
111
|
-
},
|
|
112
|
-
],
|
|
183
|
+
xAxis: buildBubbleAxis(xField, xAxis.isCategory, xAxis.categories, getAxisFormatterJs(xFormat, currency.symbol, currency.locale), 24, colors),
|
|
184
|
+
yAxis: buildBubbleAxis(yField, yAxis.isCategory, yAxis.categories, getAxisFormatterJs(yFormat, currency.symbol, currency.locale), 32, colors),
|
|
185
|
+
series: seriesList,
|
|
113
186
|
};
|
|
187
|
+
if (hasLegend) {
|
|
188
|
+
option.legend = buildSeriesLegend(legendData, colors);
|
|
189
|
+
}
|
|
190
|
+
return option;
|
|
114
191
|
}
|
|
115
192
|
/**
|
|
116
|
-
* Generate a bubble chart
|
|
193
|
+
* Generate a bubble chart.
|
|
117
194
|
*/
|
|
118
195
|
function generateBubble(spec) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const theme = (spec.theme ?? 'light');
|
|
122
|
-
const colors = getThemeColors(theme);
|
|
123
|
-
const optionJson = serializeOption(option);
|
|
124
|
-
return `<!DOCTYPE html>
|
|
125
|
-
<html lang="en">
|
|
126
|
-
<head>
|
|
127
|
-
<meta charset="utf-8">
|
|
128
|
-
<title>${title}</title>
|
|
129
|
-
<script src="${ECHARTS_CDN}"></script>
|
|
130
|
-
<style>
|
|
131
|
-
* { box-sizing: border-box; }
|
|
132
|
-
html, body {
|
|
133
|
-
margin: 0; padding: 0; width: 100%; height: 100%;
|
|
134
|
-
background-color: ${colors.background}; font-family: ${FONT_STACK};
|
|
135
|
-
}
|
|
136
|
-
.container { padding: 20px; width: 100%; height: 100%; display: flex; flex-direction: column; }
|
|
137
|
-
.red-line { width: 100%; height: 3px; background-color: ${COLORS.ERROR_RED}; margin-bottom: 12px; }
|
|
138
|
-
h2 { font-family: ${FONT_STACK}; color: ${colors.text}; font-size: 18px; font-weight: 900; margin: 0 0 4px 0; }
|
|
139
|
-
#chart { width: 100%; flex: 1; min-height: 300px; }
|
|
140
|
-
</style>
|
|
141
|
-
</head>
|
|
142
|
-
<body>
|
|
143
|
-
<div class="container">
|
|
144
|
-
<div class="red-line"></div>
|
|
145
|
-
<h2>${title}</h2>
|
|
146
|
-
<div id="chart"></div>
|
|
147
|
-
</div>
|
|
148
|
-
<script>
|
|
149
|
-
var chart = echarts.init(document.getElementById('chart'));
|
|
150
|
-
var option = ${optionJson};
|
|
151
|
-
chart.setOption(option);
|
|
152
|
-
window.addEventListener('resize', function() { chart.resize(); });
|
|
153
|
-
</script>
|
|
154
|
-
</body>
|
|
155
|
-
</html>`;
|
|
196
|
+
const height = typeof spec.height === 'number' ? spec.height : DEFAULT_CHART_HEIGHT;
|
|
197
|
+
return wrapHtml('chart', buildBubbleOptions(spec), '', '100%', height);
|
|
156
198
|
}
|
|
157
199
|
// Register the chart
|
|
158
200
|
registerChart('bubble')(generateBubble);
|
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]];
|