eleventy-plugin-uncharted 1.0.0-beta.1 → 1.0.0-beta.2
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/eleventy.config.js +11 -0
- package/package.json +1 -1
- package/src/columns.js +8 -64
- package/src/config.js +21 -119
- package/src/deprecation.js +111 -0
- package/src/renderers/bubble.js +7 -6
- package/src/renderers/donut.js +3 -4
- package/src/renderers/index.js +1 -3
- package/src/renderers/line.js +215 -3
- package/src/renderers/scatter.js +6 -5
- package/src/renderers/stacked-bar.js +4 -5
- package/src/renderers/stacked-column.js +5 -6
- package/src/renderers/timeseries.js +4 -4
- package/src/renderers/dot.js +0 -222
package/eleventy.config.js
CHANGED
|
@@ -4,6 +4,7 @@ import { renderers } from './src/renderers/index.js';
|
|
|
4
4
|
import { loadCSV } from './src/csv.js';
|
|
5
5
|
import { normalizeConfig } from './src/config.js';
|
|
6
6
|
import { resolveColumns } from './src/columns.js';
|
|
7
|
+
import { validateChartType, checkDeprecatedOptions } from './src/deprecation.js';
|
|
7
8
|
import {
|
|
8
9
|
normalizeImageOptions,
|
|
9
10
|
queueChartForImage,
|
|
@@ -145,11 +146,21 @@ export default function(eleventyConfig, options = {}) {
|
|
|
145
146
|
return `<!-- Chart "${chartId}" has no type specified -->`;
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
// Check for deprecated chart type (ERROR - prevents rendering)
|
|
150
|
+
const typeError = validateChartType(chartType, chartId);
|
|
151
|
+
if (typeError) {
|
|
152
|
+
console.error(`[uncharted] ${typeError}`);
|
|
153
|
+
return `<!-- ${typeError} -->`;
|
|
154
|
+
}
|
|
155
|
+
|
|
148
156
|
const renderer = renderers[chartType];
|
|
149
157
|
if (!renderer) {
|
|
150
158
|
return `<!-- Unknown chart type "${chartType}" for chart "${chartId}" -->`;
|
|
151
159
|
}
|
|
152
160
|
|
|
161
|
+
// Check for deprecated config options (WARNING - chart renders but option ignored)
|
|
162
|
+
checkDeprecatedOptions(chartConfig, chartId);
|
|
163
|
+
|
|
153
164
|
// Load data from CSV file or use inline data
|
|
154
165
|
let data = chartConfig.data;
|
|
155
166
|
if (chartConfig.file && !data) {
|
package/package.json
CHANGED
package/src/columns.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Column resolution for Uncharted
|
|
3
3
|
* Handles explicit column mapping and implicit detection
|
|
4
|
-
* Supports both new unified schema (x.column, y.columns) and deprecated columns key
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { parseAxisConfig } from './config.js';
|
|
@@ -9,13 +8,10 @@ import { parseAxisConfig } from './config.js';
|
|
|
9
8
|
/**
|
|
10
9
|
* Resolve column mappings for chart data
|
|
11
10
|
*
|
|
12
|
-
* New unified schema
|
|
11
|
+
* New unified schema:
|
|
13
12
|
* - x: { column: "month" } or x: "month"
|
|
14
13
|
* - y: { columns: ["a", "b"] } or y: { columns: { a: "Label A" } }
|
|
15
14
|
*
|
|
16
|
-
* Deprecated schema (fallback):
|
|
17
|
-
* - columns: { x: "month", y: ["a", "b"] }
|
|
18
|
-
*
|
|
19
15
|
* @param {Object} config - Chart configuration with optional columns mapping
|
|
20
16
|
* @param {Object[]} data - Chart data array
|
|
21
17
|
* @param {string} chartType - Chart type for context-aware defaults
|
|
@@ -42,7 +38,6 @@ export function resolveColumns(config, data, chartType) {
|
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
const keys = Object.keys(data[0]);
|
|
45
|
-
const deprecatedColumns = config.columns || {};
|
|
46
41
|
|
|
47
42
|
// Helper to find column by name (case-insensitive)
|
|
48
43
|
const findKey = name => keys.find(k => k.toLowerCase() === name.toLowerCase()) || null;
|
|
@@ -100,40 +95,28 @@ export function resolveColumns(config, data, chartType) {
|
|
|
100
95
|
// Chart-type specific resolution
|
|
101
96
|
if (chartType === 'bubble') {
|
|
102
97
|
// Bubble charts use x (categorical), y, series, size columns
|
|
103
|
-
//
|
|
104
|
-
// New schema: x.column, y.column, series.column, size.column
|
|
105
|
-
// Deprecated: columns.x, columns.y, columns.series, columns.size
|
|
98
|
+
// Schema: x.column, y.column, series.column, size.column
|
|
106
99
|
|
|
107
100
|
// X column (categorical)
|
|
108
101
|
if (xConfig?.columns?.length) {
|
|
109
102
|
resolved.x = validateColumn('x.column', xConfig.columns[0]);
|
|
110
|
-
} else if (deprecatedColumns.x) {
|
|
111
|
-
resolved.x = validateColumn('columns.x', deprecatedColumns.x);
|
|
112
103
|
}
|
|
113
104
|
|
|
114
105
|
// Y column
|
|
115
106
|
if (yConfig?.columns?.length) {
|
|
116
107
|
resolved.y = validateColumn('y.column', yConfig.columns[0]);
|
|
117
|
-
} else if (deprecatedColumns.y) {
|
|
118
|
-
resolved.y = validateColumn('columns.y', deprecatedColumns.y);
|
|
119
108
|
}
|
|
120
109
|
|
|
121
110
|
// Series column (for coloring)
|
|
122
111
|
if (seriesConfig?.columns?.length) {
|
|
123
112
|
resolved.series = validateColumn('series.column', seriesConfig.columns[0]);
|
|
124
113
|
resolved.seriesTitle = seriesConfig.title;
|
|
125
|
-
} else if (deprecatedColumns.series) {
|
|
126
|
-
resolved.series = validateColumn('columns.series', deprecatedColumns.series);
|
|
127
|
-
resolved.seriesTitle = config.legendTitle; // deprecated
|
|
128
114
|
}
|
|
129
115
|
|
|
130
116
|
// Size column
|
|
131
117
|
if (sizeConfig?.columns?.length) {
|
|
132
118
|
resolved.size = validateColumn('size.column', sizeConfig.columns[0]);
|
|
133
119
|
resolved.sizeTitle = sizeConfig.title;
|
|
134
|
-
} else if (deprecatedColumns.size) {
|
|
135
|
-
resolved.size = validateColumn('columns.size', deprecatedColumns.size);
|
|
136
|
-
resolved.sizeTitle = config.sizeTitle; // deprecated
|
|
137
120
|
}
|
|
138
121
|
|
|
139
122
|
// Implicit detection for bubble if not explicitly specified
|
|
@@ -168,28 +151,21 @@ export function resolveColumns(config, data, chartType) {
|
|
|
168
151
|
|
|
169
152
|
} else if (chartType === 'scatter') {
|
|
170
153
|
// Scatter charts use x, y, label, series, size columns
|
|
171
|
-
//
|
|
172
|
-
// Deprecated: columns.x, columns.y, columns.label, columns.series, columns.size
|
|
154
|
+
// Schema: x.column, y.column, label.column, series.column, size.column
|
|
173
155
|
|
|
174
156
|
// X column
|
|
175
157
|
if (xConfig?.columns?.length) {
|
|
176
158
|
resolved.x = validateColumn('x.column', xConfig.columns[0]);
|
|
177
|
-
} else if (deprecatedColumns.x) {
|
|
178
|
-
resolved.x = validateColumn('columns.x', deprecatedColumns.x);
|
|
179
159
|
}
|
|
180
160
|
|
|
181
161
|
// Y column
|
|
182
162
|
if (yConfig?.columns?.length) {
|
|
183
163
|
resolved.y = validateColumn('y.column', yConfig.columns[0]);
|
|
184
|
-
} else if (deprecatedColumns.y) {
|
|
185
|
-
resolved.y = validateColumn('columns.y', deprecatedColumns.y);
|
|
186
164
|
}
|
|
187
165
|
|
|
188
166
|
// Label column (point identifier)
|
|
189
167
|
if (labelConfig?.columns?.length) {
|
|
190
168
|
resolved.label = validateColumn('label.column', labelConfig.columns[0]);
|
|
191
|
-
} else if (deprecatedColumns.label) {
|
|
192
|
-
resolved.label = validateColumn('columns.label', deprecatedColumns.label);
|
|
193
169
|
} else {
|
|
194
170
|
resolved.label = keys[0];
|
|
195
171
|
}
|
|
@@ -198,18 +174,12 @@ export function resolveColumns(config, data, chartType) {
|
|
|
198
174
|
if (seriesConfig?.columns?.length) {
|
|
199
175
|
resolved.series = validateColumn('series.column', seriesConfig.columns[0]);
|
|
200
176
|
resolved.seriesTitle = seriesConfig.title;
|
|
201
|
-
} else if (deprecatedColumns.series) {
|
|
202
|
-
resolved.series = validateColumn('columns.series', deprecatedColumns.series);
|
|
203
|
-
resolved.seriesTitle = config.legendTitle; // deprecated
|
|
204
177
|
}
|
|
205
178
|
|
|
206
179
|
// Size column
|
|
207
180
|
if (sizeConfig?.columns?.length) {
|
|
208
181
|
resolved.size = validateColumn('size.column', sizeConfig.columns[0]);
|
|
209
182
|
resolved.sizeTitle = sizeConfig.title;
|
|
210
|
-
} else if (deprecatedColumns.size) {
|
|
211
|
-
resolved.size = validateColumn('columns.size', deprecatedColumns.size);
|
|
212
|
-
resolved.sizeTitle = config.sizeTitle; // deprecated
|
|
213
183
|
}
|
|
214
184
|
|
|
215
185
|
// Implicit detection for scatter if not explicitly specified
|
|
@@ -236,21 +206,16 @@ export function resolveColumns(config, data, chartType) {
|
|
|
236
206
|
|
|
237
207
|
} else if (chartType === 'sankey') {
|
|
238
208
|
// Sankey charts use source, target, value columns
|
|
239
|
-
//
|
|
240
|
-
// Deprecated: columns.source, columns.target, columns.value
|
|
209
|
+
// Schema: source.column, target.column, value.column
|
|
241
210
|
|
|
242
211
|
if (sourceConfig?.columns?.length) {
|
|
243
212
|
resolved.source = validateColumn('source.column', sourceConfig.columns[0]);
|
|
244
|
-
} else if (deprecatedColumns.source) {
|
|
245
|
-
resolved.source = validateColumn('columns.source', deprecatedColumns.source);
|
|
246
213
|
} else {
|
|
247
214
|
resolved.source = keys[0];
|
|
248
215
|
}
|
|
249
216
|
|
|
250
217
|
if (targetConfig?.columns?.length) {
|
|
251
218
|
resolved.target = validateColumn('target.column', targetConfig.columns[0]);
|
|
252
|
-
} else if (deprecatedColumns.target) {
|
|
253
|
-
resolved.target = validateColumn('columns.target', deprecatedColumns.target);
|
|
254
219
|
} else {
|
|
255
220
|
resolved.target = keys[1];
|
|
256
221
|
}
|
|
@@ -258,22 +223,17 @@ export function resolveColumns(config, data, chartType) {
|
|
|
258
223
|
if (valueConfig?.columns?.length) {
|
|
259
224
|
resolved.value = validateColumn('value.column', valueConfig.columns[0]);
|
|
260
225
|
resolved.valueFormat = valueConfig.format;
|
|
261
|
-
} else if (deprecatedColumns.value) {
|
|
262
|
-
resolved.value = validateColumn('columns.value', deprecatedColumns.value);
|
|
263
226
|
} else {
|
|
264
227
|
resolved.value = keys[2];
|
|
265
228
|
}
|
|
266
229
|
|
|
267
230
|
} else if (chartType === 'donut') {
|
|
268
231
|
// Donut charts use label, value columns
|
|
269
|
-
//
|
|
270
|
-
// Deprecated: columns.label, columns.value (or implicit first/second columns)
|
|
232
|
+
// Schema: label.column, value.column
|
|
271
233
|
|
|
272
234
|
if (labelConfig?.columns?.length) {
|
|
273
235
|
resolved.label = validateColumn('label.column', labelConfig.columns[0]);
|
|
274
236
|
resolved.yLabels = labelConfig.labels || {};
|
|
275
|
-
} else if (deprecatedColumns.label) {
|
|
276
|
-
resolved.label = validateColumn('columns.label', deprecatedColumns.label);
|
|
277
237
|
} else {
|
|
278
238
|
resolved.label = keys[0];
|
|
279
239
|
}
|
|
@@ -284,9 +244,6 @@ export function resolveColumns(config, data, chartType) {
|
|
|
284
244
|
resolved.values = [resolved.values];
|
|
285
245
|
}
|
|
286
246
|
resolved.valueFormat = valueConfig.format;
|
|
287
|
-
} else if (deprecatedColumns.value) {
|
|
288
|
-
const val = validateColumn('columns.value', deprecatedColumns.value);
|
|
289
|
-
resolved.values = val ? [val] : [];
|
|
290
247
|
} else {
|
|
291
248
|
// Default: all columns except label
|
|
292
249
|
resolved.values = keys.filter(k => k !== resolved.label);
|
|
@@ -294,14 +251,11 @@ export function resolveColumns(config, data, chartType) {
|
|
|
294
251
|
|
|
295
252
|
} else if (chartType === 'stacked-bar') {
|
|
296
253
|
// Stacked bar: y = categories (left side), x = value series (bars extend right)
|
|
297
|
-
//
|
|
298
|
-
// Deprecated: columns.y (or label), columns.x (or y as values)
|
|
254
|
+
// Schema: y.column (categories), x.columns (values with labels)
|
|
299
255
|
|
|
300
256
|
// Category column (on Y axis for stacked-bar)
|
|
301
257
|
if (yConfig?.columns?.length) {
|
|
302
258
|
resolved.label = validateColumn('y.column', yConfig.columns[0]);
|
|
303
|
-
} else if (deprecatedColumns.label) {
|
|
304
|
-
resolved.label = validateColumn('columns.label', deprecatedColumns.label);
|
|
305
259
|
} else {
|
|
306
260
|
resolved.label = keys[0];
|
|
307
261
|
}
|
|
@@ -310,25 +264,18 @@ export function resolveColumns(config, data, chartType) {
|
|
|
310
264
|
if (xConfig?.columns?.length) {
|
|
311
265
|
resolved.values = validateColumn('x.columns', xConfig.columns) || [];
|
|
312
266
|
resolved.xLabels = xConfig.labels || {};
|
|
313
|
-
} else if (deprecatedColumns.y) {
|
|
314
|
-
// Deprecated: columns.y was used for value columns in stacked-bar
|
|
315
|
-
const explicitY = validateColumn('columns.y', deprecatedColumns.y);
|
|
316
|
-
resolved.values = Array.isArray(explicitY) ? explicitY : (explicitY ? [explicitY] : []);
|
|
317
267
|
} else {
|
|
318
268
|
// Implicit: all columns except label are values
|
|
319
269
|
resolved.values = keys.filter(k => k !== resolved.label);
|
|
320
270
|
}
|
|
321
271
|
|
|
322
272
|
} else {
|
|
323
|
-
// Standard charts (
|
|
324
|
-
//
|
|
325
|
-
// Deprecated: columns.label (or implicit first), columns.y
|
|
273
|
+
// Standard charts (line, stacked-column): x = categories, y = multi-series values
|
|
274
|
+
// Schema: x.column (categories), y.columns (values with labels)
|
|
326
275
|
|
|
327
276
|
// Label/category column (X axis)
|
|
328
277
|
if (xConfig?.columns?.length) {
|
|
329
278
|
resolved.label = validateColumn('x.column', xConfig.columns[0]);
|
|
330
|
-
} else if (deprecatedColumns.label) {
|
|
331
|
-
resolved.label = validateColumn('columns.label', deprecatedColumns.label);
|
|
332
279
|
} else {
|
|
333
280
|
resolved.label = keys[0];
|
|
334
281
|
}
|
|
@@ -337,9 +284,6 @@ export function resolveColumns(config, data, chartType) {
|
|
|
337
284
|
if (yConfig?.columns?.length) {
|
|
338
285
|
resolved.values = validateColumn('y.columns', yConfig.columns) || [];
|
|
339
286
|
resolved.yLabels = yConfig.labels || {};
|
|
340
|
-
} else if (deprecatedColumns.y) {
|
|
341
|
-
const explicitY = validateColumn('columns.y', deprecatedColumns.y);
|
|
342
|
-
resolved.values = Array.isArray(explicitY) ? explicitY : (explicitY ? [explicitY] : []);
|
|
343
287
|
} else {
|
|
344
288
|
// Implicit: all columns except label are values
|
|
345
289
|
resolved.values = keys.filter(k => k !== resolved.label);
|
package/src/config.js
CHANGED
|
@@ -3,58 +3,22 @@
|
|
|
3
3
|
* Transforms various config formats into a standardized structure
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
// Deprecated keys mapping for deprecation warnings
|
|
7
|
-
const DEPRECATED_KEYS = {
|
|
8
|
-
maxX: 'x.max',
|
|
9
|
-
minX: 'x.min',
|
|
10
|
-
maxY: 'y.max',
|
|
11
|
-
minY: 'y.min',
|
|
12
|
-
titleX: 'x.title',
|
|
13
|
-
titleY: 'y.title',
|
|
14
|
-
legendTitle: 'series.title',
|
|
15
|
-
sizeTitle: 'size.title',
|
|
16
|
-
rotateLabels: 'x.rotateLabels'
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// Track which deprecation warnings have been shown to avoid spam
|
|
20
|
-
const warnedConfigs = new Set();
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Log a deprecation warning (once per config/key combination)
|
|
24
|
-
* @param {string} chartId - Chart identifier
|
|
25
|
-
* @param {string} oldKey - Deprecated key name
|
|
26
|
-
* @param {string} newKey - New key path
|
|
27
|
-
*/
|
|
28
|
-
function warnDeprecation(chartId, oldKey, newKey) {
|
|
29
|
-
const key = `${chartId}:${oldKey}`;
|
|
30
|
-
if (warnedConfigs.has(key)) return;
|
|
31
|
-
warnedConfigs.add(key);
|
|
32
|
-
console.warn(`[uncharted] Chart "${chartId}": "${oldKey}" is deprecated, use "${newKey}" instead`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
6
|
/**
|
|
36
7
|
* Normalize chart configuration to standardized structure
|
|
37
8
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* Format precedence:
|
|
44
|
-
* 1. x.format / y.format (new nested format)
|
|
45
|
-
* 2. format.x / format.y (deprecated nested format)
|
|
46
|
-
* 3. format (global fallback)
|
|
9
|
+
* Axis properties:
|
|
10
|
+
* - x.max / y.max, x.min / y.min
|
|
11
|
+
* - x.title / y.title
|
|
12
|
+
* - x.format / y.format
|
|
47
13
|
*
|
|
48
|
-
* Column mapping
|
|
49
|
-
*
|
|
50
|
-
* 2. columns.x / columns.y (deprecated format)
|
|
14
|
+
* Column mapping:
|
|
15
|
+
* - x.column / x.columns / y.column / y.columns
|
|
51
16
|
*
|
|
52
|
-
* Legend
|
|
53
|
-
*
|
|
54
|
-
* 2. legend: ["Label1", "Label2"] (deprecated)
|
|
17
|
+
* Legend:
|
|
18
|
+
* - y.columns: { key: "Label" } or x.columns (for stacked-bar)
|
|
55
19
|
*
|
|
56
20
|
* @param {Object} config - Raw chart configuration
|
|
57
|
-
* @param {string} [chartId] - Chart ID for
|
|
21
|
+
* @param {string} [chartId] - Chart ID (unused, kept for API compatibility)
|
|
58
22
|
* @returns {Object} - Normalized configuration
|
|
59
23
|
*/
|
|
60
24
|
export function normalizeConfig(config, chartId = 'unknown') {
|
|
@@ -63,26 +27,10 @@ export function normalizeConfig(config, chartId = 'unknown') {
|
|
|
63
27
|
const normalized = { ...config };
|
|
64
28
|
|
|
65
29
|
// Build x axis config
|
|
66
|
-
normalized.x = buildAxisConfig('x', config
|
|
30
|
+
normalized.x = buildAxisConfig('x', config);
|
|
67
31
|
|
|
68
32
|
// Build y axis config
|
|
69
|
-
normalized.y = buildAxisConfig('y', config
|
|
70
|
-
|
|
71
|
-
// Warn for deprecated legend array (when used for labels, not boolean)
|
|
72
|
-
if (Array.isArray(config.legend)) {
|
|
73
|
-
warnDeprecation(chartId, 'legend (array)', 'y.columns: { key: "Label" }');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Warn for deprecated scatter-specific keys
|
|
77
|
-
if (config.legendTitle !== undefined) {
|
|
78
|
-
warnDeprecation(chartId, 'legendTitle', 'series.title');
|
|
79
|
-
}
|
|
80
|
-
if (config.sizeTitle !== undefined) {
|
|
81
|
-
warnDeprecation(chartId, 'sizeTitle', 'size.title');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Clean up deprecated top-level keys (keep them for backwards compat but don't pass to renderers)
|
|
85
|
-
// The renderers will use normalized.x and normalized.y instead
|
|
33
|
+
normalized.y = buildAxisConfig('y', config);
|
|
86
34
|
|
|
87
35
|
return normalized;
|
|
88
36
|
}
|
|
@@ -91,58 +39,22 @@ export function normalizeConfig(config, chartId = 'unknown') {
|
|
|
91
39
|
* Build normalized axis configuration
|
|
92
40
|
* @param {'x' | 'y'} axis - Axis name
|
|
93
41
|
* @param {Object} config - Raw config
|
|
94
|
-
* @param {string} chartId - Chart ID for warnings
|
|
95
42
|
* @returns {Object} - Normalized axis config
|
|
96
43
|
*/
|
|
97
|
-
function buildAxisConfig(axis, config
|
|
98
|
-
const axisUpper = axis.toUpperCase();
|
|
44
|
+
function buildAxisConfig(axis, config) {
|
|
99
45
|
const existingAxisConfig = config[axis] || {};
|
|
100
46
|
|
|
101
47
|
// Start with existing axis config if present
|
|
102
48
|
const axisConfig = { ...existingAxisConfig };
|
|
103
49
|
|
|
104
|
-
// max: x.max >
|
|
105
|
-
if (axisConfig.max === undefined) {
|
|
106
|
-
|
|
107
|
-
if (config[deprecatedKey] !== undefined) {
|
|
108
|
-
warnDeprecation(chartId, deprecatedKey, `${axis}.max`);
|
|
109
|
-
axisConfig.max = config[deprecatedKey];
|
|
110
|
-
} else if (config.max !== undefined) {
|
|
111
|
-
axisConfig.max = config.max;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// min: x.min > minX > min
|
|
116
|
-
if (axisConfig.min === undefined) {
|
|
117
|
-
const deprecatedKey = `min${axisUpper}`;
|
|
118
|
-
if (config[deprecatedKey] !== undefined) {
|
|
119
|
-
warnDeprecation(chartId, deprecatedKey, `${axis}.min`);
|
|
120
|
-
axisConfig.min = config[deprecatedKey];
|
|
121
|
-
} else if (config.min !== undefined) {
|
|
122
|
-
axisConfig.min = config.min;
|
|
123
|
-
}
|
|
50
|
+
// max: x.max > max (global fallback)
|
|
51
|
+
if (axisConfig.max === undefined && config.max !== undefined) {
|
|
52
|
+
axisConfig.max = config.max;
|
|
124
53
|
}
|
|
125
54
|
|
|
126
|
-
//
|
|
127
|
-
if (axisConfig.
|
|
128
|
-
|
|
129
|
-
if (config[deprecatedKey] !== undefined) {
|
|
130
|
-
warnDeprecation(chartId, deprecatedKey, `${axis}.title`);
|
|
131
|
-
axisConfig.title = config[deprecatedKey];
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// format: x.format > format.x > format
|
|
136
|
-
if (axisConfig.format === undefined) {
|
|
137
|
-
const globalFormat = config.format || {};
|
|
138
|
-
if (globalFormat[axis] !== undefined) {
|
|
139
|
-
// format.x or format.y (deprecated nested format)
|
|
140
|
-
warnDeprecation(chartId, `format.${axis}`, `${axis}.format`);
|
|
141
|
-
axisConfig.format = globalFormat[axis];
|
|
142
|
-
} else if (typeof globalFormat === 'object' && !globalFormat.x && !globalFormat.y) {
|
|
143
|
-
// Global format object (no x/y nesting) - use as fallback
|
|
144
|
-
axisConfig.format = globalFormat;
|
|
145
|
-
}
|
|
55
|
+
// min: x.min > min (global fallback)
|
|
56
|
+
if (axisConfig.min === undefined && config.min !== undefined) {
|
|
57
|
+
axisConfig.min = config.min;
|
|
146
58
|
}
|
|
147
59
|
|
|
148
60
|
return axisConfig;
|
|
@@ -236,20 +148,10 @@ export function parseAxisConfig(axisConfig) {
|
|
|
236
148
|
}
|
|
237
149
|
|
|
238
150
|
/**
|
|
239
|
-
* Get rotateLabels setting from axis config
|
|
151
|
+
* Get rotateLabels setting from axis config
|
|
240
152
|
* @param {Object} config - Normalized config
|
|
241
|
-
* @param {string} chartId - Chart ID for deprecation warnings
|
|
242
153
|
* @returns {boolean} - Whether to rotate labels
|
|
243
154
|
*/
|
|
244
|
-
export function getRotateLabels(config
|
|
245
|
-
|
|
246
|
-
if (config.x?.rotateLabels !== undefined) {
|
|
247
|
-
return config.x.rotateLabels;
|
|
248
|
-
}
|
|
249
|
-
// Deprecated: top-level rotateLabels
|
|
250
|
-
if (config.rotateLabels !== undefined) {
|
|
251
|
-
warnDeprecation(chartId, 'rotateLabels', 'x.rotateLabels');
|
|
252
|
-
return config.rotateLabels;
|
|
253
|
-
}
|
|
254
|
-
return false;
|
|
155
|
+
export function getRotateLabels(config) {
|
|
156
|
+
return config.x?.rotateLabels ?? false;
|
|
255
157
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deprecation validation for Uncharted
|
|
3
|
+
* Validates config for deprecated usage and logs appropriate warnings/errors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Track which deprecation warnings have been shown to avoid spam
|
|
7
|
+
const warnedConfigs = new Set();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Log a deprecation warning (once per config/key combination)
|
|
11
|
+
* @param {string} chartId - Chart identifier
|
|
12
|
+
* @param {string} oldKey - Deprecated key name
|
|
13
|
+
* @param {string} newKey - New key path
|
|
14
|
+
*/
|
|
15
|
+
function warnDeprecation(chartId, oldKey, newKey) {
|
|
16
|
+
const key = `${chartId}:${oldKey}`;
|
|
17
|
+
if (warnedConfigs.has(key)) return;
|
|
18
|
+
warnedConfigs.add(key);
|
|
19
|
+
console.warn(`[uncharted] Chart "${chartId}": "${oldKey}" is deprecated, use "${newKey}" instead`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate chart type and return error message if deprecated
|
|
24
|
+
* @param {string} type - Chart type
|
|
25
|
+
* @param {string} chartId - Chart identifier
|
|
26
|
+
* @returns {string|null} - Error message or null if valid
|
|
27
|
+
*/
|
|
28
|
+
export function validateChartType(type, chartId) {
|
|
29
|
+
if (type === 'dot') {
|
|
30
|
+
return `Chart "${chartId}": type "dot" is no longer supported. Use type "line" with lines: false instead.`;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check config for deprecated options and log warnings
|
|
37
|
+
* @param {Object} config - Chart configuration
|
|
38
|
+
* @param {string} chartId - Chart identifier
|
|
39
|
+
*/
|
|
40
|
+
export function checkDeprecatedOptions(config, chartId) {
|
|
41
|
+
if (!config) return;
|
|
42
|
+
|
|
43
|
+
// Check for deprecated axis shorthand keys
|
|
44
|
+
if (config.maxX !== undefined) {
|
|
45
|
+
warnDeprecation(chartId, 'maxX', 'x.max');
|
|
46
|
+
}
|
|
47
|
+
if (config.minX !== undefined) {
|
|
48
|
+
warnDeprecation(chartId, 'minX', 'x.min');
|
|
49
|
+
}
|
|
50
|
+
if (config.maxY !== undefined) {
|
|
51
|
+
warnDeprecation(chartId, 'maxY', 'y.max');
|
|
52
|
+
}
|
|
53
|
+
if (config.minY !== undefined) {
|
|
54
|
+
warnDeprecation(chartId, 'minY', 'y.min');
|
|
55
|
+
}
|
|
56
|
+
if (config.titleX !== undefined) {
|
|
57
|
+
warnDeprecation(chartId, 'titleX', 'x.title');
|
|
58
|
+
}
|
|
59
|
+
if (config.titleY !== undefined) {
|
|
60
|
+
warnDeprecation(chartId, 'titleY', 'y.title');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check for deprecated top-level rotateLabels
|
|
64
|
+
if (config.rotateLabels !== undefined) {
|
|
65
|
+
warnDeprecation(chartId, 'rotateLabels', 'x.rotateLabels');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check for deprecated legend array (when used for labels, not boolean)
|
|
69
|
+
if (Array.isArray(config.legend)) {
|
|
70
|
+
warnDeprecation(chartId, 'legend (array)', 'y.columns: { key: "Label" }');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check for deprecated scatter-specific keys
|
|
74
|
+
if (config.legendTitle !== undefined) {
|
|
75
|
+
warnDeprecation(chartId, 'legendTitle', 'series.title');
|
|
76
|
+
}
|
|
77
|
+
if (config.sizeTitle !== undefined) {
|
|
78
|
+
warnDeprecation(chartId, 'sizeTitle', 'size.title');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check for deprecated format.x / format.y structure
|
|
82
|
+
if (config.format && typeof config.format === 'object') {
|
|
83
|
+
if (config.format.x !== undefined) {
|
|
84
|
+
warnDeprecation(chartId, 'format.x', 'x.format');
|
|
85
|
+
}
|
|
86
|
+
if (config.format.y !== undefined) {
|
|
87
|
+
warnDeprecation(chartId, 'format.y', 'y.format');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check for deprecated columns.* structure
|
|
92
|
+
if (config.columns && typeof config.columns === 'object') {
|
|
93
|
+
const deprecatedColumnKeys = ['x', 'y', 'label', 'series', 'size', 'source', 'target', 'value'];
|
|
94
|
+
for (const key of deprecatedColumnKeys) {
|
|
95
|
+
if (config.columns[key] !== undefined) {
|
|
96
|
+
const newPath = key === 'label' ? 'x.column or label.column' :
|
|
97
|
+
key === 'x' ? 'x.column' :
|
|
98
|
+
key === 'y' ? 'y.columns' :
|
|
99
|
+
`${key}.column`;
|
|
100
|
+
warnDeprecation(chartId, `columns.${key}`, newPath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Clear warning cache (useful for testing)
|
|
108
|
+
*/
|
|
109
|
+
export function clearWarningCache() {
|
|
110
|
+
warnedConfigs.clear();
|
|
111
|
+
}
|
package/src/renderers/bubble.js
CHANGED
|
@@ -27,7 +27,7 @@ export function renderBubble(config) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const animateClass = animate ? ' chart-animate' : '';
|
|
30
|
-
const rotateLabels = getRotateLabels(config
|
|
30
|
+
const rotateLabels = getRotateLabels(config);
|
|
31
31
|
|
|
32
32
|
// Use resolved columns if available, otherwise fall back to implicit detection
|
|
33
33
|
const keys = Object.keys(data[0]);
|
|
@@ -49,9 +49,9 @@ export function renderBubble(config) {
|
|
|
49
49
|
seriesKey = findKey('series');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// Get legend/size titles from resolved columns (new schema)
|
|
53
|
-
const legendTitle = _columns?.seriesTitle
|
|
54
|
-
const sizeTitle = _columns?.sizeTitle
|
|
52
|
+
// Get legend/size titles from resolved columns (new schema only)
|
|
53
|
+
const legendTitle = _columns?.seriesTitle;
|
|
54
|
+
const sizeTitle = _columns?.sizeTitle;
|
|
55
55
|
|
|
56
56
|
// Axis titles
|
|
57
57
|
const xAxisTitle = getAxisTitle(config, 'x', '');
|
|
@@ -222,8 +222,9 @@ export function renderBubble(config) {
|
|
|
222
222
|
html += `</div>`; // close chart-body
|
|
223
223
|
|
|
224
224
|
// Legend (if multiple series or legendTitle specified)
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
// Note: legend array is deprecated; use series names directly
|
|
226
|
+
if (seriesList.length > 1 || legendTitle) {
|
|
227
|
+
const legendLabels = seriesList;
|
|
227
228
|
if (legendTitle) {
|
|
228
229
|
html += `<span class="chart-legend-title">${escapeHtml(legendTitle)}</span>`;
|
|
229
230
|
}
|
package/src/renderers/donut.js
CHANGED
|
@@ -103,11 +103,10 @@ export function renderDonut(config) {
|
|
|
103
103
|
|
|
104
104
|
html += `</div>`; // Close donut-body
|
|
105
105
|
|
|
106
|
-
// Build label lookup from
|
|
106
|
+
// Build label lookup from yLabels (new schema) or segment labels
|
|
107
107
|
const labelMap = _columns?.yLabels || {};
|
|
108
|
-
const getSegmentLabel = (segment
|
|
108
|
+
const getSegmentLabel = (segment) => {
|
|
109
109
|
if (labelMap[segment.label]) return labelMap[segment.label];
|
|
110
|
-
if (Array.isArray(legend)) return legend[index] ?? segment.label;
|
|
111
110
|
return segment.label;
|
|
112
111
|
};
|
|
113
112
|
|
|
@@ -120,7 +119,7 @@ export function renderDonut(config) {
|
|
|
120
119
|
if (showLegend) {
|
|
121
120
|
html += `<div class="chart-legend">`;
|
|
122
121
|
segments.forEach((segment, i) => {
|
|
123
|
-
const label = getSegmentLabel(segment
|
|
122
|
+
const label = getSegmentLabel(segment);
|
|
124
123
|
let displayValue;
|
|
125
124
|
if (showPercentages) {
|
|
126
125
|
displayValue = ((segment.value / total) * 100).toFixed(1) + '%';
|
package/src/renderers/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { renderStackedBar } from './stacked-bar.js';
|
|
2
2
|
import { renderStackedColumn } from './stacked-column.js';
|
|
3
3
|
import { renderDonut } from './donut.js';
|
|
4
|
-
import { renderDot } from './dot.js';
|
|
5
4
|
import { renderScatter } from './scatter.js';
|
|
6
5
|
import { renderSankey } from './sankey.js';
|
|
7
6
|
import { renderLine } from './line.js';
|
|
@@ -12,7 +11,6 @@ export const renderers = {
|
|
|
12
11
|
'stacked-bar': renderStackedBar,
|
|
13
12
|
'stacked-column': renderStackedColumn,
|
|
14
13
|
'donut': renderDonut,
|
|
15
|
-
'dot': renderDot,
|
|
16
14
|
'scatter': renderScatter,
|
|
17
15
|
'sankey': renderSankey,
|
|
18
16
|
'line': renderLine,
|
|
@@ -20,4 +18,4 @@ export const renderers = {
|
|
|
20
18
|
'bubble': renderBubble
|
|
21
19
|
};
|
|
22
20
|
|
|
23
|
-
export { renderStackedBar, renderStackedColumn, renderDonut,
|
|
21
|
+
export { renderStackedBar, renderStackedColumn, renderDonut, renderScatter, renderSankey, renderLine, renderTimeseries, renderBubble };
|
package/src/renderers/line.js
CHANGED
|
@@ -1,6 +1,218 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { slugify, escapeHtml, getLabelKey, getSeriesNames, renderDownloadLink } from '../utils.js';
|
|
2
|
+
import { formatNumber } from '../formatters.js';
|
|
3
|
+
import { getAxisMax, getAxisMin, getAxisFormat, getAxisTitle, getRotateLabels } from '../config.js';
|
|
2
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Render a line chart (categorical X axis with connected data points)
|
|
7
|
+
* @param {Object} config - Chart configuration (normalized)
|
|
8
|
+
* @param {string} config.title - Chart title
|
|
9
|
+
* @param {string} [config.subtitle] - Chart subtitle
|
|
10
|
+
* @param {Object[]} config.data - Chart data with label column and value columns
|
|
11
|
+
* @param {Object} [config.y] - Y-axis configuration { max, min, format }
|
|
12
|
+
* @param {string[]} [config.legend] - Legend labels (defaults to series names)
|
|
13
|
+
* @param {boolean} [config.animate] - Enable animations
|
|
14
|
+
* @param {boolean} [config.lines] - Show connecting lines (default: true)
|
|
15
|
+
* @param {boolean} [config.dots] - Show dots at data points (default: true)
|
|
16
|
+
* @param {Object} [config._columns] - Resolved column mappings
|
|
17
|
+
* @returns {string} - HTML string
|
|
18
|
+
*/
|
|
3
19
|
export function renderLine(config) {
|
|
4
|
-
const
|
|
5
|
-
|
|
20
|
+
const { title, subtitle, data, max, min, legend, legendTitle, animate, format, id, downloadData, downloadDataUrl, icons, _columns } = config;
|
|
21
|
+
|
|
22
|
+
// Line-specific options
|
|
23
|
+
const showLines = config.lines !== false; // default true
|
|
24
|
+
const showDots = config.dots !== false; // default true
|
|
25
|
+
|
|
26
|
+
if (!data || data.length === 0) {
|
|
27
|
+
return `<!-- Line chart: no data provided -->`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Get label key and series keys (use resolved columns if available)
|
|
31
|
+
const labelKey = _columns?.label ?? getLabelKey(data);
|
|
32
|
+
const seriesKeys = _columns?.values?.length > 0 ? _columns.values : getSeriesNames(data);
|
|
33
|
+
|
|
34
|
+
// Build legend labels from yLabels (new schema) or column names
|
|
35
|
+
const yLabels = _columns?.yLabels || {};
|
|
36
|
+
const getSeriesLabel = (key) => {
|
|
37
|
+
if (yLabels[key]) return yLabels[key];
|
|
38
|
+
return key;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Helper to get icon for a series
|
|
42
|
+
const getSeriesIcon = (key) => {
|
|
43
|
+
if (!icons) return null;
|
|
44
|
+
if (typeof icons === 'string') return icons;
|
|
45
|
+
return icons[key] ?? null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const animateClass = animate ? ' chart-animate' : '';
|
|
49
|
+
const rotateLabels = getRotateLabels(config);
|
|
50
|
+
|
|
51
|
+
// Get Y-axis format and axis titles
|
|
52
|
+
const yFormat = getAxisFormat(config, 'y');
|
|
53
|
+
const xAxisTitle = getAxisTitle(config, 'x', '');
|
|
54
|
+
const yAxisTitle = getAxisTitle(config, 'y', '');
|
|
55
|
+
|
|
56
|
+
// Calculate min and max values for Y scaling (exclude null values)
|
|
57
|
+
const allValues = data.flatMap(row =>
|
|
58
|
+
seriesKeys.map(key => row[key]).filter(val => val !== null && val !== undefined && val !== '')
|
|
59
|
+
).map(val => typeof val === 'number' ? val : parseFloat(val)).filter(v => !isNaN(v));
|
|
60
|
+
const dataMax = Math.max(...allValues);
|
|
61
|
+
const dataMin = Math.min(...allValues);
|
|
62
|
+
|
|
63
|
+
// Use normalized axis config, fall back to legacy top-level max/min
|
|
64
|
+
const maxValue = getAxisMax(config, 'y') ?? max ?? dataMax;
|
|
65
|
+
const minValue = getAxisMin(config, 'y') ?? min ?? (dataMin < 0 ? dataMin : 0);
|
|
66
|
+
const range = maxValue - minValue;
|
|
67
|
+
const hasNegativeY = minValue < 0;
|
|
68
|
+
|
|
69
|
+
// Calculate zero position for axis line
|
|
70
|
+
const zeroPct = hasNegativeY ? ((0 - minValue) / range) * 100 : 0;
|
|
71
|
+
|
|
72
|
+
const negativeClass = hasNegativeY ? ' has-negative-y' : '';
|
|
73
|
+
const idClass = id ? ` chart-${id}` : '';
|
|
74
|
+
const rotateClass = rotateLabels ? ' rotate-labels' : '';
|
|
75
|
+
// Add no-dots class if dots are disabled (icons only affect legend, not dots)
|
|
76
|
+
const dotsClass = !showDots ? ' no-dots' : '';
|
|
77
|
+
let html = `<figure class="chart chart-line${animateClass}${negativeClass}${idClass}${rotateClass}${dotsClass}">`;
|
|
78
|
+
|
|
79
|
+
if (title) {
|
|
80
|
+
html += `<figcaption class="chart-title">${escapeHtml(title)}`;
|
|
81
|
+
if (subtitle) {
|
|
82
|
+
html += `<span class="chart-subtitle">${escapeHtml(subtitle)}</span>`;
|
|
83
|
+
}
|
|
84
|
+
html += `</figcaption>`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
html += `<div class="chart-body">`;
|
|
88
|
+
|
|
89
|
+
// Y-axis
|
|
90
|
+
const yAxisStyle = hasNegativeY ? ` style="--zero-position: ${zeroPct.toFixed(2)}%"` : '';
|
|
91
|
+
html += `<div class="chart-y-axis"${yAxisStyle}>`;
|
|
92
|
+
html += `<span class="axis-label">${formatNumber(maxValue, yFormat) || maxValue}</span>`;
|
|
93
|
+
const midLabelY = hasNegativeY ? 0 : Math.round((maxValue + minValue) / 2);
|
|
94
|
+
html += `<span class="axis-label">${formatNumber(midLabelY, yFormat) || midLabelY}</span>`;
|
|
95
|
+
html += `<span class="axis-label">${formatNumber(minValue, yFormat) || minValue}</span>`;
|
|
96
|
+
if (yAxisTitle) {
|
|
97
|
+
html += `<span class="axis-title">${escapeHtml(yAxisTitle)}</span>`;
|
|
98
|
+
}
|
|
99
|
+
html += `</div>`;
|
|
100
|
+
|
|
101
|
+
// Scroll wrapper for chart + labels
|
|
102
|
+
html += `<div class="chart-scroll">`;
|
|
103
|
+
|
|
104
|
+
// Calculate delay step to cap total stagger at 1s
|
|
105
|
+
const maxStagger = 1; // seconds
|
|
106
|
+
const defaultDelay = 0.08; // seconds
|
|
107
|
+
const delayStep = data.length > 1 ? Math.min(defaultDelay, maxStagger / (data.length - 1)) : 0;
|
|
108
|
+
const styleVars = [`--delay-step: ${delayStep.toFixed(3)}s`];
|
|
109
|
+
if (hasNegativeY) styleVars.push(`--zero-position: ${zeroPct.toFixed(2)}%`);
|
|
110
|
+
html += `<div class="dot-chart" style="${styleVars.join('; ')}">`;
|
|
111
|
+
html += `<div class="dot-field">`;
|
|
112
|
+
|
|
113
|
+
// CSS line segments connecting dots (rendered before dot-cols so they stack behind)
|
|
114
|
+
// Skip segments where either endpoint is null (gap in data)
|
|
115
|
+
if (showLines && data.length > 1) {
|
|
116
|
+
let segIndex = 0;
|
|
117
|
+
seriesKeys.forEach((key, i) => {
|
|
118
|
+
const colorClass = `chart-color-${i + 1}`;
|
|
119
|
+
const seriesClass = `chart-series-${slugify(key)}`;
|
|
120
|
+
for (let colIndex = 0; colIndex < data.length - 1; colIndex++) {
|
|
121
|
+
const val1 = data[colIndex][key];
|
|
122
|
+
const val2 = data[colIndex + 1][key];
|
|
123
|
+
// Skip segment if either endpoint is null/missing
|
|
124
|
+
if (val1 === null || val1 === undefined || val1 === '' ||
|
|
125
|
+
val2 === null || val2 === undefined || val2 === '') {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const v1 = typeof val1 === 'number' ? val1 : parseFloat(val1);
|
|
129
|
+
const v2 = typeof val2 === 'number' ? val2 : parseFloat(val2);
|
|
130
|
+
const y1 = range > 0 ? ((v1 - minValue) / range) * 100 : 0;
|
|
131
|
+
const y2 = range > 0 ? ((v2 - minValue) / range) * 100 : 0;
|
|
132
|
+
const x1 = ((colIndex + 0.5) / data.length) * 100;
|
|
133
|
+
const x2 = ((colIndex + 1.5) / data.length) * 100;
|
|
134
|
+
html += `<div class="chart-line-segment ${colorClass} ${seriesClass}" `;
|
|
135
|
+
html += `style="--x1: ${x1.toFixed(2)}; --y1: ${y1.toFixed(2)}; --x2: ${x2.toFixed(2)}; --y2: ${y2.toFixed(2)}; --seg-index: ${segIndex}">`;
|
|
136
|
+
html += `</div>`;
|
|
137
|
+
segIndex++;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Always render dots for hover/tooltips (CSS handles visibility)
|
|
143
|
+
// Only show icons inside dots when dots are visible
|
|
144
|
+
data.forEach((row, colIndex) => {
|
|
145
|
+
const label = row[labelKey] ?? '';
|
|
146
|
+
|
|
147
|
+
html += `<div class="dot-col" style="--col-index: ${colIndex}">`;
|
|
148
|
+
|
|
149
|
+
seriesKeys.forEach((key, i) => {
|
|
150
|
+
const val = row[key];
|
|
151
|
+
// Skip null/missing values - don't render a dot
|
|
152
|
+
if (val === null || val === undefined || val === '') return;
|
|
153
|
+
|
|
154
|
+
const value = typeof val === 'number' ? val : parseFloat(val) || 0;
|
|
155
|
+
const yPct = range > 0 ? ((value - minValue) / range) * 100 : 0;
|
|
156
|
+
const colorClass = `chart-color-${i + 1}`;
|
|
157
|
+
const seriesClass = `chart-series-${slugify(key)}`;
|
|
158
|
+
const tooltipLabel = getSeriesLabel(key);
|
|
159
|
+
const icon = showDots ? getSeriesIcon(key) : null;
|
|
160
|
+
const iconClass = icon ? ' has-icon' : '';
|
|
161
|
+
|
|
162
|
+
html += `<div class="dot ${colorClass} ${seriesClass}${iconClass}" `;
|
|
163
|
+
html += `style="--value: ${yPct.toFixed(2)}%" `;
|
|
164
|
+
html += `title="${escapeHtml(tooltipLabel)}: ${formatNumber(value, yFormat) || value}"`;
|
|
165
|
+
html += `>`;
|
|
166
|
+
if (icon) {
|
|
167
|
+
html += `<i class="${escapeHtml(icon)}"></i>`;
|
|
168
|
+
}
|
|
169
|
+
html += `</div>`;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
html += `</div>`;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
html += `</div>`; // close dot-field
|
|
176
|
+
html += `</div>`; // close dot-chart
|
|
177
|
+
|
|
178
|
+
// X-axis labels
|
|
179
|
+
html += `<div class="dot-labels">`;
|
|
180
|
+
data.forEach(row => {
|
|
181
|
+
const label = row[labelKey] ?? '';
|
|
182
|
+
html += `<span class="dot-label">${escapeHtml(label)}</span>`;
|
|
183
|
+
});
|
|
184
|
+
if (xAxisTitle) {
|
|
185
|
+
html += `<span class="axis-title">${escapeHtml(xAxisTitle)}</span>`;
|
|
186
|
+
}
|
|
187
|
+
html += `</div>`;
|
|
188
|
+
|
|
189
|
+
html += `</div>`; // close chart-scroll
|
|
190
|
+
html += `</div>`; // close chart-body
|
|
191
|
+
|
|
192
|
+
// Legend (show if legend !== false and we have series keys or legendTitle)
|
|
193
|
+
const showLegend = config.legend !== false && (seriesKeys.length > 0 || legendTitle);
|
|
194
|
+
if (showLegend) {
|
|
195
|
+
if (legendTitle) {
|
|
196
|
+
html += `<span class="chart-legend-title">${escapeHtml(legendTitle)}</span>`;
|
|
197
|
+
}
|
|
198
|
+
html += `<div class="chart-legend">`;
|
|
199
|
+
seriesKeys.forEach((key, i) => {
|
|
200
|
+
const label = getSeriesLabel(key);
|
|
201
|
+
const colorClass = `chart-color-${i + 1}`;
|
|
202
|
+
const seriesClass = `chart-series-${slugify(key)}`;
|
|
203
|
+
const icon = getSeriesIcon(key);
|
|
204
|
+
const iconClass = icon ? ' has-icon' : '';
|
|
205
|
+
html += `<span class="chart-legend-item ${colorClass} ${seriesClass}${iconClass}">`;
|
|
206
|
+
if (icon) {
|
|
207
|
+
html += `<i class="${escapeHtml(icon)}"></i>`;
|
|
208
|
+
}
|
|
209
|
+
html += `${escapeHtml(label)}</span>`;
|
|
210
|
+
});
|
|
211
|
+
html += `</div>`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
html += renderDownloadLink(downloadDataUrl, downloadData);
|
|
215
|
+
html += `</figure>`;
|
|
216
|
+
|
|
217
|
+
return html;
|
|
6
218
|
}
|
package/src/renderers/scatter.js
CHANGED
|
@@ -54,9 +54,9 @@ export function renderScatter(config) {
|
|
|
54
54
|
seriesKey = findKey('series');
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// Get legend/size titles from resolved columns (new schema)
|
|
58
|
-
const legendTitle = _columns?.seriesTitle
|
|
59
|
-
const sizeTitle = _columns?.sizeTitle
|
|
57
|
+
// Get legend/size titles from resolved columns (new schema only)
|
|
58
|
+
const legendTitle = _columns?.seriesTitle;
|
|
59
|
+
const sizeTitle = _columns?.sizeTitle;
|
|
60
60
|
|
|
61
61
|
// Axis titles: use normalized config, fall back to column names
|
|
62
62
|
const xAxisTitle = getAxisTitle(config, 'x', xKey);
|
|
@@ -219,8 +219,9 @@ export function renderScatter(config) {
|
|
|
219
219
|
html += `</div>`;
|
|
220
220
|
|
|
221
221
|
// Legend (if multiple series or legendTitle specified)
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
// Note: legend array is deprecated; use series names directly
|
|
223
|
+
if (seriesList.length > 1 || legendTitle) {
|
|
224
|
+
const legendLabels = seriesList;
|
|
224
225
|
if (legendTitle) {
|
|
225
226
|
html += `<span class="chart-legend-title">${escapeHtml(legendTitle)}</span>`;
|
|
226
227
|
}
|
|
@@ -25,11 +25,10 @@ export function renderStackedBar(config) {
|
|
|
25
25
|
const labelKey = _columns?.label ?? getLabelKey(data);
|
|
26
26
|
const seriesKeys = _columns?.values?.length > 0 ? _columns.values : getSeriesNames(data);
|
|
27
27
|
|
|
28
|
-
// Build legend labels from
|
|
28
|
+
// Build legend labels from xLabels (new schema) or column names
|
|
29
29
|
const xLabels = _columns?.xLabels || {};
|
|
30
|
-
const getSeriesLabel = (key
|
|
30
|
+
const getSeriesLabel = (key) => {
|
|
31
31
|
if (xLabels[key]) return xLabels[key];
|
|
32
|
-
if (Array.isArray(legend)) return legend[index] ?? key;
|
|
33
32
|
return key;
|
|
34
33
|
};
|
|
35
34
|
|
|
@@ -67,7 +66,7 @@ export function renderStackedBar(config) {
|
|
|
67
66
|
}
|
|
68
67
|
html += `<div class="chart-legend">`;
|
|
69
68
|
seriesKeys.forEach((key, i) => {
|
|
70
|
-
const label = getSeriesLabel(key
|
|
69
|
+
const label = getSeriesLabel(key);
|
|
71
70
|
const colorClass = `chart-color-${i + 1}`;
|
|
72
71
|
const seriesClass = `chart-series-${slugify(key)}`;
|
|
73
72
|
html += `<span class="chart-legend-item ${colorClass} ${seriesClass}">${escapeHtml(label)}</span>`;
|
|
@@ -101,7 +100,7 @@ export function renderStackedBar(config) {
|
|
|
101
100
|
if (pct > 0) {
|
|
102
101
|
const colorClass = `chart-color-${i + 1}`;
|
|
103
102
|
const seriesClass = `chart-series-${slugify(key)}`;
|
|
104
|
-
const seriesLabel = getSeriesLabel(key
|
|
103
|
+
const seriesLabel = getSeriesLabel(key);
|
|
105
104
|
html += `<div class="bar-fill ${colorClass} ${seriesClass}" style="--value: ${pct.toFixed(2)}%" title="${escapeHtml(seriesLabel)}: ${formatNumber(value, xFormat) || value}"></div>`;
|
|
106
105
|
}
|
|
107
106
|
});
|
|
@@ -25,16 +25,15 @@ export function renderStackedColumn(config) {
|
|
|
25
25
|
const labelKey = _columns?.label ?? getLabelKey(data);
|
|
26
26
|
const seriesKeys = _columns?.values?.length > 0 ? _columns.values : getSeriesNames(data);
|
|
27
27
|
|
|
28
|
-
// Build legend labels from
|
|
28
|
+
// Build legend labels from yLabels (new schema) or column names
|
|
29
29
|
const yLabels = _columns?.yLabels || {};
|
|
30
|
-
const getSeriesLabel = (key
|
|
30
|
+
const getSeriesLabel = (key) => {
|
|
31
31
|
if (yLabels[key]) return yLabels[key];
|
|
32
|
-
if (Array.isArray(legend)) return legend[index] ?? key;
|
|
33
32
|
return key;
|
|
34
33
|
};
|
|
35
34
|
|
|
36
35
|
const animateClass = animate ? ' chart-animate' : '';
|
|
37
|
-
const rotateLabels = getRotateLabels(config
|
|
36
|
+
const rotateLabels = getRotateLabels(config);
|
|
38
37
|
|
|
39
38
|
// Get Y-axis format
|
|
40
39
|
const yFormat = getAxisFormat(config, 'y');
|
|
@@ -122,7 +121,7 @@ export function renderStackedColumn(config) {
|
|
|
122
121
|
const value = typeof val === 'number' ? val : parseFloat(val) || 0;
|
|
123
122
|
const colorClass = `chart-color-${i + 1}`;
|
|
124
123
|
const seriesClass = `chart-series-${slugify(key)}`;
|
|
125
|
-
const seriesLabel = getSeriesLabel(key
|
|
124
|
+
const seriesLabel = getSeriesLabel(key);
|
|
126
125
|
const segmentHeight = range > 0 ? (Math.abs(value) / range) * 100 : 0;
|
|
127
126
|
|
|
128
127
|
if (value >= 0) {
|
|
@@ -201,7 +200,7 @@ export function renderStackedColumn(config) {
|
|
|
201
200
|
if (showLegend) {
|
|
202
201
|
html += `<div class="chart-legend">`;
|
|
203
202
|
seriesKeys.forEach((key, i) => {
|
|
204
|
-
const label = getSeriesLabel(key
|
|
203
|
+
const label = getSeriesLabel(key);
|
|
205
204
|
const colorClass = `chart-color-${i + 1}`;
|
|
206
205
|
const seriesClass = `chart-series-${slugify(key)}`;
|
|
207
206
|
html += `<span class="chart-legend-item ${colorClass} ${seriesClass}">${escapeHtml(label)}</span>`;
|
|
@@ -262,14 +262,14 @@ export function renderTimeseries(config) {
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
// Build legend labels
|
|
265
|
+
// Build legend labels from yLabels (new schema) or column names
|
|
265
266
|
const yLabels = _columns?.yLabels || {};
|
|
266
267
|
const yColumnsLabels = config.y?.columns && typeof config.y.columns === 'object' && !Array.isArray(config.y.columns)
|
|
267
268
|
? config.y.columns
|
|
268
269
|
: {};
|
|
269
|
-
const getSeriesLabel = (key
|
|
270
|
+
const getSeriesLabel = (key) => {
|
|
270
271
|
if (yLabels[key]) return yLabels[key];
|
|
271
272
|
if (yColumnsLabels[key]) return yColumnsLabels[key];
|
|
272
|
-
if (Array.isArray(legend)) return legend[index] ?? key;
|
|
273
273
|
return key;
|
|
274
274
|
};
|
|
275
275
|
|
|
@@ -433,7 +433,7 @@ export function renderTimeseries(config) {
|
|
|
433
433
|
const points = seriesData.get(key);
|
|
434
434
|
const colorClass = `chart-color-${seriesIdx + 1}`;
|
|
435
435
|
const seriesClass = `chart-series-${slugify(key)}`;
|
|
436
|
-
const seriesLabel = getSeriesLabel(key
|
|
436
|
+
const seriesLabel = getSeriesLabel(key);
|
|
437
437
|
const icon = showDots ? getSeriesIcon(key) : null;
|
|
438
438
|
const iconClass = icon ? ' has-icon' : '';
|
|
439
439
|
|
|
@@ -484,7 +484,7 @@ export function renderTimeseries(config) {
|
|
|
484
484
|
}
|
|
485
485
|
html += `<div class="chart-legend">`;
|
|
486
486
|
seriesKeys.forEach((key, i) => {
|
|
487
|
-
const label = getSeriesLabel(key
|
|
487
|
+
const label = getSeriesLabel(key);
|
|
488
488
|
const colorClass = `chart-color-${i + 1}`;
|
|
489
489
|
const seriesClass = `chart-series-${slugify(key)}`;
|
|
490
490
|
const icon = getSeriesIcon(key);
|
package/src/renderers/dot.js
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import { slugify, escapeHtml, getLabelKey, getSeriesNames, renderDownloadLink } from '../utils.js';
|
|
2
|
-
import { formatNumber } from '../formatters.js';
|
|
3
|
-
import { getAxisMax, getAxisMin, getAxisFormat, getAxisTitle, getRotateLabels } from '../config.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Render a categorical dot chart (columns with dots at different Y positions)
|
|
7
|
-
* Like atlas-wrapped's adoption chart - discrete X axis, continuous Y axis
|
|
8
|
-
* @param {Object} config - Chart configuration (normalized)
|
|
9
|
-
* @param {string} config.title - Chart title
|
|
10
|
-
* @param {string} [config.subtitle] - Chart subtitle
|
|
11
|
-
* @param {Object[]} config.data - Chart data with label column and value columns
|
|
12
|
-
* @param {Object} [config.y] - Y-axis configuration { max, min, format }
|
|
13
|
-
* @param {string[]} [config.legend] - Legend labels (defaults to series names)
|
|
14
|
-
* @param {boolean} [config.animate] - Enable animations
|
|
15
|
-
* @param {Object} [config._columns] - Resolved column mappings
|
|
16
|
-
* @returns {string} - HTML string
|
|
17
|
-
*/
|
|
18
|
-
export function renderDot(config) {
|
|
19
|
-
const { title, subtitle, data, max, min, legend, legendTitle, animate, format, id, downloadData, downloadDataUrl, connectDots, dots: showDots = true, icons, chartType = 'dot', _columns } = config;
|
|
20
|
-
|
|
21
|
-
// Deprecation warning for dot chart type
|
|
22
|
-
if (chartType === 'dot') {
|
|
23
|
-
console.warn(
|
|
24
|
-
'[uncharted] Chart type "dot" is deprecated. ' +
|
|
25
|
-
'Migrate to "line" with lines: false, or use "bubble" for sized dots.'
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (!data || data.length === 0) {
|
|
30
|
-
return `<!-- Dot chart: no data provided -->`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Get label key and series keys (use resolved columns if available)
|
|
34
|
-
const labelKey = _columns?.label ?? getLabelKey(data);
|
|
35
|
-
const seriesKeys = _columns?.values?.length > 0 ? _columns.values : getSeriesNames(data);
|
|
36
|
-
|
|
37
|
-
// Build legend labels from: 1) yLabels (new), 2) legend array (deprecated), 3) column names
|
|
38
|
-
const yLabels = _columns?.yLabels || {};
|
|
39
|
-
const getSeriesLabel = (key, index) => {
|
|
40
|
-
if (yLabels[key]) return yLabels[key];
|
|
41
|
-
if (Array.isArray(legend)) return legend[index] ?? key;
|
|
42
|
-
return key;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// Helper to get icon for a series
|
|
46
|
-
const getSeriesIcon = (key) => {
|
|
47
|
-
if (!icons) return null;
|
|
48
|
-
if (typeof icons === 'string') return icons;
|
|
49
|
-
return icons[key] ?? null;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const animateClass = animate ? ' chart-animate' : '';
|
|
53
|
-
const rotateLabels = getRotateLabels(config, config.id);
|
|
54
|
-
|
|
55
|
-
// Get Y-axis format and axis titles
|
|
56
|
-
const yFormat = getAxisFormat(config, 'y');
|
|
57
|
-
const xAxisTitle = getAxisTitle(config, 'x', '');
|
|
58
|
-
const yAxisTitle = getAxisTitle(config, 'y', '');
|
|
59
|
-
|
|
60
|
-
// Calculate min and max values for Y scaling (exclude null values)
|
|
61
|
-
const allValues = data.flatMap(row =>
|
|
62
|
-
seriesKeys.map(key => row[key]).filter(val => val !== null && val !== undefined && val !== '')
|
|
63
|
-
).map(val => typeof val === 'number' ? val : parseFloat(val)).filter(v => !isNaN(v));
|
|
64
|
-
const dataMax = Math.max(...allValues);
|
|
65
|
-
const dataMin = Math.min(...allValues);
|
|
66
|
-
|
|
67
|
-
// Use normalized axis config, fall back to legacy top-level max/min
|
|
68
|
-
const maxValue = getAxisMax(config, 'y') ?? max ?? dataMax;
|
|
69
|
-
const minValue = getAxisMin(config, 'y') ?? min ?? (dataMin < 0 ? dataMin : 0);
|
|
70
|
-
const range = maxValue - minValue;
|
|
71
|
-
const hasNegativeY = minValue < 0;
|
|
72
|
-
|
|
73
|
-
// Calculate zero position for axis line
|
|
74
|
-
const zeroPct = hasNegativeY ? ((0 - minValue) / range) * 100 : 0;
|
|
75
|
-
|
|
76
|
-
const negativeClass = hasNegativeY ? ' has-negative-y' : '';
|
|
77
|
-
const idClass = id ? ` chart-${id}` : '';
|
|
78
|
-
const rotateClass = rotateLabels ? ' rotate-labels' : '';
|
|
79
|
-
// Add no-dots class if dots are disabled (icons only affect legend, not dots)
|
|
80
|
-
const dotsClass = !showDots ? ' no-dots' : '';
|
|
81
|
-
let html = `<figure class="chart chart-${chartType}${animateClass}${negativeClass}${idClass}${rotateClass}${dotsClass}">`;
|
|
82
|
-
|
|
83
|
-
if (title) {
|
|
84
|
-
html += `<figcaption class="chart-title">${escapeHtml(title)}`;
|
|
85
|
-
if (subtitle) {
|
|
86
|
-
html += `<span class="chart-subtitle">${escapeHtml(subtitle)}</span>`;
|
|
87
|
-
}
|
|
88
|
-
html += `</figcaption>`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
html += `<div class="chart-body">`;
|
|
92
|
-
|
|
93
|
-
// Y-axis
|
|
94
|
-
const yAxisStyle = hasNegativeY ? ` style="--zero-position: ${zeroPct.toFixed(2)}%"` : '';
|
|
95
|
-
html += `<div class="chart-y-axis"${yAxisStyle}>`;
|
|
96
|
-
html += `<span class="axis-label">${formatNumber(maxValue, yFormat) || maxValue}</span>`;
|
|
97
|
-
const midLabelY = hasNegativeY ? 0 : Math.round((maxValue + minValue) / 2);
|
|
98
|
-
html += `<span class="axis-label">${formatNumber(midLabelY, yFormat) || midLabelY}</span>`;
|
|
99
|
-
html += `<span class="axis-label">${formatNumber(minValue, yFormat) || minValue}</span>`;
|
|
100
|
-
if (yAxisTitle) {
|
|
101
|
-
html += `<span class="axis-title">${escapeHtml(yAxisTitle)}</span>`;
|
|
102
|
-
}
|
|
103
|
-
html += `</div>`;
|
|
104
|
-
|
|
105
|
-
// Scroll wrapper for chart + labels
|
|
106
|
-
html += `<div class="chart-scroll">`;
|
|
107
|
-
|
|
108
|
-
// Calculate delay step to cap total stagger at 1s
|
|
109
|
-
const maxStagger = 1; // seconds
|
|
110
|
-
const defaultDelay = 0.08; // seconds
|
|
111
|
-
const delayStep = data.length > 1 ? Math.min(defaultDelay, maxStagger / (data.length - 1)) : 0;
|
|
112
|
-
const styleVars = [`--delay-step: ${delayStep.toFixed(3)}s`];
|
|
113
|
-
if (hasNegativeY) styleVars.push(`--zero-position: ${zeroPct.toFixed(2)}%`);
|
|
114
|
-
html += `<div class="dot-chart" style="${styleVars.join('; ')}">`;
|
|
115
|
-
html += `<div class="dot-field">`;
|
|
116
|
-
|
|
117
|
-
// CSS line segments connecting dots (rendered before dot-cols so they stack behind)
|
|
118
|
-
// Skip segments where either endpoint is null (gap in data)
|
|
119
|
-
if (connectDots && data.length > 1) {
|
|
120
|
-
let segIndex = 0;
|
|
121
|
-
seriesKeys.forEach((key, i) => {
|
|
122
|
-
const colorClass = `chart-color-${i + 1}`;
|
|
123
|
-
const seriesClass = `chart-series-${slugify(key)}`;
|
|
124
|
-
for (let colIndex = 0; colIndex < data.length - 1; colIndex++) {
|
|
125
|
-
const val1 = data[colIndex][key];
|
|
126
|
-
const val2 = data[colIndex + 1][key];
|
|
127
|
-
// Skip segment if either endpoint is null/missing
|
|
128
|
-
if (val1 === null || val1 === undefined || val1 === '' ||
|
|
129
|
-
val2 === null || val2 === undefined || val2 === '') {
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
const v1 = typeof val1 === 'number' ? val1 : parseFloat(val1);
|
|
133
|
-
const v2 = typeof val2 === 'number' ? val2 : parseFloat(val2);
|
|
134
|
-
const y1 = range > 0 ? ((v1 - minValue) / range) * 100 : 0;
|
|
135
|
-
const y2 = range > 0 ? ((v2 - minValue) / range) * 100 : 0;
|
|
136
|
-
const x1 = ((colIndex + 0.5) / data.length) * 100;
|
|
137
|
-
const x2 = ((colIndex + 1.5) / data.length) * 100;
|
|
138
|
-
html += `<div class="chart-line-segment ${colorClass} ${seriesClass}" `;
|
|
139
|
-
html += `style="--x1: ${x1.toFixed(2)}; --y1: ${y1.toFixed(2)}; --x2: ${x2.toFixed(2)}; --y2: ${y2.toFixed(2)}; --seg-index: ${segIndex}">`;
|
|
140
|
-
html += `</div>`;
|
|
141
|
-
segIndex++;
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Always render dots for hover/tooltips (CSS handles visibility)
|
|
147
|
-
// Only show icons inside dots when dots are visible
|
|
148
|
-
data.forEach((row, colIndex) => {
|
|
149
|
-
const label = row[labelKey] ?? '';
|
|
150
|
-
|
|
151
|
-
html += `<div class="dot-col" style="--col-index: ${colIndex}">`;
|
|
152
|
-
|
|
153
|
-
seriesKeys.forEach((key, i) => {
|
|
154
|
-
const val = row[key];
|
|
155
|
-
// Skip null/missing values - don't render a dot
|
|
156
|
-
if (val === null || val === undefined || val === '') return;
|
|
157
|
-
|
|
158
|
-
const value = typeof val === 'number' ? val : parseFloat(val) || 0;
|
|
159
|
-
const yPct = range > 0 ? ((value - minValue) / range) * 100 : 0;
|
|
160
|
-
const colorClass = `chart-color-${i + 1}`;
|
|
161
|
-
const seriesClass = `chart-series-${slugify(key)}`;
|
|
162
|
-
const tooltipLabel = getSeriesLabel(key, i);
|
|
163
|
-
const icon = showDots ? getSeriesIcon(key) : null;
|
|
164
|
-
const iconClass = icon ? ' has-icon' : '';
|
|
165
|
-
|
|
166
|
-
html += `<div class="dot ${colorClass} ${seriesClass}${iconClass}" `;
|
|
167
|
-
html += `style="--value: ${yPct.toFixed(2)}%" `;
|
|
168
|
-
html += `title="${escapeHtml(tooltipLabel)}: ${formatNumber(value, yFormat) || value}"`;
|
|
169
|
-
html += `>`;
|
|
170
|
-
if (icon) {
|
|
171
|
-
html += `<i class="${escapeHtml(icon)}"></i>`;
|
|
172
|
-
}
|
|
173
|
-
html += `</div>`;
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
html += `</div>`;
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
html += `</div>`; // close dot-field
|
|
180
|
-
html += `</div>`; // close dot-chart
|
|
181
|
-
|
|
182
|
-
// X-axis labels
|
|
183
|
-
html += `<div class="dot-labels">`;
|
|
184
|
-
data.forEach(row => {
|
|
185
|
-
const label = row[labelKey] ?? '';
|
|
186
|
-
html += `<span class="dot-label">${escapeHtml(label)}</span>`;
|
|
187
|
-
});
|
|
188
|
-
if (xAxisTitle) {
|
|
189
|
-
html += `<span class="axis-title">${escapeHtml(xAxisTitle)}</span>`;
|
|
190
|
-
}
|
|
191
|
-
html += `</div>`;
|
|
192
|
-
|
|
193
|
-
html += `</div>`; // close chart-scroll
|
|
194
|
-
html += `</div>`; // close chart-body
|
|
195
|
-
|
|
196
|
-
// Legend (show if legend !== false and we have series keys or legendTitle)
|
|
197
|
-
const showLegend = config.legend !== false && (seriesKeys.length > 0 || legendTitle);
|
|
198
|
-
if (showLegend) {
|
|
199
|
-
if (legendTitle) {
|
|
200
|
-
html += `<span class="chart-legend-title">${escapeHtml(legendTitle)}</span>`;
|
|
201
|
-
}
|
|
202
|
-
html += `<div class="chart-legend">`;
|
|
203
|
-
seriesKeys.forEach((key, i) => {
|
|
204
|
-
const label = getSeriesLabel(key, i);
|
|
205
|
-
const colorClass = `chart-color-${i + 1}`;
|
|
206
|
-
const seriesClass = `chart-series-${slugify(key)}`;
|
|
207
|
-
const icon = getSeriesIcon(key);
|
|
208
|
-
const iconClass = icon ? ' has-icon' : '';
|
|
209
|
-
html += `<span class="chart-legend-item ${colorClass} ${seriesClass}${iconClass}">`;
|
|
210
|
-
if (icon) {
|
|
211
|
-
html += `<i class="${escapeHtml(icon)}"></i>`;
|
|
212
|
-
}
|
|
213
|
-
html += `${escapeHtml(label)}</span>`;
|
|
214
|
-
});
|
|
215
|
-
html += `</div>`;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
html += renderDownloadLink(downloadDataUrl, downloadData);
|
|
219
|
-
html += `</figure>`;
|
|
220
|
-
|
|
221
|
-
return html;
|
|
222
|
-
}
|