mviz 1.6.0 → 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/bubble.d.ts +2 -2
- package/dist/charts/bubble.js +172 -130
- package/dist/charts/scatter.js +44 -41
- package/dist/components/table.js +4 -1
- package/dist/core/chart-helpers.d.ts +21 -0
- package/dist/core/chart-helpers.js +46 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/package.json +1 -1
- package/schema/mviz.v1.schema.json +12 -0
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 { FONT_SIZE_TINY,
|
|
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.accent}; 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/scatter.js
CHANGED
|
@@ -5,6 +5,20 @@ import { SCATTER_SYMBOL_SIZE, FONT_SIZE_TINY, DEFAULT_CHART_HEIGHT, getThemeColo
|
|
|
5
5
|
import { wrapHtml } from '../core/serializer.js';
|
|
6
6
|
import { registerChart, registerOptions } from './registry.js';
|
|
7
7
|
import { inferFormat, getAxisFormatterJs, formatValue, resolveCurrency } from '../core/formatting.js';
|
|
8
|
+
import { buildPointLabelConfig, groupDataBySeries, buildSeriesLegend } from '../core/chart-helpers.js';
|
|
9
|
+
/** Scatter point opacity */
|
|
10
|
+
const SCATTER_OPACITY = 0.75;
|
|
11
|
+
/** Label sits at index 2 in scatter point arrays: [x, y, label?] */
|
|
12
|
+
const SCATTER_LABEL_INDEX = 2;
|
|
13
|
+
/**
|
|
14
|
+
* Build a scatter data point: [x, y, label?]
|
|
15
|
+
*/
|
|
16
|
+
function buildScatterPoint(d, xField, yField, labelField) {
|
|
17
|
+
const point = [d[xField] ?? 0, d[yField] ?? 0];
|
|
18
|
+
if (labelField)
|
|
19
|
+
point.push(String(d[labelField] ?? ''));
|
|
20
|
+
return point;
|
|
21
|
+
}
|
|
8
22
|
/**
|
|
9
23
|
* Build ECharts options for a scatter plot
|
|
10
24
|
*/
|
|
@@ -14,6 +28,7 @@ export function buildScatterOptions(spec) {
|
|
|
14
28
|
const yField = spec.y ?? 'y';
|
|
15
29
|
const seriesField = spec.series;
|
|
16
30
|
const labelField = spec.label;
|
|
31
|
+
const showLabels = spec.showLabels;
|
|
17
32
|
const theme = (spec.theme ?? 'light');
|
|
18
33
|
const colors = getThemeColors(theme);
|
|
19
34
|
const palette = getPaletteWithCustom(theme, spec.customTheme);
|
|
@@ -25,48 +40,41 @@ export function buildScatterOptions(spec) {
|
|
|
25
40
|
const currency = resolveCurrency(spec.currency);
|
|
26
41
|
const xAxisFormatter = getAxisFormatterJs(xFormat, currency.symbol, currency.locale);
|
|
27
42
|
const yAxisFormatter = getAxisFormatterJs(yFormat, currency.symbol, currency.locale);
|
|
43
|
+
// Label config for persistent point labels
|
|
44
|
+
const labelConfig = showLabels && labelField
|
|
45
|
+
? buildPointLabelConfig(SCATTER_LABEL_INDEX, colors)
|
|
46
|
+
: undefined;
|
|
28
47
|
// Build series - either single series or grouped by seriesField
|
|
29
48
|
let seriesList;
|
|
30
49
|
let legendData = [];
|
|
50
|
+
const pointBuilder = (d) => buildScatterPoint(d, xField, yField, labelField);
|
|
31
51
|
if (seriesField) {
|
|
32
|
-
|
|
33
|
-
const groups = new Map();
|
|
34
|
-
for (const d of data) {
|
|
35
|
-
const groupName = String(d[seriesField] ?? 'Unknown');
|
|
36
|
-
if (!groups.has(groupName)) {
|
|
37
|
-
groups.set(groupName, []);
|
|
38
|
-
}
|
|
39
|
-
const point = [d[xField] ?? 0, d[yField] ?? 0];
|
|
40
|
-
if (labelField)
|
|
41
|
-
point.push(String(d[labelField] ?? ''));
|
|
42
|
-
groups.get(groupName).push(point);
|
|
43
|
-
}
|
|
44
|
-
// Create a series for each group
|
|
52
|
+
const groups = groupDataBySeries(data, seriesField, pointBuilder);
|
|
45
53
|
legendData = [...groups.keys()];
|
|
46
|
-
seriesList = legendData.map((name, idx) =>
|
|
47
|
-
|
|
48
|
-
name,
|
|
49
|
-
data: groups.get(name),
|
|
50
|
-
symbolSize: SCATTER_SYMBOL_SIZE,
|
|
51
|
-
itemStyle: { color: palette[idx % palette.length], opacity: 0.75 },
|
|
52
|
-
}));
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
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
|
-
});
|
|
62
|
-
seriesList = [
|
|
63
|
-
{
|
|
54
|
+
seriesList = legendData.map((name, idx) => {
|
|
55
|
+
const seriesObj = {
|
|
64
56
|
type: 'scatter',
|
|
65
|
-
|
|
57
|
+
name,
|
|
58
|
+
data: groups.get(name),
|
|
66
59
|
symbolSize: SCATTER_SYMBOL_SIZE,
|
|
67
|
-
itemStyle: { color: palette[
|
|
68
|
-
}
|
|
69
|
-
|
|
60
|
+
itemStyle: { color: palette[idx % palette.length], opacity: SCATTER_OPACITY },
|
|
61
|
+
};
|
|
62
|
+
if (labelConfig)
|
|
63
|
+
seriesObj.label = labelConfig;
|
|
64
|
+
return seriesObj;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const scatterData = data.map(pointBuilder);
|
|
69
|
+
const seriesObj = {
|
|
70
|
+
type: 'scatter',
|
|
71
|
+
data: scatterData,
|
|
72
|
+
symbolSize: SCATTER_SYMBOL_SIZE,
|
|
73
|
+
itemStyle: { color: palette[0], opacity: SCATTER_OPACITY },
|
|
74
|
+
};
|
|
75
|
+
if (labelConfig)
|
|
76
|
+
seriesObj.label = labelConfig;
|
|
77
|
+
seriesList = [seriesObj];
|
|
70
78
|
}
|
|
71
79
|
// Build tooltip formatter with label support and value formatting
|
|
72
80
|
const xFormatExpr = formatValue(xFormat, currency.symbol, currency.locale);
|
|
@@ -129,12 +137,7 @@ export function buildScatterOptions(spec) {
|
|
|
129
137
|
};
|
|
130
138
|
// Add legend for multi-series
|
|
131
139
|
if (seriesField && legendData.length > 1) {
|
|
132
|
-
option.legend =
|
|
133
|
-
show: true,
|
|
134
|
-
data: legendData,
|
|
135
|
-
top: 0,
|
|
136
|
-
textStyle: { color: colors.textSecondary, fontSize: FONT_SIZE_TINY },
|
|
137
|
-
};
|
|
140
|
+
option.legend = buildSeriesLegend(legendData, colors);
|
|
138
141
|
}
|
|
139
142
|
return option;
|
|
140
143
|
}
|
package/dist/components/table.js
CHANGED
|
@@ -350,7 +350,10 @@ export function formatCell(value, colDef, themeColors, heatmapRanges, dumbbellRa
|
|
|
350
350
|
if (colType === 'heatmap') {
|
|
351
351
|
const range = heatmapRanges.get(colId);
|
|
352
352
|
if (typeof rawValue === 'number' && range) {
|
|
353
|
-
const
|
|
353
|
+
const baseColors = colDef.heatmapColors ?? defaultHeatmapColors;
|
|
354
|
+
const heatmapColors = colDef.higherIsBetter === false
|
|
355
|
+
? [...baseColors].reverse()
|
|
356
|
+
: baseColors;
|
|
354
357
|
const colorResult = calculateHeatmapColorWithContrast(rawValue, range.min, range.max, heatmapColors);
|
|
355
358
|
bgColor = colorResult.bgColor;
|
|
356
359
|
textColor = colorResult.textColor;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for scatter-family charts (scatter, bubble).
|
|
3
|
+
*
|
|
4
|
+
* Consolidates series-grouping, label config, and legend patterns
|
|
5
|
+
* that were duplicated across scatter.ts and bubble.ts.
|
|
6
|
+
*/
|
|
7
|
+
import type { DataPoint, ThemeColors } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Build ECharts label config for persistent point labels.
|
|
10
|
+
* @param labelIndex - Index of the label value in the point array
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildPointLabelConfig(labelIndex: number, colors: ThemeColors): Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Group data points by a series field, applying a point builder to each.
|
|
15
|
+
*/
|
|
16
|
+
export declare function groupDataBySeries<T>(data: DataPoint[], seriesField: string, buildPoint: (d: DataPoint) => T): Map<string, T[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Build ECharts legend config for multi-series charts.
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildSeriesLegend(legendData: string[], colors: ThemeColors): Record<string, unknown>;
|
|
21
|
+
//# sourceMappingURL=chart-helpers.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for scatter-family charts (scatter, bubble).
|
|
3
|
+
*
|
|
4
|
+
* Consolidates series-grouping, label config, and legend patterns
|
|
5
|
+
* that were duplicated across scatter.ts and bubble.ts.
|
|
6
|
+
*/
|
|
7
|
+
import { FONT_SIZE_TINY } from './themes.js';
|
|
8
|
+
/**
|
|
9
|
+
* Build ECharts label config for persistent point labels.
|
|
10
|
+
* @param labelIndex - Index of the label value in the point array
|
|
11
|
+
*/
|
|
12
|
+
export function buildPointLabelConfig(labelIndex, colors) {
|
|
13
|
+
return {
|
|
14
|
+
show: true,
|
|
15
|
+
formatter: { _js_: `function(params) { return params.value[${labelIndex}] || ""; }` },
|
|
16
|
+
position: 'right',
|
|
17
|
+
fontSize: FONT_SIZE_TINY,
|
|
18
|
+
color: colors.textSecondary,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Group data points by a series field, applying a point builder to each.
|
|
23
|
+
*/
|
|
24
|
+
export function groupDataBySeries(data, seriesField, buildPoint) {
|
|
25
|
+
const groups = new Map();
|
|
26
|
+
for (const d of data) {
|
|
27
|
+
const groupName = String(d[seriesField] ?? 'Unknown');
|
|
28
|
+
if (!groups.has(groupName)) {
|
|
29
|
+
groups.set(groupName, []);
|
|
30
|
+
}
|
|
31
|
+
groups.get(groupName).push(buildPoint(d));
|
|
32
|
+
}
|
|
33
|
+
return groups;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build ECharts legend config for multi-series charts.
|
|
37
|
+
*/
|
|
38
|
+
export function buildSeriesLegend(legendData, colors) {
|
|
39
|
+
return {
|
|
40
|
+
show: true,
|
|
41
|
+
data: legendData,
|
|
42
|
+
top: 0,
|
|
43
|
+
textStyle: { color: colors.textSecondary, fontSize: FONT_SIZE_TINY },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=chart-helpers.js.map
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
package/package.json
CHANGED
|
@@ -415,6 +415,10 @@
|
|
|
415
415
|
"type": "string",
|
|
416
416
|
"description": "Field name for point labels shown in tooltips"
|
|
417
417
|
},
|
|
418
|
+
"showLabels": {
|
|
419
|
+
"type": "boolean",
|
|
420
|
+
"description": "Show persistent labels next to points (requires label field)"
|
|
421
|
+
},
|
|
418
422
|
"title": {
|
|
419
423
|
"type": "string"
|
|
420
424
|
},
|
|
@@ -461,10 +465,18 @@
|
|
|
461
465
|
"size": {
|
|
462
466
|
"type": "string"
|
|
463
467
|
},
|
|
468
|
+
"series": {
|
|
469
|
+
"type": "string",
|
|
470
|
+
"description": "Field name to group data points by (creates multiple colored series)"
|
|
471
|
+
},
|
|
464
472
|
"label": {
|
|
465
473
|
"type": "string",
|
|
466
474
|
"description": "Field name for point labels shown in tooltips"
|
|
467
475
|
},
|
|
476
|
+
"showLabels": {
|
|
477
|
+
"type": "boolean",
|
|
478
|
+
"description": "Show persistent labels next to points (requires label field)"
|
|
479
|
+
},
|
|
468
480
|
"title": {
|
|
469
481
|
"type": "string"
|
|
470
482
|
},
|