@allternit/viz 0.1.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.
Files changed (45) hide show
  1. package/README.md +217 -0
  2. package/dist/charts/canvas-renderer.d.ts +40 -0
  3. package/dist/charts/canvas-renderer.d.ts.bak +40 -0
  4. package/dist/charts/canvas-renderer.d.ts.map +1 -0
  5. package/dist/charts/canvas-renderer.js +580 -0
  6. package/dist/charts/canvas-renderer.js.bak +580 -0
  7. package/dist/charts/canvas-renderer.js.map +1 -0
  8. package/dist/charts/svg-renderer.d.ts +21 -0
  9. package/dist/charts/svg-renderer.d.ts.bak +21 -0
  10. package/dist/charts/svg-renderer.d.ts.map +1 -0
  11. package/dist/charts/svg-renderer.js +285 -0
  12. package/dist/charts/svg-renderer.js.bak +285 -0
  13. package/dist/charts/svg-renderer.js.map +1 -0
  14. package/dist/index.d.ts +34 -0
  15. package/dist/index.d.ts.bak +34 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +35 -0
  18. package/dist/index.js.bak +35 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/types.d.ts +342 -0
  21. package/dist/types.d.ts.bak +342 -0
  22. package/dist/types.d.ts.map +1 -0
  23. package/dist/types.js +39 -0
  24. package/dist/types.js.bak +39 -0
  25. package/dist/types.js.map +1 -0
  26. package/dist/utils/data-processor.d.ts +64 -0
  27. package/dist/utils/data-processor.d.ts.bak +64 -0
  28. package/dist/utils/data-processor.d.ts.map +1 -0
  29. package/dist/utils/data-processor.js +326 -0
  30. package/dist/utils/data-processor.js.bak +326 -0
  31. package/dist/utils/data-processor.js.map +1 -0
  32. package/package.json +41 -0
  33. package/src/charts/canvas-renderer.ts +735 -0
  34. package/src/charts/canvas-renderer.ts.bak +735 -0
  35. package/src/charts/svg-renderer.test.ts.bak +837 -0
  36. package/src/charts/svg-renderer.ts +380 -0
  37. package/src/charts/svg-renderer.ts.bak +380 -0
  38. package/src/index.ts +64 -0
  39. package/src/index.ts.bak +64 -0
  40. package/src/types.ts +410 -0
  41. package/src/types.ts.bak +410 -0
  42. package/src/utils/data-processor.test.ts.bak +720 -0
  43. package/src/utils/data-processor.ts +420 -0
  44. package/src/utils/data-processor.ts.bak +420 -0
  45. package/tsconfig.json +22 -0
@@ -0,0 +1,837 @@
1
+ /**
2
+ * SVG Renderer Tests
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { createSVGRenderer, globalSVGRenderer } from './svg-renderer';
7
+ import type { ChartConfig, DataSeries } from '../types';
8
+
9
+ describe('SVG Renderer', () => {
10
+ describe('createSVGRenderer', () => {
11
+ it('should create a renderer instance', () => {
12
+ const renderer = createSVGRenderer();
13
+ expect(renderer).toBeDefined();
14
+ expect(typeof renderer.render).toBe('function');
15
+ expect(typeof renderer.getMetadata).toBe('function');
16
+ });
17
+ });
18
+
19
+ describe('globalSVGRenderer', () => {
20
+ it('should export a global renderer instance', () => {
21
+ expect(globalSVGRenderer).toBeDefined();
22
+ expect(typeof globalSVGRenderer.render).toBe('function');
23
+ });
24
+ });
25
+
26
+ describe('bar chart rendering', () => {
27
+ const renderer = createSVGRenderer();
28
+
29
+ it('should render a basic bar chart', () => {
30
+ const config: ChartConfig = {
31
+ type: 'bar',
32
+ title: 'Sales by Month',
33
+ width: 800,
34
+ height: 400,
35
+ };
36
+
37
+ const series: DataSeries[] = [
38
+ {
39
+ id: 'sales',
40
+ name: 'Sales',
41
+ data: [
42
+ { x: 'Jan', y: 100 },
43
+ { x: 'Feb', y: 150 },
44
+ { x: 'Mar', y: 200 },
45
+ ],
46
+ },
47
+ ];
48
+
49
+ const svg = renderer.render(config, series);
50
+
51
+ expect(svg).toContain('<svg');
52
+ expect(svg).toContain('</svg>');
53
+ expect(svg).toContain('Sales by Month');
54
+ expect(svg).toContain('<rect');
55
+ });
56
+
57
+ it('should render multiple data series', () => {
58
+ const config: ChartConfig = {
59
+ type: 'bar',
60
+ width: 800,
61
+ height: 400,
62
+ };
63
+
64
+ const series: DataSeries[] = [
65
+ {
66
+ id: 'sales2023',
67
+ name: 'Sales 2023',
68
+ data: [
69
+ { x: 'Q1', y: 100 },
70
+ { x: 'Q2', y: 150 },
71
+ ],
72
+ },
73
+ {
74
+ id: 'sales2024',
75
+ name: 'Sales 2024',
76
+ data: [
77
+ { x: 'Q1', y: 120 },
78
+ { x: 'Q2', y: 180 },
79
+ ],
80
+ },
81
+ ];
82
+
83
+ const svg = renderer.render(config, series);
84
+
85
+ // Should contain rectangles for bars
86
+ const rectMatches = svg.match(/<rect/g);
87
+ expect(rectMatches?.length).toBeGreaterThanOrEqual(4);
88
+ });
89
+
90
+ it('should handle empty data series', () => {
91
+ const config: ChartConfig = {
92
+ type: 'bar',
93
+ width: 800,
94
+ height: 400,
95
+ };
96
+
97
+ const svg = renderer.render(config, []);
98
+
99
+ expect(svg).toContain('<svg');
100
+ expect(svg).toContain('</svg>');
101
+ });
102
+
103
+ it('should use custom series colors', () => {
104
+ const config: ChartConfig = {
105
+ type: 'bar',
106
+ width: 800,
107
+ height: 400,
108
+ };
109
+
110
+ const series: DataSeries[] = [
111
+ {
112
+ id: 'sales',
113
+ name: 'Sales',
114
+ color: '#ff0000',
115
+ data: [{ x: 'Jan', y: 100 }],
116
+ },
117
+ ];
118
+
119
+ const svg = renderer.render(config, series);
120
+
121
+ expect(svg).toContain('#ff0000');
122
+ });
123
+ });
124
+
125
+ describe('line chart rendering', () => {
126
+ const renderer = createSVGRenderer();
127
+
128
+ it('should render a basic line chart', () => {
129
+ const config: ChartConfig = {
130
+ type: 'line',
131
+ title: 'Revenue Trend',
132
+ width: 800,
133
+ height: 400,
134
+ };
135
+
136
+ const series: DataSeries[] = [
137
+ {
138
+ id: 'revenue',
139
+ name: 'Revenue',
140
+ data: [
141
+ { x: 'Jan', y: 100 },
142
+ { x: 'Feb', y: 150 },
143
+ { x: 'Mar', y: 200 },
144
+ ],
145
+ },
146
+ ];
147
+
148
+ const svg = renderer.render(config, series);
149
+
150
+ expect(svg).toContain('<svg');
151
+ expect(svg).toContain('Revenue Trend');
152
+ expect(svg).toContain('<path');
153
+ expect(svg).toContain('fill="none"');
154
+ });
155
+
156
+ it('should render data points when dataLabels is enabled', () => {
157
+ const config: ChartConfig = {
158
+ type: 'line',
159
+ width: 800,
160
+ height: 400,
161
+ dataLabels: true,
162
+ };
163
+
164
+ const series: DataSeries[] = [
165
+ {
166
+ id: 'revenue',
167
+ name: 'Revenue',
168
+ data: [
169
+ { x: 'Jan', y: 100 },
170
+ { x: 'Feb', y: 150 },
171
+ ],
172
+ },
173
+ ];
174
+
175
+ const svg = renderer.render(config, series);
176
+
177
+ expect(svg).toContain('<circle');
178
+ });
179
+
180
+ it('should handle single data point', () => {
181
+ const config: ChartConfig = {
182
+ type: 'line',
183
+ width: 800,
184
+ height: 400,
185
+ };
186
+
187
+ const series: DataSeries[] = [
188
+ {
189
+ id: 'revenue',
190
+ name: 'Revenue',
191
+ data: [{ x: 'Jan', y: 100 }],
192
+ },
193
+ ];
194
+
195
+ const svg = renderer.render(config, series);
196
+
197
+ expect(svg).toContain('<svg');
198
+ expect(svg).toContain('<path');
199
+ });
200
+ });
201
+
202
+ describe('pie chart rendering', () => {
203
+ const renderer = createSVGRenderer();
204
+
205
+ it('should render a pie chart', () => {
206
+ const config: ChartConfig = {
207
+ type: 'pie',
208
+ title: 'Market Share',
209
+ width: 800,
210
+ height: 400,
211
+ };
212
+
213
+ const series: DataSeries[] = [
214
+ {
215
+ id: 'share',
216
+ name: 'Market Share',
217
+ data: [
218
+ { name: 'Product A', value: 40 },
219
+ { name: 'Product B', value: 30 },
220
+ { name: 'Product C', value: 30 },
221
+ ],
222
+ },
223
+ ];
224
+
225
+ const svg = renderer.render(config, series);
226
+
227
+ expect(svg).toContain('<svg');
228
+ expect(svg).toContain('Market Share');
229
+ expect(svg).toContain('<path');
230
+ });
231
+
232
+ it('should calculate correct pie slices', () => {
233
+ const config: ChartConfig = {
234
+ type: 'pie',
235
+ width: 800,
236
+ height: 400,
237
+ };
238
+
239
+ const series: DataSeries[] = [
240
+ {
241
+ id: 'share',
242
+ name: 'Market Share',
243
+ data: [
244
+ { name: 'A', value: 50 },
245
+ { name: 'B', value: 50 },
246
+ ],
247
+ },
248
+ ];
249
+
250
+ const svg = renderer.render(config, series);
251
+
252
+ // Should have 2 path elements for the slices
253
+ const pathMatches = svg.match(/<path/g);
254
+ expect(pathMatches?.length).toBeGreaterThanOrEqual(2);
255
+ });
256
+ });
257
+
258
+ describe('donut chart rendering', () => {
259
+ const renderer = createSVGRenderer();
260
+
261
+ it('should render a donut chart', () => {
262
+ const config: ChartConfig = {
263
+ type: 'donut',
264
+ title: 'Sales Distribution',
265
+ width: 800,
266
+ height: 400,
267
+ };
268
+
269
+ const series: DataSeries[] = [
270
+ {
271
+ id: 'sales',
272
+ name: 'Sales',
273
+ data: [
274
+ { name: 'Online', value: 60 },
275
+ { name: 'Retail', value: 40 },
276
+ ],
277
+ },
278
+ ];
279
+
280
+ const svg = renderer.render(config, series);
281
+
282
+ expect(svg).toContain('<svg');
283
+ expect(svg).toContain('Sales Distribution');
284
+ });
285
+
286
+ it('should create donut shape with inner radius', () => {
287
+ const config: ChartConfig = {
288
+ type: 'donut',
289
+ width: 800,
290
+ height: 400,
291
+ };
292
+
293
+ const series: DataSeries[] = [
294
+ {
295
+ id: 'sales',
296
+ name: 'Sales',
297
+ data: [
298
+ { name: 'A', value: 50 },
299
+ { name: 'B', value: 50 },
300
+ ],
301
+ },
302
+ ];
303
+
304
+ const svg = renderer.render(config, series);
305
+
306
+ // Donut paths should contain both outer and inner arc commands
307
+ expect(svg).toContain('A');
308
+ });
309
+ });
310
+
311
+ describe('area chart rendering', () => {
312
+ const renderer = createSVGRenderer();
313
+
314
+ it('should render an area chart', () => {
315
+ const config: ChartConfig = {
316
+ type: 'area',
317
+ title: 'Cumulative Growth',
318
+ width: 800,
319
+ height: 400,
320
+ };
321
+
322
+ const series: DataSeries[] = [
323
+ {
324
+ id: 'growth',
325
+ name: 'Growth',
326
+ data: [
327
+ { x: 'Q1', y: 100 },
328
+ { x: 'Q2', y: 150 },
329
+ { x: 'Q3', y: 200 },
330
+ ],
331
+ },
332
+ ];
333
+
334
+ const svg = renderer.render(config, series);
335
+
336
+ expect(svg).toContain('<svg');
337
+ expect(svg).toContain('Cumulative Growth');
338
+ expect(svg).toContain('<path');
339
+ });
340
+
341
+ it('should create closed area path', () => {
342
+ const config: ChartConfig = {
343
+ type: 'area',
344
+ width: 800,
345
+ height: 400,
346
+ };
347
+
348
+ const series: DataSeries[] = [
349
+ {
350
+ id: 'growth',
351
+ name: 'Growth',
352
+ data: [
353
+ { x: 'Q1', y: 100 },
354
+ { x: 'Q2', y: 150 },
355
+ ],
356
+ },
357
+ ];
358
+
359
+ const svg = renderer.render(config, series);
360
+
361
+ // Area should close the path
362
+ expect(svg).toContain('Z');
363
+ });
364
+ });
365
+
366
+ describe('scatter chart rendering', () => {
367
+ const renderer = createSVGRenderer();
368
+
369
+ it('should render scatter chart as placeholder (unsupported)', () => {
370
+ const config: ChartConfig = {
371
+ type: 'scatter',
372
+ title: 'Correlation Analysis',
373
+ width: 800,
374
+ height: 400,
375
+ };
376
+
377
+ const series: DataSeries[] = [
378
+ {
379
+ id: 'data',
380
+ name: 'Data Points',
381
+ data: [
382
+ { x: 1, y: 2 },
383
+ { x: 2, y: 4 },
384
+ { x: 3, y: 6 },
385
+ ],
386
+ },
387
+ ];
388
+
389
+ const svg = renderer.render(config, series);
390
+
391
+ expect(svg).toContain('<svg');
392
+ expect(svg).toContain('scatter chart');
393
+ });
394
+ });
395
+
396
+ describe('theme support', () => {
397
+ const renderer = createSVGRenderer();
398
+
399
+ it('should use light theme by default', () => {
400
+ const config: ChartConfig = {
401
+ type: 'bar',
402
+ width: 800,
403
+ height: 400,
404
+ };
405
+
406
+ const series: DataSeries[] = [
407
+ {
408
+ id: 'data',
409
+ name: 'Data',
410
+ data: [{ x: 'A', y: 100 }],
411
+ },
412
+ ];
413
+
414
+ const svg = renderer.render(config, series);
415
+
416
+ // Light theme background
417
+ expect(svg).toContain('#ffffff');
418
+ });
419
+
420
+ it('should use dark theme when specified', () => {
421
+ const config: ChartConfig = {
422
+ type: 'bar',
423
+ width: 800,
424
+ height: 400,
425
+ theme: 'dark',
426
+ };
427
+
428
+ const series: DataSeries[] = [
429
+ {
430
+ id: 'data',
431
+ name: 'Data',
432
+ data: [{ x: 'A', y: 100 }],
433
+ },
434
+ ];
435
+
436
+ const svg = renderer.render(config, series);
437
+
438
+ // Dark theme background
439
+ expect(svg).toContain('#1f2937');
440
+ });
441
+ });
442
+
443
+ describe('color palette generation', () => {
444
+ const renderer = createSVGRenderer();
445
+
446
+ it('should use default palette colors', () => {
447
+ const config: ChartConfig = {
448
+ type: 'bar',
449
+ width: 800,
450
+ height: 400,
451
+ };
452
+
453
+ const series: DataSeries[] = [
454
+ {
455
+ id: 'series1',
456
+ name: 'Series 1',
457
+ data: [{ x: 'A', y: 100 }],
458
+ },
459
+ ];
460
+
461
+ const svg = renderer.render(config, series);
462
+
463
+ // Should use primary color from default palette
464
+ expect(svg).toContain('#3b82f6');
465
+ });
466
+
467
+ it('should cycle through palette colors for multiple series', () => {
468
+ const config: ChartConfig = {
469
+ type: 'bar',
470
+ width: 800,
471
+ height: 400,
472
+ };
473
+
474
+ const series: DataSeries[] = [
475
+ { id: 's1', name: 'S1', data: [{ x: 'A', y: 100 }] },
476
+ { id: 's2', name: 'S2', data: [{ x: 'A', y: 200 }] },
477
+ { id: 's3', name: 'S3', data: [{ x: 'A', y: 300 }] },
478
+ ];
479
+
480
+ const svg = renderer.render(config, series);
481
+
482
+ // Should use different colors from palette
483
+ expect(svg).toContain('#3b82f6'); // blue
484
+ expect(svg).toContain('#10b981'); // green
485
+ expect(svg).toContain('#f59e0b'); // amber
486
+ });
487
+ });
488
+
489
+ describe('responsive sizing', () => {
490
+ const renderer = createSVGRenderer();
491
+
492
+ it('should use default dimensions when not specified', () => {
493
+ const config: ChartConfig = {
494
+ type: 'bar',
495
+ };
496
+
497
+ const series: DataSeries[] = [
498
+ {
499
+ id: 'data',
500
+ name: 'Data',
501
+ data: [{ x: 'A', y: 100 }],
502
+ },
503
+ ];
504
+
505
+ const svg = renderer.render(config, series);
506
+
507
+ expect(svg).toContain('width="800"');
508
+ expect(svg).toContain('height="400"');
509
+ });
510
+
511
+ it('should use custom dimensions when specified', () => {
512
+ const config: ChartConfig = {
513
+ type: 'bar',
514
+ width: 1200,
515
+ height: 600,
516
+ };
517
+
518
+ const series: DataSeries[] = [
519
+ {
520
+ id: 'data',
521
+ name: 'Data',
522
+ data: [{ x: 'A', y: 100 }],
523
+ },
524
+ ];
525
+
526
+ const svg = renderer.render(config, series);
527
+
528
+ expect(svg).toContain('width="1200"');
529
+ expect(svg).toContain('height="600"');
530
+ expect(svg).toContain('viewBox="0 0 1200 600"');
531
+ });
532
+ });
533
+
534
+ describe('axes rendering', () => {
535
+ const renderer = createSVGRenderer();
536
+
537
+ it('should render axes for bar chart', () => {
538
+ const config: ChartConfig = {
539
+ type: 'bar',
540
+ width: 800,
541
+ height: 400,
542
+ };
543
+
544
+ const series: DataSeries[] = [
545
+ {
546
+ id: 'data',
547
+ name: 'Data',
548
+ data: [
549
+ { x: 'A', y: 100 },
550
+ { x: 'B', y: 200 },
551
+ ],
552
+ },
553
+ ];
554
+
555
+ const svg = renderer.render(config, series);
556
+
557
+ // Should have axis lines
558
+ expect(svg).toContain('<line');
559
+ });
560
+
561
+ it('should render y-axis title', () => {
562
+ const config: ChartConfig = {
563
+ type: 'bar',
564
+ width: 800,
565
+ height: 400,
566
+ yAxis: {
567
+ title: 'Sales Amount',
568
+ },
569
+ };
570
+
571
+ const series: DataSeries[] = [
572
+ {
573
+ id: 'data',
574
+ name: 'Data',
575
+ data: [{ x: 'A', y: 100 }],
576
+ },
577
+ ];
578
+
579
+ const svg = renderer.render(config, series);
580
+
581
+ expect(svg).toContain('Sales Amount');
582
+ });
583
+
584
+ it('should render x-axis title', () => {
585
+ const config: ChartConfig = {
586
+ type: 'bar',
587
+ width: 800,
588
+ height: 400,
589
+ xAxis: {
590
+ title: 'Categories',
591
+ },
592
+ };
593
+
594
+ const series: DataSeries[] = [
595
+ {
596
+ id: 'data',
597
+ name: 'Data',
598
+ data: [{ x: 'A', y: 100 }],
599
+ },
600
+ ];
601
+
602
+ const svg = renderer.render(config, series);
603
+
604
+ expect(svg).toContain('Categories');
605
+ });
606
+
607
+ it('should render grid lines when enabled', () => {
608
+ const config: ChartConfig = {
609
+ type: 'line',
610
+ width: 800,
611
+ height: 400,
612
+ xAxis: {
613
+ grid: {
614
+ enabled: true,
615
+ },
616
+ },
617
+ };
618
+
619
+ const series: DataSeries[] = [
620
+ {
621
+ id: 'data',
622
+ name: 'Data',
623
+ data: [
624
+ { x: 'A', y: 100 },
625
+ { x: 'B', y: 200 },
626
+ ],
627
+ },
628
+ ];
629
+
630
+ const svg = renderer.render(config, series);
631
+
632
+ // Grid lines use stroke-dasharray
633
+ expect(svg).toContain('stroke-dasharray');
634
+ });
635
+ });
636
+
637
+ describe('metadata', () => {
638
+ const renderer = createSVGRenderer();
639
+
640
+ it('should return metadata for line chart', () => {
641
+ const metadata = renderer.getMetadata('line');
642
+
643
+ expect(metadata.type).toBe('line');
644
+ expect(metadata.name).toBe('Line Chart');
645
+ expect(metadata.axes).toContain('x');
646
+ expect(metadata.axes).toContain('y');
647
+ expect(metadata.useCases).toContain('Time series');
648
+ });
649
+
650
+ it('should return metadata for bar chart', () => {
651
+ const metadata = renderer.getMetadata('bar');
652
+
653
+ expect(metadata.type).toBe('bar');
654
+ expect(metadata.name).toBe('Bar Chart');
655
+ expect(metadata.axes).toContain('x');
656
+ expect(metadata.axes).toContain('y');
657
+ expect(metadata.useCases).toContain('Comparisons');
658
+ });
659
+
660
+ it('should return metadata for pie chart', () => {
661
+ const metadata = renderer.getMetadata('pie');
662
+
663
+ expect(metadata.type).toBe('pie');
664
+ expect(metadata.name).toBe('Pie Chart');
665
+ expect(metadata.axes).toHaveLength(0);
666
+ });
667
+
668
+ it('should return metadata for donut chart', () => {
669
+ const metadata = renderer.getMetadata('donut');
670
+
671
+ expect(metadata.type).toBe('donut');
672
+ expect(metadata.name).toBe('Donut Chart');
673
+ expect(metadata.axes).toHaveLength(0);
674
+ });
675
+
676
+ it('should return metadata for area chart', () => {
677
+ const metadata = renderer.getMetadata('area');
678
+
679
+ expect(metadata.type).toBe('area');
680
+ expect(metadata.name).toBe('Area Chart');
681
+ expect(metadata.axes).toContain('x');
682
+ expect(metadata.axes).toContain('y');
683
+ });
684
+
685
+ it('should return line metadata as default for unknown types', () => {
686
+ const metadata = renderer.getMetadata('unknown');
687
+
688
+ expect(metadata.type).toBe('line');
689
+ });
690
+ });
691
+
692
+ describe('title rendering', () => {
693
+ const renderer = createSVGRenderer();
694
+
695
+ it('should render chart title when provided', () => {
696
+ const config: ChartConfig = {
697
+ type: 'bar',
698
+ title: 'My Chart Title',
699
+ width: 800,
700
+ height: 400,
701
+ };
702
+
703
+ const series: DataSeries[] = [
704
+ {
705
+ id: 'data',
706
+ name: 'Data',
707
+ data: [{ x: 'A', y: 100 }],
708
+ },
709
+ ];
710
+
711
+ const svg = renderer.render(config, series);
712
+
713
+ expect(svg).toContain('My Chart Title');
714
+ expect(svg).toContain('font-weight="bold"');
715
+ });
716
+
717
+ it('should not render title when not provided', () => {
718
+ const config: ChartConfig = {
719
+ type: 'bar',
720
+ width: 800,
721
+ height: 400,
722
+ };
723
+
724
+ const series: DataSeries[] = [
725
+ {
726
+ id: 'data',
727
+ name: 'Data',
728
+ data: [{ x: 'A', y: 100 }],
729
+ },
730
+ ];
731
+
732
+ const svg = renderer.render(config, series);
733
+
734
+ // Should not contain font-weight="bold" for title
735
+ const boldMatches = svg.match(/font-weight="bold"/g);
736
+ expect(boldMatches).toBeNull();
737
+ });
738
+ });
739
+
740
+ describe('HTML escaping', () => {
741
+ const renderer = createSVGRenderer();
742
+
743
+ it('should escape HTML in titles', () => {
744
+ const config: ChartConfig = {
745
+ type: 'bar',
746
+ title: '<script>alert("xss")</script>',
747
+ width: 800,
748
+ height: 400,
749
+ };
750
+
751
+ const series: DataSeries[] = [
752
+ {
753
+ id: 'data',
754
+ name: 'Data',
755
+ data: [{ x: 'A', y: 100 }],
756
+ },
757
+ ];
758
+
759
+ const svg = renderer.render(config, series);
760
+
761
+ expect(svg).not.toContain('<script>');
762
+ expect(svg).toContain('&lt;script&gt;');
763
+ });
764
+
765
+ it('should escape HTML in axis titles', () => {
766
+ const config: ChartConfig = {
767
+ type: 'bar',
768
+ width: 800,
769
+ height: 400,
770
+ xAxis: {
771
+ title: '<b>Bold</b>',
772
+ },
773
+ };
774
+
775
+ const series: DataSeries[] = [
776
+ {
777
+ id: 'data',
778
+ name: 'Data',
779
+ data: [{ x: 'A', y: 100 }],
780
+ },
781
+ ];
782
+
783
+ const svg = renderer.render(config, series);
784
+
785
+ expect(svg).not.toContain('<b>');
786
+ expect(svg).toContain('&lt;b&gt;');
787
+ });
788
+ });
789
+
790
+ describe('export functionality', () => {
791
+ const renderer = createSVGRenderer();
792
+
793
+ it('should generate valid SVG markup', () => {
794
+ const config: ChartConfig = {
795
+ type: 'bar',
796
+ title: 'Export Test',
797
+ width: 800,
798
+ height: 400,
799
+ };
800
+
801
+ const series: DataSeries[] = [
802
+ {
803
+ id: 'data',
804
+ name: 'Data',
805
+ data: [{ x: 'A', y: 100 }],
806
+ },
807
+ ];
808
+
809
+ const svg = renderer.render(config, series);
810
+
811
+ // Valid SVG structure
812
+ expect(svg.startsWith('<svg')).toBe(true);
813
+ expect(svg.endsWith('</svg>')).toBe(true);
814
+ expect(svg).toContain('xmlns="http://www.w3.org/2000/svg"');
815
+ });
816
+
817
+ it('should include viewBox for scaling', () => {
818
+ const config: ChartConfig = {
819
+ type: 'bar',
820
+ width: 1200,
821
+ height: 600,
822
+ };
823
+
824
+ const series: DataSeries[] = [
825
+ {
826
+ id: 'data',
827
+ name: 'Data',
828
+ data: [{ x: 'A', y: 100 }],
829
+ },
830
+ ];
831
+
832
+ const svg = renderer.render(config, series);
833
+
834
+ expect(svg).toContain('viewBox="0 0 1200 600"');
835
+ });
836
+ });
837
+ });