eleventy-plugin-uncharted 1.0.0-beta.2 → 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 +5 -2
- package/eleventy.config.js +23 -2
- package/package.json +1 -1
- package/src/image/index.js +26 -2
- package/src/image/renderer.js +7 -3
- package/src/renderers/bubble.js +3 -3
- package/src/renderers/donut.js +3 -3
- package/src/renderers/line.js +3 -3
- package/src/renderers/sankey.js +3 -3
- package/src/renderers/scatter.js +3 -3
- package/src/renderers/stacked-bar.js +3 -3
- package/src/renderers/stacked-column.js +3 -3
- package/src/renderers/timeseries.js +3 -3
- package/src/utils.js +28 -0
package/css/uncharted.css
CHANGED
|
@@ -1674,8 +1674,11 @@
|
|
|
1674
1674
|
Download Link
|
|
1675
1675
|
========================================================================== */
|
|
1676
1676
|
|
|
1677
|
-
.chart-
|
|
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
|
+
}
|
package/eleventy.config.js
CHANGED
|
@@ -28,9 +28,11 @@ 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
|
-
* @param {string} [options.image.outputDir='/images/charts/'] - Output directory for images
|
|
34
|
+
* @param {string} [options.image.outputDir='/images/charts/'] - Output directory for images (URL path)
|
|
35
|
+
* @param {string} [options.image.cacheDir] - Source directory for cached images (enables caching when set)
|
|
34
36
|
* @param {number} [options.image.width=800] - Default image width in pixels
|
|
35
37
|
* @param {number} [options.image.height=400] - Default image height in pixels
|
|
36
38
|
* @param {number} [options.image.scale=2] - Device scale factor (2 for retina)
|
|
@@ -66,6 +68,7 @@ export default function(eleventyConfig, options = {}) {
|
|
|
66
68
|
const dataPassthrough = options.dataPassthrough ?? false;
|
|
67
69
|
const dataPath = options.dataPath || '/data/';
|
|
68
70
|
const globalDownloadData = options.downloadData ?? false;
|
|
71
|
+
const globalDownloadImage = options.downloadImage ?? false;
|
|
69
72
|
|
|
70
73
|
// Image generation options
|
|
71
74
|
const imageOptions = normalizeImageOptions(options.image);
|
|
@@ -123,6 +126,15 @@ export default function(eleventyConfig, options = {}) {
|
|
|
123
126
|
});
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
// Image cache passthrough - copies cached images to output
|
|
130
|
+
if (imageOptions.cacheDir) {
|
|
131
|
+
const cacheDirClean = imageOptions.cacheDir.replace(/^\/|\/$/g, '');
|
|
132
|
+
const outputDirClean = imageOptions.outputDir.replace(/^\/|\/$/g, '');
|
|
133
|
+
eleventyConfig.addPassthroughCopy({
|
|
134
|
+
[cacheDirClean]: outputDirClean
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
126
138
|
eleventyConfig.addShortcode('chart', function(chartId) {
|
|
127
139
|
// Get resolved data directory (from Eleventy config or plugin options)
|
|
128
140
|
const resolvedDataDir = getDataDir();
|
|
@@ -174,6 +186,7 @@ export default function(eleventyConfig, options = {}) {
|
|
|
174
186
|
// Render the chart (chart-specific settings override global)
|
|
175
187
|
const animate = chartConfig.animate ?? globalAnimate;
|
|
176
188
|
const downloadData = chartConfig.downloadData ?? globalDownloadData;
|
|
189
|
+
const downloadImage = chartConfig.downloadImage ?? globalDownloadImage;
|
|
177
190
|
|
|
178
191
|
// Calculate download URL if download is enabled and file is specified
|
|
179
192
|
let downloadDataUrl = null;
|
|
@@ -182,6 +195,13 @@ export default function(eleventyConfig, options = {}) {
|
|
|
182
195
|
downloadDataUrl = normalizedDataPath + chartConfig.file;
|
|
183
196
|
}
|
|
184
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
|
+
|
|
185
205
|
// Normalize configuration (handles deprecated keys, axis config)
|
|
186
206
|
const normalizedConfig = normalizeConfig(chartConfig, chartId);
|
|
187
207
|
|
|
@@ -196,6 +216,8 @@ export default function(eleventyConfig, options = {}) {
|
|
|
196
216
|
animate,
|
|
197
217
|
downloadData,
|
|
198
218
|
downloadDataUrl,
|
|
219
|
+
downloadImage,
|
|
220
|
+
downloadImageUrl,
|
|
199
221
|
_columns: columns
|
|
200
222
|
});
|
|
201
223
|
|
|
@@ -207,7 +229,6 @@ export default function(eleventyConfig, options = {}) {
|
|
|
207
229
|
);
|
|
208
230
|
|
|
209
231
|
// Handle image generation
|
|
210
|
-
const chartImageEnabled = chartConfig.image?.enabled ?? imageOptions.enabled;
|
|
211
232
|
if (chartImageEnabled && !skipImageGeneration && eleventyDirs?.output) {
|
|
212
233
|
// Queue chart for image generation
|
|
213
234
|
queueChartForImage(chartId, chartHtml, chartConfig, imageOptions, eleventyDirs.output);
|
package/package.json
CHANGED
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
|
|
package/src/image/renderer.js
CHANGED
|
@@ -170,7 +170,8 @@ ${stylesheetLinks}
|
|
|
170
170
|
* @param {Object} chart - Chart data
|
|
171
171
|
* @param {string} chart.html - Chart HTML
|
|
172
172
|
* @param {Object} chart.config - Image configuration
|
|
173
|
-
* @param {string} chart.outputPath - Output file path
|
|
173
|
+
* @param {string} chart.outputPath - Output file path (_site)
|
|
174
|
+
* @param {string} [chart.cachePath] - Cache file path (source directory, if caching enabled)
|
|
174
175
|
* @param {string} css - Chart CSS content
|
|
175
176
|
* @param {Object} defaults - Default image options
|
|
176
177
|
* @returns {Promise<void>}
|
|
@@ -183,6 +184,9 @@ async function renderChart(page, chart, css, defaults) {
|
|
|
183
184
|
const background = config.background || '#ffffff';
|
|
184
185
|
const stylesheets = config.stylesheets || defaults.stylesheets || [];
|
|
185
186
|
|
|
187
|
+
// Use cache path if provided, otherwise write directly to output
|
|
188
|
+
const writePath = chart.cachePath || chart.outputPath;
|
|
189
|
+
|
|
186
190
|
// Set viewport
|
|
187
191
|
await page.setViewport({
|
|
188
192
|
width,
|
|
@@ -206,14 +210,14 @@ async function renderChart(page, chart, css, defaults) {
|
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
// Ensure output directory exists
|
|
209
|
-
const outputDir = path.dirname(
|
|
213
|
+
const outputDir = path.dirname(writePath);
|
|
210
214
|
if (!fs.existsSync(outputDir)) {
|
|
211
215
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
212
216
|
}
|
|
213
217
|
|
|
214
218
|
// Take screenshot
|
|
215
219
|
await page.screenshot({
|
|
216
|
-
path:
|
|
220
|
+
path: writePath,
|
|
217
221
|
type: 'png',
|
|
218
222
|
omitBackground: background === 'transparent'
|
|
219
223
|
});
|
package/src/renderers/bubble.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { slugify, escapeHtml,
|
|
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 +=
|
|
265
|
+
html += renderDownloadLinks(downloadDataUrl, downloadData, downloadImageUrl, downloadImage);
|
|
266
266
|
html += `</figure>`;
|
|
267
267
|
|
|
268
268
|
return html;
|
package/src/renderers/donut.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { slugify, escapeHtml, getLabelKey, getValueKey, getSeriesNames,
|
|
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 +=
|
|
139
|
+
html += renderDownloadLinks(downloadDataUrl, downloadData, downloadImageUrl, downloadImage);
|
|
140
140
|
html += `</figure>`;
|
|
141
141
|
|
|
142
142
|
return html;
|
package/src/renderers/line.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { slugify, escapeHtml, getLabelKey, getSeriesNames,
|
|
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 +=
|
|
214
|
+
html += renderDownloadLinks(downloadDataUrl, downloadData, downloadImageUrl, downloadImage);
|
|
215
215
|
html += `</figure>`;
|
|
216
216
|
|
|
217
217
|
return html;
|
package/src/renderers/sankey.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { slugify, escapeHtml,
|
|
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 +=
|
|
529
|
+
html += renderDownloadLinks(downloadDataUrl, downloadData, downloadImageUrl, downloadImage);
|
|
530
530
|
html += `</figure>`;
|
|
531
531
|
|
|
532
532
|
return html;
|
package/src/renderers/scatter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { slugify, escapeHtml,
|
|
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 +=
|
|
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,
|
|
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 +=
|
|
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,
|
|
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 +=
|
|
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,
|
|
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 +=
|
|
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
|
+
|