@malinconico/nmcharts 2.3.0 → 2.5.0

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.
@@ -1,320 +0,0 @@
1
- /**
2
- * NMCharts Extras — Themes, SVG Export, ResizeObserver, Accessibility
3
- * Load after nmcharts.js and nmcharts-categories.js
4
- */
5
- (function (root) {
6
- 'use strict';
7
-
8
- var NM = root.NMCharts;
9
- if (!NM) throw new Error('NMCharts-Extras: NMCharts must be loaded first.');
10
-
11
- // ---------------------------------------------------------------------------
12
- // 1. PREDEFINED THEMES
13
- // ---------------------------------------------------------------------------
14
-
15
- NM.themes = {
16
- corporate: ['#1e3a5f', '#2d6a9f', '#4a90d9', '#7cb5ec', '#a8d1f0', '#2f4858', '#547a96', '#8fb3c9'],
17
- pastel: ['#a8d8ea', '#aa96da', '#fcbad3', '#ffffd2', '#b5ead7', '#c7ceea', '#ffdac1', '#e2f0cb'],
18
- vibrant: ['#ff6b6b', '#feca57', '#48dbfb', '#ff9ff3', '#54a0ff', '#5f27cd', '#01a3a4', '#f368e0'],
19
- monochrome: ['#1a1a2e', '#3d3d5c', '#5f5f8a', '#8282b8', '#a5a5d0', '#c8c8e6', '#e0e0f0', '#f0f0f8'],
20
- colorblind: ['#0072B2', '#E69F00', '#009E73', '#D55E00', '#CC79A7', '#56B4E9', '#F0E442', '#999999']
21
- };
22
-
23
- // ---------------------------------------------------------------------------
24
- // 2. HELPERS
25
- // ---------------------------------------------------------------------------
26
-
27
- /** Debounce: returns a function that delays invoking fn until after wait ms. */
28
- function debounce(fn, wait) {
29
- var timer;
30
- return function () {
31
- var ctx = this, args = arguments;
32
- clearTimeout(timer);
33
- timer = setTimeout(function () { fn.apply(ctx, args); }, wait);
34
- };
35
- }
36
-
37
- /**
38
- * Inject theme colors into opts.series entries that lack explicit colors,
39
- * and into opts.colors if not already set.
40
- * Mutates a shallow clone of opts to avoid modifying the caller's object.
41
- */
42
- function applyTheme(opts) {
43
- if (!opts || !opts.theme) return opts;
44
- var palette = NM.themes[opts.theme];
45
- if (!palette) return opts;
46
-
47
- // Shallow clone
48
- var o = Object.assign({}, opts);
49
-
50
- // Apply colors array if not explicitly provided
51
- if (!o.colors) {
52
- o.colors = palette;
53
- }
54
-
55
- // Apply per-series colors where missing
56
- if (o.series) {
57
- o.series = o.series.map(function (s, i) {
58
- if (s.color) return s;
59
- return Object.assign({}, s, { color: palette[i % palette.length] });
60
- });
61
- }
62
-
63
- return o;
64
- }
65
-
66
- // ---------------------------------------------------------------------------
67
- // 3. ACCESSIBILITY HELPERS
68
- // ---------------------------------------------------------------------------
69
-
70
- /** Inject ARIA attributes and a visually-hidden data table for screen readers. */
71
- function injectA11y(instance, chartType) {
72
- var root = instance._root;
73
- if (!root) return;
74
-
75
- var opts = instance.opts || {};
76
- var title = opts.title || '';
77
- var label = title ? (title + ' — ' + chartType + ' chart') : (chartType + ' chart');
78
-
79
- root.setAttribute('role', 'img');
80
- root.setAttribute('aria-label', label);
81
- root.setAttribute('tabindex', '0');
82
-
83
- // Build a visually-hidden table from series data when available
84
- var series = instance.series;
85
- var categories = instance.categories;
86
- if (!series || !series.length) return;
87
-
88
- // Only build if categories exist (category charts), or data arrays exist
89
- var table = document.createElement('table');
90
- table.style.cssText = [
91
- 'position:absolute',
92
- 'width:1px',
93
- 'height:1px',
94
- 'padding:0',
95
- 'margin:-1px',
96
- 'overflow:hidden',
97
- 'clip:rect(0,0,0,0)',
98
- 'white-space:nowrap',
99
- 'border:0'
100
- ].join(';');
101
-
102
- // Caption
103
- var caption = document.createElement('caption');
104
- caption.textContent = label;
105
- table.appendChild(caption);
106
-
107
- if (categories && categories.length) {
108
- // Header row: category labels
109
- var thead = document.createElement('thead');
110
- var headerRow = document.createElement('tr');
111
- var thEmpty = document.createElement('th');
112
- thEmpty.textContent = 'Series';
113
- headerRow.appendChild(thEmpty);
114
- categories.forEach(function (cat) {
115
- var th = document.createElement('th');
116
- th.setAttribute('scope', 'col');
117
- th.textContent = String(cat);
118
- headerRow.appendChild(th);
119
- });
120
- thead.appendChild(headerRow);
121
- table.appendChild(thead);
122
-
123
- // Body rows: one row per series
124
- var tbody = document.createElement('tbody');
125
- series.forEach(function (s) {
126
- var tr = document.createElement('tr');
127
- var th = document.createElement('th');
128
- th.setAttribute('scope', 'row');
129
- th.textContent = s.name || '';
130
- tr.appendChild(th);
131
- (s.data || []).forEach(function (v) {
132
- var td = document.createElement('td');
133
- td.textContent = v != null ? String(v) : '';
134
- tr.appendChild(td);
135
- });
136
- tbody.appendChild(tr);
137
- });
138
- table.appendChild(tbody);
139
- } else {
140
- // Single-value series (gauge, donut-like): just list name + value
141
- var tbody2 = document.createElement('tbody');
142
- series.forEach(function (s) {
143
- var tr = document.createElement('tr');
144
- var tdName = document.createElement('td');
145
- tdName.textContent = s.name || '';
146
- var tdVal = document.createElement('td');
147
- tdVal.textContent = s.value != null ? String(s.value) : (Array.isArray(s.data) ? s.data.join(', ') : '');
148
- tr.appendChild(tdName);
149
- tr.appendChild(tdVal);
150
- tbody2.appendChild(tr);
151
- });
152
- table.appendChild(tbody2);
153
- }
154
-
155
- // Append to root (positioned so it doesn't affect layout)
156
- root.style.position = root.style.position || 'relative';
157
- root.appendChild(table);
158
- }
159
-
160
- // ---------------------------------------------------------------------------
161
- // 4. SVG EXPORT
162
- // ---------------------------------------------------------------------------
163
-
164
- /**
165
- * Export chart as SVG by wrapping the canvas data URL in an SVG <image> element.
166
- * Works on any chart instance that has a .canvas property.
167
- * @param {string} [filename] - Download filename (default: 'chart.svg')
168
- */
169
- function exportSVG(filename) {
170
- var canvas = this.canvas;
171
- if (!canvas) {
172
- console.warn('NMCharts exportSVG: no canvas found on instance');
173
- return;
174
- }
175
-
176
- var w = canvas.width / (window.devicePixelRatio || 1);
177
- var h = canvas.height / (window.devicePixelRatio || 1);
178
- var dataURL = canvas.toDataURL('image/png');
179
-
180
- var svg = [
181
- '<?xml version="1.0" encoding="UTF-8"?>',
182
- '<svg xmlns="http://www.w3.org/2000/svg"',
183
- ' xmlns:xlink="http://www.w3.org/1999/xlink"',
184
- ' width="' + w + '" height="' + h + '"',
185
- ' viewBox="0 0 ' + w + ' ' + h + '">',
186
- ' <title>' + ((this.opts && this.opts.title) ? escapeXml(this.opts.title) : 'Chart') + '</title>',
187
- ' <image x="0" y="0" width="' + w + '" height="' + h + '"',
188
- ' xlink:href="' + dataURL + '"',
189
- ' href="' + dataURL + '"/>',
190
- '</svg>'
191
- ].join('\n');
192
-
193
- var blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
194
- var url = URL.createObjectURL(blob);
195
- var a = document.createElement('a');
196
- a.download = filename || 'chart.svg';
197
- a.href = url;
198
- a.click();
199
- setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
200
- }
201
-
202
- function escapeXml(str) {
203
- return String(str)
204
- .replace(/&/g, '&amp;')
205
- .replace(/</g, '&lt;')
206
- .replace(/>/g, '&gt;')
207
- .replace(/"/g, '&quot;')
208
- .replace(/'/g, '&apos;');
209
- }
210
-
211
- // Attach exportSVG to the time-series prototype
212
- NM.prototype.exportSVG = exportSVG;
213
-
214
- // ---------------------------------------------------------------------------
215
- // 5. FACTORY METHOD WRAPPING
216
- // Wraps each factory method to:
217
- // a) apply theme colors from opts.theme
218
- // b) attach ResizeObserver for re-render on container resize
219
- // c) inject accessibility attributes
220
- // d) expose exportSVG on the returned instance
221
- // ---------------------------------------------------------------------------
222
-
223
- var FACTORY_METHODS = [
224
- 'create',
225
- 'bar',
226
- 'column',
227
- 'barStacked',
228
- 'lollipop',
229
- 'combo',
230
- 'donut',
231
- 'gauge',
232
- 'heatmap',
233
- 'radar',
234
- 'lineBar',
235
- 'waterfall',
236
- 'bullet',
237
- 'timeline',
238
- 'treemap',
239
- 'funnel',
240
- 'polar',
241
- 'scatter',
242
- 'sankey',
243
- 'boxplot',
244
- 'sparkline'
245
- ];
246
-
247
- var RESIZE_DEBOUNCE_MS = 150;
248
-
249
- FACTORY_METHODS.forEach(function (method) {
250
- var original = NM[method];
251
- if (typeof original !== 'function') return;
252
-
253
- NM[method] = function (selector, opts) {
254
- // a) Apply theme if specified
255
- var themedOpts = applyTheme(opts);
256
-
257
- // b) Create chart via original factory
258
- var instance = original.call(NM, selector, themedOpts);
259
- if (!instance) return instance;
260
-
261
- // c) Expose exportSVG if not already present
262
- if (typeof instance.exportSVG !== 'function') {
263
- instance.exportSVG = exportSVG.bind(instance);
264
- }
265
-
266
- // d) Accessibility
267
- var chartType = method === 'create' ? 'line' : method;
268
- injectA11y(instance, chartType);
269
-
270
- // e) ResizeObserver
271
- if (typeof ResizeObserver !== 'undefined' && instance._root) {
272
- var debouncedRender = debounce(function () {
273
- if (typeof instance._initSize === 'function') {
274
- instance._initSize();
275
- }
276
- if (typeof instance.render === 'function') {
277
- instance.render();
278
- }
279
- // Time-series also needs navigator re-render
280
- if (typeof instance._renderNav === 'function') {
281
- instance._renderNav();
282
- }
283
- }, RESIZE_DEBOUNCE_MS);
284
-
285
- var observer = new ResizeObserver(function (entries) {
286
- if (!entries || !entries.length) return;
287
- debouncedRender();
288
- });
289
-
290
- // Observe the root element (the mc-root div)
291
- observer.observe(instance._root);
292
-
293
- // Store on instance for cleanup
294
- instance._resizeObserver = observer;
295
-
296
- // Patch destroy to disconnect observer
297
- var originalDestroy = instance.destroy;
298
- instance.destroy = function () {
299
- if (instance._resizeObserver) {
300
- instance._resizeObserver.disconnect();
301
- instance._resizeObserver = null;
302
- }
303
- if (typeof originalDestroy === 'function') {
304
- originalDestroy.call(instance);
305
- }
306
- };
307
- }
308
-
309
- return instance;
310
- };
311
- });
312
-
313
- // ---------------------------------------------------------------------------
314
- // UMD export (mirror pattern from nmcharts.js / nmcharts-categories.js)
315
- // ---------------------------------------------------------------------------
316
- if (typeof module === 'object' && module.exports) {
317
- module.exports = NM;
318
- }
319
-
320
- })(typeof window !== 'undefined' ? window : typeof globalThis !== 'undefined' ? globalThis : this);