eleventy-plugin-uncharted 1.0.0-beta.1 → 1.0.0-beta.3
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 +24 -2
- 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/image/index.js +26 -2
- package/src/image/renderer.js +7 -3
- 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,
|
|
@@ -29,7 +30,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
29
30
|
* @param {boolean|string} [options.downloadData] - Enable download links globally (individual charts can override)
|
|
30
31
|
* @param {Object} [options.image] - Image generation options
|
|
31
32
|
* @param {boolean} [options.image.enabled=false] - Enable PNG image generation
|
|
32
|
-
* @param {string} [options.image.outputDir='/images/charts/'] - Output directory for images
|
|
33
|
+
* @param {string} [options.image.outputDir='/images/charts/'] - Output directory for images (URL path)
|
|
34
|
+
* @param {string} [options.image.cacheDir] - Source directory for cached images (enables caching when set)
|
|
33
35
|
* @param {number} [options.image.width=800] - Default image width in pixels
|
|
34
36
|
* @param {number} [options.image.height=400] - Default image height in pixels
|
|
35
37
|
* @param {number} [options.image.scale=2] - Device scale factor (2 for retina)
|
|
@@ -68,7 +70,8 @@ export default function(eleventyConfig, options = {}) {
|
|
|
68
70
|
|
|
69
71
|
// Image generation options
|
|
70
72
|
const imageOptions = normalizeImageOptions(options.image);
|
|
71
|
-
const skipImageGeneration = shouldSkipInDevMode(imageOptions)
|
|
73
|
+
const skipImageGeneration = shouldSkipInDevMode(imageOptions) ||
|
|
74
|
+
process.argv.includes('--skip-images');
|
|
72
75
|
|
|
73
76
|
// Clear image URLs at start of each build
|
|
74
77
|
clearImageUrls();
|
|
@@ -122,6 +125,15 @@ export default function(eleventyConfig, options = {}) {
|
|
|
122
125
|
});
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
// Image cache passthrough - copies cached images to output
|
|
129
|
+
if (imageOptions.cacheDir) {
|
|
130
|
+
const cacheDirClean = imageOptions.cacheDir.replace(/^\/|\/$/g, '');
|
|
131
|
+
const outputDirClean = imageOptions.outputDir.replace(/^\/|\/$/g, '');
|
|
132
|
+
eleventyConfig.addPassthroughCopy({
|
|
133
|
+
[cacheDirClean]: outputDirClean
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
125
137
|
eleventyConfig.addShortcode('chart', function(chartId) {
|
|
126
138
|
// Get resolved data directory (from Eleventy config or plugin options)
|
|
127
139
|
const resolvedDataDir = getDataDir();
|
|
@@ -145,11 +157,21 @@ export default function(eleventyConfig, options = {}) {
|
|
|
145
157
|
return `<!-- Chart "${chartId}" has no type specified -->`;
|
|
146
158
|
}
|
|
147
159
|
|
|
160
|
+
// Check for deprecated chart type (ERROR - prevents rendering)
|
|
161
|
+
const typeError = validateChartType(chartType, chartId);
|
|
162
|
+
if (typeError) {
|
|
163
|
+
console.error(`[uncharted] ${typeError}`);
|
|
164
|
+
return `<!-- ${typeError} -->`;
|
|
165
|
+
}
|
|
166
|
+
|
|
148
167
|
const renderer = renderers[chartType];
|
|
149
168
|
if (!renderer) {
|
|
150
169
|
return `<!-- Unknown chart type "${chartType}" for chart "${chartId}" -->`;
|
|
151
170
|
}
|
|
152
171
|
|
|
172
|
+
// Check for deprecated config options (WARNING - chart renders but option ignored)
|
|
173
|
+
checkDeprecatedOptions(chartConfig, chartId);
|
|
174
|
+
|
|
153
175
|
// Load data from CSV file or use inline data
|
|
154
176
|
let data = chartConfig.data;
|
|
155
177
|
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/image/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
15
15
|
* @typedef {Object} ImageOptions
|
|
16
16
|
* @property {boolean} [enabled=false] - Enable image generation
|
|
17
17
|
* @property {string} [outputDir='/images/charts/'] - Output directory for images (URL path)
|
|
18
|
+
* @property {string} [cacheDir=null] - Source directory for cached images (enables caching when set)
|
|
18
19
|
* @property {number} [width=800] - Default image width
|
|
19
20
|
* @property {number} [height=400] - Default image height
|
|
20
21
|
* @property {number} [scale=2] - Device scale factor (2 for retina)
|
|
@@ -33,6 +34,7 @@ const imageUrls = new Map();
|
|
|
33
34
|
const defaultOptions = {
|
|
34
35
|
enabled: false,
|
|
35
36
|
outputDir: '/images/charts/',
|
|
37
|
+
cacheDir: null,
|
|
36
38
|
width: 800,
|
|
37
39
|
height: 400,
|
|
38
40
|
scale: 2,
|
|
@@ -83,6 +85,23 @@ export function getImageUrl(chartId, chartConfig, globalOptions) {
|
|
|
83
85
|
return `${dir}${filename}.png`;
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Get the cache file path for a chart image.
|
|
90
|
+
* @param {string} chartId - Chart identifier
|
|
91
|
+
* @param {Object} chartConfig - Chart configuration
|
|
92
|
+
* @param {ImageOptions} globalOptions - Global image options
|
|
93
|
+
* @returns {string|null} Absolute cache path, or null if caching disabled
|
|
94
|
+
*/
|
|
95
|
+
export function getCachePath(chartId, chartConfig, globalOptions) {
|
|
96
|
+
if (!globalOptions.cacheDir) return null;
|
|
97
|
+
|
|
98
|
+
const imageConfig = chartConfig.image || {};
|
|
99
|
+
const filename = imageConfig.filename || chartId;
|
|
100
|
+
const cacheDir = globalOptions.cacheDir.replace(/\/$/, '');
|
|
101
|
+
|
|
102
|
+
return path.resolve(process.cwd(), cacheDir, `${filename}.png`);
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
/**
|
|
87
106
|
* Store the image URL for a chart (for shortcode lookup).
|
|
88
107
|
* @param {string} chartId
|
|
@@ -143,6 +162,7 @@ export function queueChartForImage(chartId, chartHtml, chartConfig, globalOption
|
|
|
143
162
|
|
|
144
163
|
const outputPath = getImageOutputPath(chartId, chartConfig, globalOptions, outputDir);
|
|
145
164
|
const url = getImageUrl(chartId, chartConfig, globalOptions);
|
|
165
|
+
const cachePath = getCachePath(chartId, chartConfig, globalOptions);
|
|
146
166
|
|
|
147
167
|
// Store URL for shortcode lookup
|
|
148
168
|
storeImageUrl(chartId, url);
|
|
@@ -152,7 +172,8 @@ export function queueChartForImage(chartId, chartHtml, chartConfig, globalOption
|
|
|
152
172
|
id: chartId,
|
|
153
173
|
html: chartHtml,
|
|
154
174
|
config,
|
|
155
|
-
outputPath
|
|
175
|
+
outputPath,
|
|
176
|
+
cachePath
|
|
156
177
|
});
|
|
157
178
|
}
|
|
158
179
|
|
|
@@ -171,7 +192,10 @@ export async function processQueue(options) {
|
|
|
171
192
|
if (!available) {
|
|
172
193
|
const count = getQueueSize();
|
|
173
194
|
clearQueue();
|
|
174
|
-
|
|
195
|
+
// Suppress warning if cacheDir is set (cached images will be used via passthrough)
|
|
196
|
+
if (!options.cacheDir) {
|
|
197
|
+
console.warn(`[uncharted] Puppeteer not installed. Skipped ${count} chart image(s).`);
|
|
198
|
+
}
|
|
175
199
|
return { success: [], failed: [], skipped: true };
|
|
176
200
|
}
|
|
177
201
|
|