jomasego commited on
Commit
b2ca876
·
verified ·
1 Parent(s): 8ff3681

Upload static/js/charts_module.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. static/js/charts_module.js +742 -0
static/js/charts_module.js ADDED
@@ -0,0 +1,742 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Advanced Chart Visualizations
3
+ * Provides different chart types for international trade data visualization
4
+ */
5
+
6
+ const TradeCharts = (function() {
7
+ // Store chart instances to destroy when needed
8
+ const chartInstances = {};
9
+
10
+ // Color schemes for different chart types
11
+ const colorSchemes = {
12
+ default: [
13
+ 'rgba(25, 118, 210, 0.7)', // Primary blue
14
+ 'rgba(229, 57, 53, 0.7)', // Red
15
+ 'rgba(67, 160, 71, 0.7)', // Green
16
+ 'rgba(251, 192, 45, 0.7)', // Yellow
17
+ 'rgba(156, 39, 176, 0.7)', // Purple
18
+ 'rgba(0, 188, 212, 0.7)', // Cyan
19
+ 'rgba(255, 152, 0, 0.7)', // Orange
20
+ 'rgba(121, 85, 72, 0.7)', // Brown
21
+ 'rgba(96, 125, 139, 0.7)', // Blue Grey
22
+ 'rgba(233, 30, 99, 0.7)' // Pink
23
+ ],
24
+ borders: [
25
+ 'rgba(25, 118, 210, 1)', // Primary blue
26
+ 'rgba(229, 57, 53, 1)', // Red
27
+ 'rgba(67, 160, 71, 1)', // Green
28
+ 'rgba(251, 192, 45, 1)', // Yellow
29
+ 'rgba(156, 39, 176, 1)', // Purple
30
+ 'rgba(0, 188, 212, 1)', // Cyan
31
+ 'rgba(255, 152, 0, 1)', // Orange
32
+ 'rgba(121, 85, 72, 1)', // Brown
33
+ 'rgba(96, 125, 139, 1)', // Blue Grey
34
+ 'rgba(233, 30, 99, 1)' // Pink
35
+ ],
36
+ gradients: function(ctx) {
37
+ return [
38
+ createGradient(ctx, [25, 118, 210]),
39
+ createGradient(ctx, [229, 57, 53]),
40
+ createGradient(ctx, [67, 160, 71]),
41
+ createGradient(ctx, [251, 192, 45]),
42
+ createGradient(ctx, [156, 39, 176]),
43
+ createGradient(ctx, [0, 188, 212]),
44
+ createGradient(ctx, [255, 152, 0]),
45
+ createGradient(ctx, [121, 85, 72]),
46
+ createGradient(ctx, [96, 125, 139]),
47
+ createGradient(ctx, [233, 30, 99])
48
+ ];
49
+ }
50
+ };
51
+
52
+ // Create a gradient color
53
+ function createGradient(ctx, rgbColor) {
54
+ const gradient = ctx.createLinearGradient(0, 0, 0, 400);
55
+ gradient.addColorStop(0, `rgba(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]}, 0.8)`);
56
+ gradient.addColorStop(1, `rgba(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]}, 0.2)`);
57
+ return gradient;
58
+ }
59
+
60
+ // Load Chart.js if not present
61
+ function ensureChartJsLoaded() {
62
+ return new Promise((resolve, reject) => {
63
+ if (window.Chart) {
64
+ resolve(window.Chart);
65
+ return;
66
+ }
67
+
68
+ const script = document.createElement('script');
69
+ script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js';
70
+ script.onload = () => resolve(window.Chart);
71
+ script.onerror = () => reject(new Error('Failed to load Chart.js'));
72
+ document.body.appendChild(script);
73
+ });
74
+ }
75
+
76
+ // Helper function to destroy existing chart
77
+ function destroyChart(containerId) {
78
+ if (chartInstances[containerId]) {
79
+ chartInstances[containerId].destroy();
80
+ delete chartInstances[containerId];
81
+ }
82
+ }
83
+
84
+ // Helper to extract top N items
85
+ function getTopItems(data, valueField, labelField, n = 10) {
86
+ return [...data]
87
+ .sort((a, b) => b[valueField] - a[valueField])
88
+ .slice(0, n)
89
+ .map(item => ({
90
+ value: item[valueField],
91
+ label: item[labelField]
92
+ }));
93
+ }
94
+
95
+ // Create bar chart
96
+ async function createBarChart(containerId, data, options = {}) {
97
+ await ensureChartJsLoaded();
98
+
99
+ const container = document.getElementById(containerId);
100
+ if (!container) return null;
101
+
102
+ // Create or get canvas
103
+ let canvas = container.querySelector('canvas');
104
+ if (!canvas) {
105
+ canvas = document.createElement('canvas');
106
+ container.innerHTML = '';
107
+ container.appendChild(canvas);
108
+ }
109
+
110
+ destroyChart(containerId);
111
+
112
+ // Default options
113
+ const defaults = {
114
+ valueField: 'value',
115
+ labelField: 'country',
116
+ title: 'Trade Data',
117
+ horizontal: false,
118
+ limit: 10,
119
+ showLegend: false,
120
+ animation: true
121
+ };
122
+
123
+ const chartOptions = { ...defaults, ...options };
124
+
125
+ // Prepare data - limit to top N items and process data
126
+ let chartData;
127
+ if (Array.isArray(data.rows)) {
128
+ chartData = getTopItems(
129
+ data.rows,
130
+ chartOptions.valueField,
131
+ chartOptions.labelField,
132
+ chartOptions.limit
133
+ );
134
+ } else if (Array.isArray(data)) {
135
+ chartData = getTopItems(
136
+ data,
137
+ chartOptions.valueField,
138
+ chartOptions.labelField,
139
+ chartOptions.limit
140
+ );
141
+ } else {
142
+ console.error('Invalid data format for bar chart');
143
+ return null;
144
+ }
145
+
146
+ // Get context
147
+ const ctx = canvas.getContext('2d');
148
+
149
+ // Create chart
150
+ chartInstances[containerId] = new Chart(ctx, {
151
+ type: chartOptions.horizontal ? 'horizontalBar' : 'bar',
152
+ data: {
153
+ labels: chartData.map(item => item.label),
154
+ datasets: [{
155
+ label: chartOptions.title,
156
+ data: chartData.map(item => item.value),
157
+ backgroundColor: colorSchemes.default,
158
+ borderColor: colorSchemes.borders,
159
+ borderWidth: 1
160
+ }]
161
+ },
162
+ options: {
163
+ indexAxis: chartOptions.horizontal ? 'y' : 'x',
164
+ responsive: true,
165
+ maintainAspectRatio: false,
166
+ plugins: {
167
+ legend: {
168
+ display: chartOptions.showLegend
169
+ },
170
+ title: {
171
+ display: true,
172
+ text: chartOptions.title,
173
+ font: {
174
+ size: 16
175
+ }
176
+ },
177
+ tooltip: {
178
+ callbacks: {
179
+ label: function(context) {
180
+ let label = context.dataset.label || '';
181
+ if (label) {
182
+ label += ': ';
183
+ }
184
+ if (context.parsed.y !== null) {
185
+ label += new Intl.NumberFormat().format(
186
+ chartOptions.horizontal ? context.parsed.x : context.parsed.y
187
+ );
188
+ }
189
+ return label;
190
+ }
191
+ }
192
+ }
193
+ },
194
+ animation: chartOptions.animation,
195
+ scales: {
196
+ y: {
197
+ beginAtZero: true,
198
+ ticks: {
199
+ callback: function(value) {
200
+ if (value >= 1000000000) {
201
+ return (value / 1000000000).toFixed(1) + 'B';
202
+ } else if (value >= 1000000) {
203
+ return (value / 1000000).toFixed(1) + 'M';
204
+ } else if (value >= 1000) {
205
+ return (value / 1000).toFixed(1) + 'K';
206
+ }
207
+ return value;
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
213
+ });
214
+
215
+ return chartInstances[containerId];
216
+ }
217
+
218
+ // Create pie chart
219
+ async function createPieChart(containerId, data, options = {}) {
220
+ await ensureChartJsLoaded();
221
+
222
+ const container = document.getElementById(containerId);
223
+ if (!container) return null;
224
+
225
+ // Create or get canvas
226
+ let canvas = container.querySelector('canvas');
227
+ if (!canvas) {
228
+ canvas = document.createElement('canvas');
229
+ container.innerHTML = '';
230
+ container.appendChild(canvas);
231
+ }
232
+
233
+ destroyChart(containerId);
234
+
235
+ // Default options
236
+ const defaults = {
237
+ valueField: 'value',
238
+ labelField: 'country',
239
+ title: 'Trade Distribution',
240
+ limit: 10,
241
+ showLegend: true,
242
+ animation: true,
243
+ doughnut: false
244
+ };
245
+
246
+ const chartOptions = { ...defaults, ...options };
247
+
248
+ // Prepare data - limit to top N items
249
+ let chartData;
250
+ if (Array.isArray(data.rows)) {
251
+ chartData = getTopItems(
252
+ data.rows,
253
+ chartOptions.valueField,
254
+ chartOptions.labelField,
255
+ chartOptions.limit
256
+ );
257
+ } else if (Array.isArray(data)) {
258
+ chartData = getTopItems(
259
+ data,
260
+ chartOptions.valueField,
261
+ chartOptions.labelField,
262
+ chartOptions.limit
263
+ );
264
+ } else {
265
+ console.error('Invalid data format for pie chart');
266
+ return null;
267
+ }
268
+
269
+ // Get context
270
+ const ctx = canvas.getContext('2d');
271
+
272
+ // Create chart
273
+ chartInstances[containerId] = new Chart(ctx, {
274
+ type: chartOptions.doughnut ? 'doughnut' : 'pie',
275
+ data: {
276
+ labels: chartData.map(item => item.label),
277
+ datasets: [{
278
+ data: chartData.map(item => item.value),
279
+ backgroundColor: colorSchemes.default,
280
+ borderColor: colorSchemes.borders,
281
+ borderWidth: 1
282
+ }]
283
+ },
284
+ options: {
285
+ responsive: true,
286
+ maintainAspectRatio: false,
287
+ plugins: {
288
+ legend: {
289
+ display: chartOptions.showLegend,
290
+ position: 'right'
291
+ },
292
+ title: {
293
+ display: true,
294
+ text: chartOptions.title,
295
+ font: {
296
+ size: 16
297
+ }
298
+ },
299
+ tooltip: {
300
+ callbacks: {
301
+ label: function(context) {
302
+ const label = context.label || '';
303
+ const value = context.raw;
304
+ const total = context.dataset.data.reduce((a, b) => a + b, 0);
305
+ const percentage = ((value / total) * 100).toFixed(1);
306
+ return `${label}: ${new Intl.NumberFormat().format(value)} (${percentage}%)`;
307
+ }
308
+ }
309
+ }
310
+ },
311
+ animation: chartOptions.animation
312
+ }
313
+ });
314
+
315
+ return chartInstances[containerId];
316
+ }
317
+
318
+ // Create line chart
319
+ async function createLineChart(containerId, data, options = {}) {
320
+ await ensureChartJsLoaded();
321
+
322
+ const container = document.getElementById(containerId);
323
+ if (!container) return null;
324
+
325
+ // Create or get canvas
326
+ let canvas = container.querySelector('canvas');
327
+ if (!canvas) {
328
+ canvas = document.createElement('canvas');
329
+ container.innerHTML = '';
330
+ container.appendChild(canvas);
331
+ }
332
+
333
+ destroyChart(containerId);
334
+
335
+ // Default options
336
+ const defaults = {
337
+ valueField: 'value',
338
+ labelField: 'year',
339
+ title: 'Trade Trends',
340
+ showLegend: true,
341
+ animation: true,
342
+ fill: true,
343
+ seriesField: null, // If provided, creates multiple series based on this field
344
+ timeScale: false
345
+ };
346
+
347
+ const chartOptions = { ...defaults, ...options };
348
+
349
+ // Get context
350
+ const ctx = canvas.getContext('2d');
351
+
352
+ // Prepare datasets
353
+ let datasets = [];
354
+
355
+ if (chartOptions.seriesField) {
356
+ // Group data by series field
357
+ const seriesData = {};
358
+ const sourceData = Array.isArray(data.rows) ? data.rows : data;
359
+
360
+ sourceData.forEach(item => {
361
+ const seriesKey = item[chartOptions.seriesField];
362
+ if (!seriesData[seriesKey]) {
363
+ seriesData[seriesKey] = [];
364
+ }
365
+ seriesData[seriesKey].push({
366
+ x: item[chartOptions.labelField],
367
+ y: item[chartOptions.valueField]
368
+ });
369
+ });
370
+
371
+ // Create a dataset for each series
372
+ let colorIndex = 0;
373
+ for (const seriesKey in seriesData) {
374
+ datasets.push({
375
+ label: seriesKey,
376
+ data: seriesData[seriesKey],
377
+ backgroundColor: chartOptions.fill ? colorSchemes.gradients(ctx)[colorIndex % 10] : colorSchemes.default[colorIndex % 10],
378
+ borderColor: colorSchemes.borders[colorIndex % 10],
379
+ borderWidth: 2,
380
+ fill: chartOptions.fill,
381
+ tension: 0.1
382
+ });
383
+ colorIndex++;
384
+ }
385
+ } else {
386
+ // Single series
387
+ const sourceData = Array.isArray(data.rows) ? data.rows : data;
388
+
389
+ // Sort data by label (typically year)
390
+ sourceData.sort((a, b) => {
391
+ if (chartOptions.timeScale) {
392
+ return new Date(a[chartOptions.labelField]) - new Date(b[chartOptions.labelField]);
393
+ }
394
+ return a[chartOptions.labelField] - b[chartOptions.labelField];
395
+ });
396
+
397
+ datasets.push({
398
+ label: chartOptions.title,
399
+ data: sourceData.map(item => ({
400
+ x: item[chartOptions.labelField],
401
+ y: item[chartOptions.valueField]
402
+ })),
403
+ backgroundColor: chartOptions.fill ? colorSchemes.gradients(ctx)[0] : colorSchemes.default[0],
404
+ borderColor: colorSchemes.borders[0],
405
+ borderWidth: 2,
406
+ fill: chartOptions.fill,
407
+ tension: 0.1
408
+ });
409
+ }
410
+
411
+ // Create chart
412
+ chartInstances[containerId] = new Chart(ctx, {
413
+ type: 'line',
414
+ data: {
415
+ datasets: datasets
416
+ },
417
+ options: {
418
+ responsive: true,
419
+ maintainAspectRatio: false,
420
+ plugins: {
421
+ legend: {
422
+ display: chartOptions.showLegend
423
+ },
424
+ title: {
425
+ display: true,
426
+ text: chartOptions.title,
427
+ font: {
428
+ size: 16
429
+ }
430
+ },
431
+ tooltip: {
432
+ callbacks: {
433
+ label: function(context) {
434
+ let label = context.dataset.label || '';
435
+ if (label) {
436
+ label += ': ';
437
+ }
438
+ if (context.parsed.y !== null) {
439
+ label += new Intl.NumberFormat().format(context.parsed.y);
440
+ }
441
+ return label;
442
+ }
443
+ }
444
+ }
445
+ },
446
+ animation: chartOptions.animation,
447
+ scales: {
448
+ x: {
449
+ type: chartOptions.timeScale ? 'time' : 'category',
450
+ time: chartOptions.timeScale ? {
451
+ unit: 'year',
452
+ displayFormats: {
453
+ year: 'yyyy'
454
+ }
455
+ } : undefined
456
+ },
457
+ y: {
458
+ beginAtZero: true,
459
+ ticks: {
460
+ callback: function(value) {
461
+ if (value >= 1000000000) {
462
+ return (value / 1000000000).toFixed(1) + 'B';
463
+ } else if (value >= 1000000) {
464
+ return (value / 1000000).toFixed(1) + 'M';
465
+ } else if (value >= 1000) {
466
+ return (value / 1000).toFixed(1) + 'K';
467
+ }
468
+ return value;
469
+ }
470
+ }
471
+ }
472
+ }
473
+ }
474
+ });
475
+
476
+ return chartInstances[containerId];
477
+ }
478
+
479
+ // Create treemap for product/country hierarchies
480
+ async function createTreemap(containerId, data, options = {}) {
481
+ // This is a simplified treemap using divs since Chart.js doesn't have built-in treemap
482
+ const container = document.getElementById(containerId);
483
+ if (!container) return null;
484
+
485
+ // Default options
486
+ const defaults = {
487
+ valueField: 'value',
488
+ labelField: 'country',
489
+ title: 'Trade Distribution',
490
+ limit: 20
491
+ };
492
+
493
+ const chartOptions = { ...defaults, ...options };
494
+
495
+ // Prepare data - limit to top N items
496
+ let chartData;
497
+ if (Array.isArray(data.rows)) {
498
+ chartData = getTopItems(
499
+ data.rows,
500
+ chartOptions.valueField,
501
+ chartOptions.labelField,
502
+ chartOptions.limit
503
+ );
504
+ } else if (Array.isArray(data)) {
505
+ chartData = getTopItems(
506
+ data,
507
+ chartOptions.valueField,
508
+ chartOptions.labelField,
509
+ chartOptions.limit
510
+ );
511
+ } else {
512
+ console.error('Invalid data format for treemap');
513
+ return null;
514
+ }
515
+
516
+ // Calculate total for percentages
517
+ const total = chartData.reduce((sum, item) => sum + item.value, 0);
518
+
519
+ // Create treemap container
520
+ container.innerHTML = `
521
+ <div class="treemap-title">${chartOptions.title}</div>
522
+ <div class="treemap-container"></div>
523
+ `;
524
+
525
+ const treemapContainer = container.querySelector('.treemap-container');
526
+ treemapContainer.style.display = 'flex';
527
+ treemapContainer.style.flexWrap = 'wrap';
528
+ treemapContainer.style.height = '400px';
529
+ treemapContainer.style.position = 'relative';
530
+
531
+ // Create rectangles
532
+ chartData.forEach((item, index) => {
533
+ const percentage = (item.value / total * 100).toFixed(1);
534
+ const div = document.createElement('div');
535
+ div.className = 'treemap-item';
536
+ div.style.backgroundColor = colorSchemes.default[index % colorSchemes.default.length];
537
+ div.style.color = '#fff';
538
+ div.style.padding = '8px';
539
+ div.style.boxSizing = 'border-box';
540
+ div.style.overflow = 'hidden';
541
+ div.style.fontSize = '12px';
542
+ div.style.position = 'relative';
543
+ div.style.flexGrow = item.value;
544
+
545
+ // Size must be proportional to value
546
+ div.style.width = `${Math.sqrt(percentage)}%`;
547
+ div.style.height = `${Math.sqrt(percentage) * 2}%`;
548
+ div.style.margin = '2px';
549
+
550
+ // Text with truncation
551
+ div.innerHTML = `
552
+ <div style="font-weight:bold;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
553
+ ${item.label}
554
+ </div>
555
+ <div>${percentage}%</div>
556
+ `;
557
+
558
+ // Tooltip on hover
559
+ div.title = `${item.label}: ${new Intl.NumberFormat().format(item.value)} (${percentage}%)`;
560
+
561
+ treemapContainer.appendChild(div);
562
+ });
563
+
564
+ return treemapContainer;
565
+ }
566
+
567
+ // Create a world map visualization
568
+ async function createWorldMapChart(containerId, data, options = {}) {
569
+ // Load leaflet script if not already loaded
570
+ if (!window.L) {
571
+ await new Promise((resolve, reject) => {
572
+ // Load CSS
573
+ const leafletCss = document.createElement('link');
574
+ leafletCss.rel = 'stylesheet';
575
+ leafletCss.href = 'https://unpkg.com/leaflet/dist/leaflet.css';
576
+ document.head.appendChild(leafletCss);
577
+
578
+ // Load script
579
+ const script = document.createElement('script');
580
+ script.src = 'https://unpkg.com/leaflet/dist/leaflet.js';
581
+ script.onload = resolve;
582
+ script.onerror = reject;
583
+ document.body.appendChild(script);
584
+ });
585
+ }
586
+
587
+ const container = document.getElementById(containerId);
588
+ if (!container) return null;
589
+
590
+ // Default options
591
+ const defaults = {
592
+ valueField: 'value',
593
+ labelField: 'country',
594
+ countryCodeField: 'code',
595
+ title: 'World Trade Map',
596
+ colorScale: ['#e6f7ff', '#0077be'],
597
+ zoom: 2
598
+ };
599
+
600
+ const chartOptions = { ...defaults, ...options };
601
+
602
+ // Set container height if not already set
603
+ if (!container.style.height || container.style.height === 'auto') {
604
+ container.style.height = '400px';
605
+ }
606
+
607
+ // Clear previous map
608
+ container.innerHTML = '';
609
+
610
+ // Create map
611
+ const map = L.map(containerId).setView([20, 0], chartOptions.zoom);
612
+
613
+ // Add tile layer
614
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
615
+ maxZoom: 19,
616
+ attribution: '© OpenStreetMap contributors'
617
+ }).addTo(map);
618
+
619
+ // Process data
620
+ const sourceData = Array.isArray(data.rows) ? data.rows : data;
621
+
622
+ // Find min/max for color scaling
623
+ const values = sourceData.map(item => item[chartOptions.valueField]);
624
+ const min = Math.min(...values);
625
+ const max = Math.max(...values);
626
+
627
+ // Function to compute color based on value
628
+ function getColor(value) {
629
+ const ratio = (value - min) / (max - min || 1);
630
+
631
+ // Linear interpolation between start and end colors
632
+ const startColor = chartOptions.colorScale[0];
633
+ const endColor = chartOptions.colorScale[1];
634
+
635
+ // Parse hex colors
636
+ const startRGB = {
637
+ r: parseInt(startColor.slice(1, 3), 16),
638
+ g: parseInt(startColor.slice(3, 5), 16),
639
+ b: parseInt(startColor.slice(5, 7), 16)
640
+ };
641
+
642
+ const endRGB = {
643
+ r: parseInt(endColor.slice(1, 3), 16),
644
+ g: parseInt(endColor.slice(3, 5), 16),
645
+ b: parseInt(endColor.slice(5, 7), 16)
646
+ };
647
+
648
+ // Interpolate
649
+ const r = Math.round(startRGB.r + ratio * (endRGB.r - startRGB.r));
650
+ const g = Math.round(startRGB.g + ratio * (endRGB.g - startRGB.g));
651
+ const b = Math.round(startRGB.b + ratio * (endRGB.b - startRGB.b));
652
+
653
+ return `rgb(${r}, ${g}, ${b})`;
654
+ }
655
+
656
+ // Add country polygons if country GeoJSON is available
657
+ // For now we'll use circles at country coordinates as a simplified version
658
+
659
+ // Find coordinates for countries (simplified - real app would use GeoJSON)
660
+ const countryCoordinates = {
661
+ // Sample coordinates for major countries
662
+ '842': [37.0902, -95.7129], // USA
663
+ '156': [35.8617, 104.1954], // China
664
+ '276': [51.1657, 10.4515], // Germany
665
+ '392': [36.2048, 138.2529], // Japan
666
+ '826': [55.3781, -3.4360], // UK
667
+ '250': [46.2276, 2.2137], // France
668
+ '380': [41.8719, 12.5674], // Italy
669
+ '124': [56.1304, -106.3468], // Canada
670
+ '410': [35.9078, 127.7669], // South Korea
671
+ '484': [23.6345, -102.5528], // Mexico
672
+
673
+ // Default coordinates for unknown countries
674
+ 'default': [0, 0]
675
+ };
676
+
677
+ // Add circles for each country
678
+ sourceData.forEach(item => {
679
+ const code = item[chartOptions.countryCodeField];
680
+ const value = item[chartOptions.valueField];
681
+ const coords = countryCoordinates[code] || countryCoordinates.default;
682
+
683
+ if (coords[0] !== 0 || coords[1] !== 0) {
684
+ // Size circle based on value
685
+ const radius = Math.max(5, Math.min(20, 5 + (value - min) / (max - min || 1) * 15));
686
+
687
+ L.circleMarker(coords, {
688
+ radius: radius,
689
+ fillColor: getColor(value),
690
+ color: '#fff',
691
+ weight: 1,
692
+ opacity: 1,
693
+ fillOpacity: 0.8
694
+ })
695
+ .addTo(map)
696
+ .bindPopup(`
697
+ <strong>${item[chartOptions.labelField]}</strong><br>
698
+ Value: ${new Intl.NumberFormat().format(value)}
699
+ `);
700
+ }
701
+ });
702
+
703
+ // Add legend
704
+ const legend = L.control({ position: 'bottomright' });
705
+
706
+ legend.onAdd = function() {
707
+ const div = L.DomUtil.create('div', 'info legend');
708
+ div.style.backgroundColor = 'white';
709
+ div.style.padding = '10px';
710
+ div.style.borderRadius = '5px';
711
+ div.style.boxShadow = '0 0 5px rgba(0,0,0,0.2)';
712
+
713
+ div.innerHTML = `
714
+ <div style="font-weight:bold;margin-bottom:5px;">${chartOptions.title}</div>
715
+ <div style="display:flex;align-items:center;margin-bottom:5px;">
716
+ <div style="width:20px;height:20px;background:${chartOptions.colorScale[0]};margin-right:5px;"></div>
717
+ <span>${new Intl.NumberFormat().format(min)}</span>
718
+ </div>
719
+ <div style="display:flex;align-items:center;">
720
+ <div style="width:20px;height:20px;background:${chartOptions.colorScale[1]};margin-right:5px;"></div>
721
+ <span>${new Intl.NumberFormat().format(max)}</span>
722
+ </div>
723
+ `;
724
+
725
+ return div;
726
+ };
727
+
728
+ legend.addTo(map);
729
+
730
+ return map;
731
+ }
732
+
733
+ // Public API
734
+ return {
735
+ createBarChart,
736
+ createPieChart,
737
+ createLineChart,
738
+ createTreemap,
739
+ createWorldMapChart,
740
+ destroyChart
741
+ };
742
+ })();