eleventy-plugin-uncharted 1.0.0-beta.3 → 1.0.0-beta.4

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/css/uncharted.css CHANGED
@@ -1674,8 +1674,11 @@
1674
1674
  Download Link
1675
1675
  ========================================================================== */
1676
1676
 
1677
- .chart-download {
1678
- display: block;
1677
+ .chart-downloads {
1679
1678
  font-size: 0.75em;
1680
1679
  margin-block-start: 0.5em;
1681
1680
  }
1681
+
1682
+ .chart-download-sep {
1683
+ opacity: 0.5;
1684
+ }
@@ -28,6 +28,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
28
  * @param {boolean} [options.dataPassthrough] - Copy CSV files to public dataPath (default: false)
29
29
  * @param {string} [options.dataPath] - Public URL path for CSV files (default: '/data/')
30
30
  * @param {boolean|string} [options.downloadData] - Enable download links globally (individual charts can override)
31
+ * @param {boolean|string} [options.downloadImage] - Enable image download links globally (requires image generation)
31
32
  * @param {Object} [options.image] - Image generation options
32
33
  * @param {boolean} [options.image.enabled=false] - Enable PNG image generation
33
34
  * @param {string} [options.image.outputDir='/images/charts/'] - Output directory for images (URL path)
@@ -67,11 +68,11 @@ export default function(eleventyConfig, options = {}) {
67
68
  const dataPassthrough = options.dataPassthrough ?? false;
68
69
  const dataPath = options.dataPath || '/data/';
69
70
  const globalDownloadData = options.downloadData ?? false;
71
+ const globalDownloadImage = options.downloadImage ?? false;
70
72
 
71
73
  // Image generation options
72
74
  const imageOptions = normalizeImageOptions(options.image);
73
- const skipImageGeneration = shouldSkipInDevMode(imageOptions) ||
74
- process.argv.includes('--skip-images');
75
+ const skipImageGeneration = shouldSkipInDevMode(imageOptions);
75
76
 
76
77
  // Clear image URLs at start of each build
77
78
  clearImageUrls();
@@ -185,6 +186,7 @@ export default function(eleventyConfig, options = {}) {
185
186
  // Render the chart (chart-specific settings override global)
186
187
  const animate = chartConfig.animate ?? globalAnimate;
187
188
  const downloadData = chartConfig.downloadData ?? globalDownloadData;
189
+ const downloadImage = chartConfig.downloadImage ?? globalDownloadImage;
188
190
 
189
191
  // Calculate download URL if download is enabled and file is specified
190
192
  let downloadDataUrl = null;
@@ -193,6 +195,13 @@ export default function(eleventyConfig, options = {}) {
193
195
  downloadDataUrl = normalizedDataPath + chartConfig.file;
194
196
  }
195
197
 
198
+ // Calculate image download URL if enabled and image generation is active
199
+ const chartImageEnabled = chartConfig.image?.enabled ?? imageOptions.enabled;
200
+ let downloadImageUrl = null;
201
+ if (downloadImage && chartImageEnabled) {
202
+ downloadImageUrl = getImageUrl(chartId, chartConfig, imageOptions);
203
+ }
204
+
196
205
  // Normalize configuration (handles deprecated keys, axis config)
197
206
  const normalizedConfig = normalizeConfig(chartConfig, chartId);
198
207
 
@@ -207,6 +216,8 @@ export default function(eleventyConfig, options = {}) {
207
216
  animate,
208
217
  downloadData,
209
218
  downloadDataUrl,
219
+ downloadImage,
220
+ downloadImageUrl,
210
221
  _columns: columns
211
222
  });
212
223
 
@@ -218,7 +229,6 @@ export default function(eleventyConfig, options = {}) {
218
229
  );
219
230
 
220
231
  // Handle image generation
221
- const chartImageEnabled = chartConfig.image?.enabled ?? imageOptions.enabled;
222
232
  if (chartImageEnabled && !skipImageGeneration && eleventyDirs?.output) {
223
233
  // Queue chart for image generation
224
234
  queueChartForImage(chartId, chartHtml, chartConfig, imageOptions, eleventyDirs.output);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eleventy-plugin-uncharted",
3
- "version": "1.0.0-beta.3",
3
+ "version": "1.0.0-beta.4",
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",
@@ -1,4 +1,4 @@
1
- import { slugify, escapeHtml, renderDownloadLink } from '../utils.js';
1
+ import { slugify, escapeHtml, renderDownloadLinks } from '../utils.js';
2
2
  import { formatNumber } from '../formatters.js';
3
3
  import { getAxisMax, getAxisMin, getAxisTitle, getAxisFormat, getRotateLabels } from '../config.js';
4
4
 
@@ -17,7 +17,7 @@ import { getAxisMax, getAxisMin, getAxisTitle, getAxisFormat, getRotateLabels }
17
17
  * @returns {string} - HTML string
18
18
  */
19
19
  export function renderBubble(config) {
20
- const { title, subtitle, data, legend, animate, format, id, downloadData, downloadDataUrl, icons, _columns } = config;
20
+ const { title, subtitle, data, legend, animate, format, id, downloadData, downloadDataUrl, downloadImage, downloadImageUrl, icons, _columns } = config;
21
21
 
22
22
  // Get axis-specific format configs
23
23
  const fmtY = getAxisFormat(config, 'y');
@@ -262,7 +262,7 @@ export function renderBubble(config) {
262
262
  html += `</div>`;
263
263
  }
264
264
 
265
- html += renderDownloadLink(downloadDataUrl, downloadData);
265
+ html += renderDownloadLinks(downloadDataUrl, downloadData, downloadImageUrl, downloadImage);
266
266
  html += `</figure>`;
267
267
 
268
268
  return html;
@@ -1,4 +1,4 @@
1
- import { slugify, escapeHtml, getLabelKey, getValueKey, getSeriesNames, renderDownloadLink } from '../utils.js';
1
+ import { slugify, escapeHtml, getLabelKey, getValueKey, getSeriesNames, renderDownloadLinks } from '../utils.js';
2
2
  import { formatNumber } from '../formatters.js';
3
3
 
4
4
  /**
@@ -17,7 +17,7 @@ import { formatNumber } from '../formatters.js';
17
17
  * @returns {string} - HTML string
18
18
  */
19
19
  export function renderDonut(config) {
20
- const { title, subtitle, data, legend, center, animate, format, id, showPercentages, downloadData, downloadDataUrl, _columns } = config;
20
+ const { title, subtitle, data, legend, center, animate, format, id, showPercentages, downloadData, downloadDataUrl, downloadImage, downloadImageUrl, _columns } = config;
21
21
 
22
22
  if (!data || data.length === 0) {
23
23
  return `<!-- Donut chart: no data provided -->`;
@@ -136,7 +136,7 @@ export function renderDonut(config) {
136
136
  html += `</div>`;
137
137
  }
138
138
 
139
- html += renderDownloadLink(downloadDataUrl, downloadData);
139
+ html += renderDownloadLinks(downloadDataUrl, downloadData, downloadImageUrl, downloadImage);
140
140
  html += `</figure>`;
141
141
 
142
142
  return html;
@@ -1,4 +1,4 @@
1
- import { slugify, escapeHtml, getLabelKey, getSeriesNames, renderDownloadLink } from '../utils.js';
1
+ import { slugify, escapeHtml, getLabelKey, getSeriesNames, renderDownloadLinks } from '../utils.js';
2
2
  import { formatNumber } from '../formatters.js';
3
3
  import { getAxisMax, getAxisMin, getAxisFormat, getAxisTitle, getRotateLabels } from '../config.js';
4
4
 
@@ -17,7 +17,7 @@ import { getAxisMax, getAxisMin, getAxisFormat, getAxisTitle, getRotateLabels }
17
17
  * @returns {string} - HTML string
18
18
  */
19
19
  export function renderLine(config) {
20
- const { title, subtitle, data, max, min, legend, legendTitle, animate, format, id, downloadData, downloadDataUrl, icons, _columns } = config;
20
+ const { title, subtitle, data, max, min, legend, legendTitle, animate, format, id, downloadData, downloadDataUrl, downloadImage, downloadImageUrl, icons, _columns } = config;
21
21
 
22
22
  // Line-specific options
23
23
  const showLines = config.lines !== false; // default true
@@ -211,7 +211,7 @@ export function renderLine(config) {
211
211
  html += `</div>`;
212
212
  }
213
213
 
214
- html += renderDownloadLink(downloadDataUrl, downloadData);
214
+ html += renderDownloadLinks(downloadDataUrl, downloadData, downloadImageUrl, downloadImage);
215
215
  html += `</figure>`;
216
216
 
217
217
  return html;
@@ -1,4 +1,4 @@
1
- import { slugify, escapeHtml, renderDownloadLink } from '../utils.js';
1
+ import { slugify, escapeHtml, renderDownloadLinks } from '../utils.js';
2
2
  import { formatNumber } from '../formatters.js';
3
3
 
4
4
  /**
@@ -17,7 +17,7 @@ import { formatNumber } from '../formatters.js';
17
17
  * @returns {string} - HTML string
18
18
  */
19
19
  export function renderSankey(config) {
20
- const { title, subtitle, data, legend, animate, format, id, downloadData, downloadDataUrl, nodeWidth = 20, nodePadding = 10, endLabelsOutside = false, proportional = false, _columns } = config;
20
+ const { title, subtitle, data, legend, animate, format, id, downloadData, downloadDataUrl, downloadImage, downloadImageUrl, nodeWidth = 20, nodePadding = 10, endLabelsOutside = false, proportional = false, _columns } = config;
21
21
 
22
22
  if (!data || data.length === 0) {
23
23
  return `<!-- Sankey chart: no data provided -->`;
@@ -526,7 +526,7 @@ export function renderSankey(config) {
526
526
  html += `</div>`;
527
527
  }
528
528
 
529
- html += renderDownloadLink(downloadDataUrl, downloadData);
529
+ html += renderDownloadLinks(downloadDataUrl, downloadData, downloadImageUrl, downloadImage);
530
530
  html += `</figure>`;
531
531
 
532
532
  return html;
@@ -1,4 +1,4 @@
1
- import { slugify, escapeHtml, renderDownloadLink } from '../utils.js';
1
+ import { slugify, escapeHtml, renderDownloadLinks } from '../utils.js';
2
2
  import { formatNumber } from '../formatters.js';
3
3
  import { getAxisMax, getAxisMin, getAxisTitle, getAxisFormat } from '../config.js';
4
4
 
@@ -18,7 +18,7 @@ import { getAxisMax, getAxisMin, getAxisTitle, getAxisFormat } from '../config.j
18
18
  * @returns {string} - HTML string
19
19
  */
20
20
  export function renderScatter(config) {
21
- const { title, subtitle, data, legend, animate, format, id, downloadData, downloadDataUrl, proportional, icons, _columns } = config;
21
+ const { title, subtitle, data, legend, animate, format, id, downloadData, downloadDataUrl, downloadImage, downloadImageUrl, proportional, icons, _columns } = config;
22
22
 
23
23
  // Get axis-specific format configs (normalized config provides x.format/y.format)
24
24
  const fmtX = getAxisFormat(config, 'x');
@@ -259,7 +259,7 @@ export function renderScatter(config) {
259
259
  html += `</div>`;
260
260
  }
261
261
 
262
- html += renderDownloadLink(downloadDataUrl, downloadData);
262
+ html += renderDownloadLinks(downloadDataUrl, downloadData, downloadImageUrl, downloadImage);
263
263
  html += `</figure>`;
264
264
 
265
265
  return html;
@@ -1,4 +1,4 @@
1
- import { slugify, calculatePercentages, getLabelKey, getSeriesNames, escapeHtml, renderDownloadLink } from '../utils.js';
1
+ import { slugify, calculatePercentages, getLabelKey, getSeriesNames, escapeHtml, renderDownloadLinks } from '../utils.js';
2
2
  import { formatNumber } from '../formatters.js';
3
3
  import { getAxisMax, getAxisFormat } from '../config.js';
4
4
 
@@ -15,7 +15,7 @@ import { getAxisMax, getAxisFormat } from '../config.js';
15
15
  * @returns {string} - HTML string
16
16
  */
17
17
  export function renderStackedBar(config) {
18
- const { title, subtitle, data, max, legend, legendTitle, animate, format, id, downloadData, downloadDataUrl, _columns } = config;
18
+ const { title, subtitle, data, max, legend, legendTitle, animate, format, id, downloadData, downloadDataUrl, downloadImage, downloadImageUrl, _columns } = config;
19
19
 
20
20
  if (!data || data.length === 0) {
21
21
  return `<!-- Stacked bar chart: no data provided -->`;
@@ -114,7 +114,7 @@ export function renderStackedBar(config) {
114
114
  });
115
115
 
116
116
  html += `</div>`;
117
- html += renderDownloadLink(downloadDataUrl, downloadData);
117
+ html += renderDownloadLinks(downloadDataUrl, downloadData, downloadImageUrl, downloadImage);
118
118
  html += `</figure>`;
119
119
 
120
120
  return html;
@@ -1,4 +1,4 @@
1
- import { slugify, getLabelKey, getSeriesNames, escapeHtml, renderDownloadLink } from '../utils.js';
1
+ import { slugify, getLabelKey, getSeriesNames, escapeHtml, renderDownloadLinks } from '../utils.js';
2
2
  import { formatNumber } from '../formatters.js';
3
3
  import { getAxisMax, getAxisMin, getAxisFormat, getRotateLabels } from '../config.js';
4
4
 
@@ -15,7 +15,7 @@ import { getAxisMax, getAxisMin, getAxisFormat, getRotateLabels } from '../confi
15
15
  * @returns {string} - HTML string
16
16
  */
17
17
  export function renderStackedColumn(config) {
18
- const { title, subtitle, data, max, min, legend, animate, format, id, downloadData, downloadDataUrl, _columns } = config;
18
+ const { title, subtitle, data, max, min, legend, animate, format, id, downloadData, downloadDataUrl, downloadImage, downloadImageUrl, _columns } = config;
19
19
 
20
20
  if (!data || data.length === 0) {
21
21
  return `<!-- Stacked column chart: no data provided -->`;
@@ -208,7 +208,7 @@ export function renderStackedColumn(config) {
208
208
  html += `</div>`;
209
209
  }
210
210
 
211
- html += renderDownloadLink(downloadDataUrl, downloadData);
211
+ html += renderDownloadLinks(downloadDataUrl, downloadData, downloadImageUrl, downloadImage);
212
212
  html += `</figure>`;
213
213
 
214
214
  return html;
@@ -1,4 +1,4 @@
1
- import { slugify, escapeHtml, getLabelKey, getSeriesNames, renderDownloadLink } from '../utils.js';
1
+ import { slugify, escapeHtml, getLabelKey, getSeriesNames, renderDownloadLinks } from '../utils.js';
2
2
  import { formatNumber } from '../formatters.js';
3
3
  import { getAxisMax, getAxisMin, getAxisTitle, getAxisFormat, getRotateLabels } from '../config.js';
4
4
 
@@ -237,7 +237,7 @@ function getAxisTicks(min, max, isDate) {
237
237
  * @returns {string} - HTML string
238
238
  */
239
239
  export function renderTimeseries(config) {
240
- const { title, subtitle, data, legend, legendTitle, animate, format, id, downloadData, downloadDataUrl, dots: showDots = false, icons, _columns } = config;
240
+ const { title, subtitle, data, legend, legendTitle, animate, format, id, downloadData, downloadDataUrl, downloadImage, downloadImageUrl, dots: showDots = false, icons, _columns } = config;
241
241
 
242
242
  if (!data || data.length === 0) {
243
243
  return `<!-- Timeseries chart: no data provided -->`;
@@ -498,7 +498,7 @@ export function renderTimeseries(config) {
498
498
  html += `</div>`;
499
499
  }
500
500
 
501
- html += renderDownloadLink(downloadDataUrl, downloadData);
501
+ html += renderDownloadLinks(downloadDataUrl, downloadData, downloadImageUrl, downloadImage);
502
502
  html += `</figure>`;
503
503
 
504
504
  return html;
package/src/utils.js CHANGED
@@ -82,3 +82,31 @@ export function renderDownloadLink(url, label) {
82
82
  return `<a href="${escapeHtml(url)}" class="chart-download" download>${escapeHtml(text)}</a>`;
83
83
  }
84
84
 
85
+ /**
86
+ * Render a download link for chart image
87
+ * @param {string} url - URL to the image file
88
+ * @param {boolean|string} label - true for default label, or custom string
89
+ * @returns {string} - HTML string for the download link, or empty string if no URL
90
+ */
91
+ export function renderDownloadImageLink(url, label) {
92
+ if (!url) return '';
93
+ const text = typeof label === 'string' ? label : '↓ Download image';
94
+ return `<a href="${escapeHtml(url)}" class="chart-download" download>${escapeHtml(text)}</a>`;
95
+ }
96
+
97
+ /**
98
+ * Render download links container (wraps data and image download links)
99
+ * @param {string} dataUrl - URL to the CSV file
100
+ * @param {boolean|string} dataLabel - true for default label, or custom string
101
+ * @param {string} imageUrl - URL to the image file
102
+ * @param {boolean|string} imageLabel - true for default label, or custom string
103
+ * @returns {string} - HTML string for the download links container, or empty string if no URLs
104
+ */
105
+ export function renderDownloadLinks(dataUrl, dataLabel, imageUrl, imageLabel) {
106
+ const dataLink = renderDownloadLink(dataUrl, dataLabel);
107
+ const imageLink = renderDownloadImageLink(imageUrl, imageLabel);
108
+ if (!dataLink && !imageLink) return '';
109
+ const separator = dataLink && imageLink ? ' <span class="chart-download-sep">•</span> ' : '';
110
+ return `<div class="chart-downloads">${dataLink}${separator}${imageLink}</div>`;
111
+ }
112
+