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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eleventy-plugin-uncharted",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
4
4
  "description": "An Eleventy plugin that renders CSS-based charts from CSV data using shortcodes",
5
5
  "main": "eleventy.config.js",
6
6
  "type": "module",
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 (takes precedence):
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
- // Like scatter but with categorical X axis
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
- // New schema: x.column, y.column, label.column, series.column, size.column
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
- // New schema: source.column, target.column, value.column
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
- // New schema: label.column, value.column
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
- // New schema: y.column (categories), x.columns (values with labels)
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 (dot, line, stacked-column): x = categories, y = multi-series values
324
- // New schema: x.column (categories), y.columns (values with labels)
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
- * Precedence for axis properties (highest to lowest):
39
- * 1. x.max / y.max (new nested format)
40
- * 2. maxX / maxY (deprecated suffixed format)
41
- * 3. max (global fallback)
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 precedence:
49
- * 1. x.column / x.columns / y.column / y.columns (new unified format)
50
- * 2. columns.x / columns.y (deprecated format)
14
+ * Column mapping:
15
+ * - x.column / x.columns / y.column / y.columns
51
16
  *
52
- * Legend precedence:
53
- * 1. y.columns: { key: "Label" } or x.columns (for stacked-bar)
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 deprecation warnings
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, chartId);
30
+ normalized.x = buildAxisConfig('x', config);
67
31
 
68
32
  // Build y axis config
69
- normalized.y = buildAxisConfig('y', config, chartId);
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, chartId) {
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 > maxX > max
105
- if (axisConfig.max === undefined) {
106
- const deprecatedKey = `max${axisUpper}`;
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
- // title: x.title > titleX
127
- if (axisConfig.title === undefined) {
128
- const deprecatedKey = `title${axisUpper}`;
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 or deprecated top-level
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, chartId) {
245
- // New schema: x.rotateLabels
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
+ }
@@ -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
- console.warn(`[uncharted] Puppeteer not installed. Skipped ${count} chart image(s).`);
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