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.
@@ -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 with JS function for symbol size
10
+ * Generate a bubble chart.
11
11
  */
12
12
  declare function generateBubble(spec: ChartSpec): string;
13
13
  export { generateBubble };
@@ -1,158 +1,200 @@
1
1
  /**
2
2
  * Bubble chart generator
3
3
  */
4
- import { FONT_SIZE_TINY, ECHARTS_CDN, FONT_STACK, getThemeColors, getPaletteWithCustom } from '../core/themes.js';
5
- import { serializeOption } from '../core/serializer.js';
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
- * Build ECharts options for a bubble chart
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
- // Auto-detect axis types from data
20
- const firstX = data.length > 0 ? data[0][xField] : null;
21
- const firstY = data.length > 0 ? data[0][yField] : null;
22
- const xIsCategory = typeof firstX === 'string';
23
- const yIsCategory = typeof firstY === 'string';
24
- // Extract unique categories if needed
25
- let xCategories = [];
26
- let yCategories = [];
27
- if (xIsCategory) {
28
- xCategories = [...new Set(data.map((d) => String(d[xField] ?? '')))];
29
- }
30
- if (yIsCategory) {
31
- yCategories = [...new Set(data.map((d) => String(d[yField] ?? '')))];
32
- }
33
- // Find min/max for size scaling
34
- const sizes = data.map((d) => (typeof d[sizeField] === 'number' ? d[sizeField] : 1));
35
- const minSize = sizes.length > 0 ? Math.min(...sizes) : 1;
36
- const maxSize = sizes.length > 0 ? Math.max(...sizes) : 1;
37
- const sizeRange = maxSize - minSize || 1;
38
- // Build bubble data with scaled sizes
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
- right: '8%',
75
- top: '12%',
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
- series: [
106
- {
107
- type: 'scatter',
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 with JS function for symbol size
193
+ * Generate a bubble chart.
117
194
  */
118
195
  function generateBubble(spec) {
119
- const option = buildBubbleOptions(spec);
120
- const title = spec.title ?? '';
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);
@@ -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
- // Group data by the series field
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
- type: 'scatter',
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
- data: scatterData,
57
+ name,
58
+ data: groups.get(name),
66
59
  symbolSize: SCATTER_SYMBOL_SIZE,
67
- itemStyle: { color: palette[0], opacity: 0.75 },
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
  }
@@ -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 heatmapColors = colDef.heatmapColors ?? defaultHeatmapColors;
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
@@ -6,4 +6,5 @@ export * from './formatting.js';
6
6
  export * from './colors.js';
7
7
  export * from './exceptions.js';
8
8
  export * from './serializer.js';
9
+ export * from './chart-helpers.js';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -6,4 +6,5 @@ export * from './formatting.js';
6
6
  export * from './colors.js';
7
7
  export * from './exceptions.js';
8
8
  export * from './serializer.js';
9
+ export * from './chart-helpers.js';
9
10
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mviz",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "A chart & report builder designed for use by AI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
  },