@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,720 @@
1
+ /**
2
+ * Data Processor Tests
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ normalizeDataPoints,
8
+ normalizeSeries,
9
+ aggregateByCategory,
10
+ sortDataPoints,
11
+ filterByRange,
12
+ calculateStats,
13
+ processTimeSeries,
14
+ movingAverage,
15
+ detectOutliers,
16
+ normalizeValues,
17
+ calculatePercentChange,
18
+ } from './data-processor';
19
+ import type { DataPoint, DataSeries } from '../types';
20
+
21
+ describe('Data Processor', () => {
22
+ describe('normalizeDataPoints', () => {
23
+ it('should normalize data points with y property', () => {
24
+ const data: DataPoint[] = [
25
+ { x: 'A', y: 100 },
26
+ { x: 'B', y: 200 },
27
+ ];
28
+
29
+ const result = normalizeDataPoints(data);
30
+
31
+ expect(result[0].y).toBe(100);
32
+ expect(result[1].y).toBe(200);
33
+ });
34
+
35
+ it('should normalize data points with value property to y', () => {
36
+ const data: DataPoint[] = [
37
+ { name: 'A', value: 100 },
38
+ { name: 'B', value: 200 },
39
+ ];
40
+
41
+ const result = normalizeDataPoints(data);
42
+
43
+ expect(result[0].y).toBe(100);
44
+ expect(result[1].y).toBe(200);
45
+ });
46
+
47
+ it('should normalize name to x when x is missing', () => {
48
+ const data: DataPoint[] = [
49
+ { name: 'Category A', value: 100 },
50
+ { name: 'Category B', value: 200 },
51
+ ];
52
+
53
+ const result = normalizeDataPoints(data);
54
+
55
+ expect(result[0].x).toBe('Category A');
56
+ expect(result[1].x).toBe('Category B');
57
+ });
58
+
59
+ it('should use defaults for missing values', () => {
60
+ const data: DataPoint[] = [{}];
61
+
62
+ const result = normalizeDataPoints(data);
63
+
64
+ expect(result[0].x).toBe('');
65
+ expect(result[0].y).toBe(0);
66
+ });
67
+
68
+ it('should preserve additional properties', () => {
69
+ const data: DataPoint[] = [
70
+ { x: 'A', y: 100, customProp: 'test' },
71
+ ];
72
+
73
+ const result = normalizeDataPoints(data);
74
+
75
+ expect(result[0].customProp).toBe('test');
76
+ });
77
+ });
78
+
79
+ describe('normalizeSeries', () => {
80
+ it('should normalize all series data points', () => {
81
+ const series: DataSeries[] = [
82
+ {
83
+ id: 's1',
84
+ name: 'Series 1',
85
+ data: [
86
+ { name: 'A', value: 100 },
87
+ { name: 'B', value: 200 },
88
+ ],
89
+ },
90
+ ];
91
+
92
+ const result = normalizeSeries(series);
93
+
94
+ expect(result[0].data[0].y).toBe(100);
95
+ expect(result[0].data[0].x).toBe('A');
96
+ });
97
+
98
+ it('should handle multiple series', () => {
99
+ const series: DataSeries[] = [
100
+ {
101
+ id: 's1',
102
+ name: 'Series 1',
103
+ data: [{ name: 'A', value: 100 }],
104
+ },
105
+ {
106
+ id: 's2',
107
+ name: 'Series 2',
108
+ data: [{ name: 'A', value: 200 }],
109
+ },
110
+ ];
111
+
112
+ const result = normalizeSeries(series);
113
+
114
+ expect(result).toHaveLength(2);
115
+ expect(result[1].data[0].y).toBe(200);
116
+ });
117
+
118
+ it('should preserve series metadata', () => {
119
+ const series: DataSeries[] = [
120
+ {
121
+ id: 's1',
122
+ name: 'Series 1',
123
+ color: '#ff0000',
124
+ data: [{ x: 'A', y: 100 }],
125
+ },
126
+ ];
127
+
128
+ const result = normalizeSeries(series);
129
+
130
+ expect(result[0].color).toBe('#ff0000');
131
+ expect(result[0].id).toBe('s1');
132
+ });
133
+ });
134
+
135
+ describe('aggregateByCategory', () => {
136
+ it('should sum values by default', () => {
137
+ const data: DataPoint[] = [
138
+ { x: 'A', y: 100 },
139
+ { x: 'A', y: 50 },
140
+ { x: 'B', y: 200 },
141
+ ];
142
+
143
+ const result = aggregateByCategory(data);
144
+
145
+ expect(result.get('A')).toBe(150);
146
+ expect(result.get('B')).toBe(200);
147
+ });
148
+
149
+ it('should calculate average when specified', () => {
150
+ const data: DataPoint[] = [
151
+ { x: 'A', y: 100 },
152
+ { x: 'A', y: 200 },
153
+ { x: 'B', y: 300 },
154
+ ];
155
+
156
+ const result = aggregateByCategory(data, 'avg');
157
+
158
+ expect(result.get('A')).toBe(150);
159
+ expect(result.get('B')).toBe(300);
160
+ });
161
+
162
+ it('should find minimum when specified', () => {
163
+ const data: DataPoint[] = [
164
+ { x: 'A', y: 100 },
165
+ { x: 'A', y: 50 },
166
+ { x: 'A', y: 200 },
167
+ ];
168
+
169
+ const result = aggregateByCategory(data, 'min');
170
+
171
+ expect(result.get('A')).toBe(50);
172
+ });
173
+
174
+ it('should find maximum when specified', () => {
175
+ const data: DataPoint[] = [
176
+ { x: 'A', y: 100 },
177
+ { x: 'A', y: 200 },
178
+ { x: 'A', y: 50 },
179
+ ];
180
+
181
+ const result = aggregateByCategory(data, 'max');
182
+
183
+ expect(result.get('A')).toBe(200);
184
+ });
185
+
186
+ it('should count values when specified', () => {
187
+ const data: DataPoint[] = [
188
+ { x: 'A', y: 100 },
189
+ { x: 'A', y: 200 },
190
+ { x: 'B', y: 300 },
191
+ ];
192
+
193
+ const result = aggregateByCategory(data, 'count');
194
+
195
+ expect(result.get('A')).toBe(2);
196
+ expect(result.get('B')).toBe(1);
197
+ });
198
+
199
+ it('should handle name property for categories', () => {
200
+ const data: DataPoint[] = [
201
+ { name: 'Category A', value: 100 },
202
+ { name: 'Category A', value: 200 },
203
+ ];
204
+
205
+ const result = aggregateByCategory(data);
206
+
207
+ expect(result.get('Category A')).toBe(300);
208
+ });
209
+ });
210
+
211
+ describe('sortDataPoints', () => {
212
+ it('should sort by value descending by default', () => {
213
+ const data: DataPoint[] = [
214
+ { x: 'A', y: 100 },
215
+ { x: 'B', y: 300 },
216
+ { x: 'C', y: 200 },
217
+ ];
218
+
219
+ const result = sortDataPoints(data);
220
+
221
+ expect(result[0].x).toBe('B');
222
+ expect(result[1].x).toBe('C');
223
+ expect(result[2].x).toBe('A');
224
+ });
225
+
226
+ it('should sort by value ascending when specified', () => {
227
+ const data: DataPoint[] = [
228
+ { x: 'A', y: 300 },
229
+ { x: 'B', y: 100 },
230
+ { x: 'C', y: 200 },
231
+ ];
232
+
233
+ const result = sortDataPoints(data, 'value', 'asc');
234
+
235
+ expect(result[0].x).toBe('B');
236
+ expect(result[1].x).toBe('C');
237
+ expect(result[2].x).toBe('A');
238
+ });
239
+
240
+ it('should sort by category alphabetically', () => {
241
+ const data: DataPoint[] = [
242
+ { x: 'Z', y: 100 },
243
+ { x: 'A', y: 200 },
244
+ { x: 'M', y: 300 },
245
+ ];
246
+
247
+ const result = sortDataPoints(data, 'category', 'asc');
248
+
249
+ expect(result[0].x).toBe('A');
250
+ expect(result[1].x).toBe('M');
251
+ expect(result[2].x).toBe('Z');
252
+ });
253
+
254
+ it('should sort by category in reverse', () => {
255
+ const data: DataPoint[] = [
256
+ { x: 'A', y: 100 },
257
+ { x: 'Z', y: 200 },
258
+ ];
259
+
260
+ const result = sortDataPoints(data, 'category', 'desc');
261
+
262
+ expect(result[0].x).toBe('Z');
263
+ expect(result[1].x).toBe('A');
264
+ });
265
+
266
+ it('should not mutate original array', () => {
267
+ const data: DataPoint[] = [
268
+ { x: 'B', y: 200 },
269
+ { x: 'A', y: 100 },
270
+ ];
271
+
272
+ sortDataPoints(data);
273
+
274
+ expect(data[0].x).toBe('B');
275
+ expect(data[1].x).toBe('A');
276
+ });
277
+ });
278
+
279
+ describe('filterByRange', () => {
280
+ it('should filter by minimum value', () => {
281
+ const data: DataPoint[] = [
282
+ { x: 'A', y: 50 },
283
+ { x: 'B', y: 100 },
284
+ { x: 'C', y: 150 },
285
+ ];
286
+
287
+ const result = filterByRange(data, 100);
288
+
289
+ expect(result).toHaveLength(2);
290
+ expect(result[0].x).toBe('B');
291
+ expect(result[1].x).toBe('C');
292
+ });
293
+
294
+ it('should filter by maximum value', () => {
295
+ const data: DataPoint[] = [
296
+ { x: 'A', y: 50 },
297
+ { x: 'B', y: 100 },
298
+ { x: 'C', y: 150 },
299
+ ];
300
+
301
+ const result = filterByRange(data, undefined, 100);
302
+
303
+ expect(result).toHaveLength(2);
304
+ expect(result[0].x).toBe('A');
305
+ expect(result[1].x).toBe('B');
306
+ });
307
+
308
+ it('should filter by both min and max', () => {
309
+ const data: DataPoint[] = [
310
+ { x: 'A', y: 50 },
311
+ { x: 'B', y: 100 },
312
+ { x: 'C', y: 150 },
313
+ { x: 'D', y: 200 },
314
+ ];
315
+
316
+ const result = filterByRange(data, 75, 175);
317
+
318
+ expect(result).toHaveLength(2);
319
+ expect(result[0].x).toBe('B');
320
+ expect(result[1].x).toBe('C');
321
+ });
322
+
323
+ it('should return all data when no range specified', () => {
324
+ const data: DataPoint[] = [
325
+ { x: 'A', y: 50 },
326
+ { x: 'B', y: 100 },
327
+ ];
328
+
329
+ const result = filterByRange(data);
330
+
331
+ expect(result).toHaveLength(2);
332
+ });
333
+ });
334
+
335
+ describe('calculateStats', () => {
336
+ it('should calculate basic statistics', () => {
337
+ const data: DataPoint[] = [
338
+ { x: 'A', y: 10 },
339
+ { x: 'B', y: 20 },
340
+ { x: 'C', y: 30 },
341
+ ];
342
+
343
+ const result = calculateStats(data);
344
+
345
+ expect(result.min).toBe(10);
346
+ expect(result.max).toBe(30);
347
+ expect(result.sum).toBe(60);
348
+ expect(result.avg).toBe(20);
349
+ expect(result.count).toBe(3);
350
+ });
351
+
352
+ it('should handle single data point', () => {
353
+ const data: DataPoint[] = [{ x: 'A', y: 100 }];
354
+
355
+ const result = calculateStats(data);
356
+
357
+ expect(result.min).toBe(100);
358
+ expect(result.max).toBe(100);
359
+ expect(result.sum).toBe(100);
360
+ expect(result.avg).toBe(100);
361
+ expect(result.count).toBe(1);
362
+ });
363
+
364
+ it('should return zeros for empty array', () => {
365
+ const result = calculateStats([]);
366
+
367
+ expect(result.min).toBe(0);
368
+ expect(result.max).toBe(0);
369
+ expect(result.sum).toBe(0);
370
+ expect(result.avg).toBe(0);
371
+ expect(result.count).toBe(0);
372
+ });
373
+
374
+ it('should handle negative values', () => {
375
+ const data: DataPoint[] = [
376
+ { x: 'A', y: -10 },
377
+ { x: 'B', y: 0 },
378
+ { x: 'C', y: 10 },
379
+ ];
380
+
381
+ const result = calculateStats(data);
382
+
383
+ expect(result.min).toBe(-10);
384
+ expect(result.max).toBe(10);
385
+ expect(result.sum).toBe(0);
386
+ expect(result.avg).toBe(0);
387
+ });
388
+ });
389
+
390
+ describe('processTimeSeries', () => {
391
+ it('should sort time series data by date', () => {
392
+ const data: DataPoint[] = [
393
+ { x: '2024-03-01', y: 100 },
394
+ { x: '2024-01-01', y: 50 },
395
+ { x: '2024-02-01', y: 75 },
396
+ ];
397
+
398
+ const result = processTimeSeries(data);
399
+
400
+ expect(result[0].x).toBe('2024-01-01');
401
+ expect(result[1].x).toBe('2024-02-01');
402
+ expect(result[2].x).toBe('2024-03-01');
403
+ });
404
+
405
+ it('should return original data when fillGaps is false', () => {
406
+ const data: DataPoint[] = [
407
+ { x: '2024-01-01', y: 100 },
408
+ { x: '2024-01-03', y: 200 },
409
+ ];
410
+
411
+ const result = processTimeSeries(data, { fillGaps: false });
412
+
413
+ expect(result).toHaveLength(2);
414
+ });
415
+
416
+ it('should fill gaps when fillGaps is true', () => {
417
+ const data: DataPoint[] = [
418
+ { x: '2024-01-01', y: 100 },
419
+ { x: '2024-01-03', y: 200 },
420
+ ];
421
+
422
+ const result = processTimeSeries(data, { fillGaps: true, interval: 'day' });
423
+
424
+ expect(result.length).toBeGreaterThan(2);
425
+ expect(result[0].x).toBe('2024-01-01');
426
+ expect(result[result.length - 1].x).toBe('2024-01-03');
427
+ });
428
+
429
+ it('should aggregate by day', () => {
430
+ const data: DataPoint[] = [
431
+ { x: '2024-01-01T10:00:00', y: 100 },
432
+ { x: '2024-01-01T14:00:00', y: 200 },
433
+ { x: '2024-01-02T10:00:00', y: 150 },
434
+ ];
435
+
436
+ const result = processTimeSeries(data, {
437
+ interval: 'day',
438
+ fillGaps: true,
439
+ aggregation: 'sum',
440
+ });
441
+
442
+ const day1 = result.find((p) => p.x === '2024-01-01');
443
+ expect(day1?.y).toBe(300);
444
+ });
445
+
446
+ it('should aggregate by month', () => {
447
+ const data: DataPoint[] = [
448
+ { x: '2024-01-15', y: 100 },
449
+ { x: '2024-01-20', y: 200 },
450
+ { x: '2024-02-10', y: 150 },
451
+ ];
452
+
453
+ const result = processTimeSeries(data, {
454
+ interval: 'month',
455
+ fillGaps: true,
456
+ aggregation: 'sum',
457
+ });
458
+
459
+ const jan = result.find((p) => p.x === '2024-01');
460
+ const feb = result.find((p) => p.x === '2024-02');
461
+ expect(jan?.y).toBe(300);
462
+ expect(feb?.y).toBe(150);
463
+ });
464
+
465
+ it('should use last value aggregation', () => {
466
+ const data: DataPoint[] = [
467
+ { x: '2024-01-01T10:00:00', y: 100 },
468
+ { x: '2024-01-01T14:00:00', y: 200 },
469
+ ];
470
+
471
+ const result = processTimeSeries(data, {
472
+ interval: 'day',
473
+ fillGaps: true,
474
+ aggregation: 'last',
475
+ });
476
+
477
+ expect(result[0].y).toBe(200);
478
+ });
479
+
480
+ it('should handle empty data', () => {
481
+ const result = processTimeSeries([]);
482
+
483
+ expect(result).toEqual([]);
484
+ });
485
+ });
486
+
487
+ describe('movingAverage', () => {
488
+ it('should calculate moving average', () => {
489
+ const data: DataPoint[] = [
490
+ { x: '1', y: 10 },
491
+ { x: '2', y: 20 },
492
+ { x: '3', y: 30 },
493
+ { x: '4', y: 40 },
494
+ { x: '5', y: 50 },
495
+ ];
496
+
497
+ const result = movingAverage(data, 3);
498
+
499
+ expect(result[0].y).toBe(10); // Not enough data
500
+ expect(result[1].y).toBe(20); // Not enough data
501
+ expect(result[2].y).toBe(20); // (10+20+30)/3
502
+ expect(result[3].y).toBe(30); // (20+30+40)/3
503
+ expect(result[4].y).toBe(40); // (30+40+50)/3
504
+ });
505
+
506
+ it('should return original data when window size is 1', () => {
507
+ const data: DataPoint[] = [
508
+ { x: '1', y: 10 },
509
+ { x: '2', y: 20 },
510
+ ];
511
+
512
+ const result = movingAverage(data, 1);
513
+
514
+ expect(result[0].y).toBe(10);
515
+ expect(result[1].y).toBe(20);
516
+ });
517
+
518
+ it('should return original data when window larger than data', () => {
519
+ const data: DataPoint[] = [
520
+ { x: '1', y: 10 },
521
+ { x: '2', y: 20 },
522
+ ];
523
+
524
+ const result = movingAverage(data, 5);
525
+
526
+ expect(result[0].y).toBe(10);
527
+ expect(result[1].y).toBe(20);
528
+ });
529
+
530
+ it('should preserve x values', () => {
531
+ const data: DataPoint[] = [
532
+ { x: 'Jan', y: 10 },
533
+ { x: 'Feb', y: 20 },
534
+ { x: 'Mar', y: 30 },
535
+ ];
536
+
537
+ const result = movingAverage(data, 2);
538
+
539
+ expect(result[2].x).toBe('Mar');
540
+ });
541
+ });
542
+
543
+ describe('detectOutliers', () => {
544
+ it('should detect outliers using IQR method', () => {
545
+ const data: DataPoint[] = [
546
+ { x: '1', y: 10 },
547
+ { x: '2', y: 12 },
548
+ { x: '3', y: 11 },
549
+ { x: '4', y: 13 },
550
+ { x: '5', y: 100 }, // Outlier
551
+ ];
552
+
553
+ const { inliers, outliers } = detectOutliers(data);
554
+
555
+ expect(inliers).toHaveLength(4);
556
+ expect(outliers).toHaveLength(1);
557
+ expect(outliers[0].y).toBe(100);
558
+ });
559
+
560
+ it('should handle data without outliers', () => {
561
+ const data: DataPoint[] = [
562
+ { x: '1', y: 10 },
563
+ { x: '2', y: 12 },
564
+ { x: '3', y: 11 },
565
+ ];
566
+
567
+ const { inliers, outliers } = detectOutliers(data);
568
+
569
+ expect(inliers).toHaveLength(3);
570
+ expect(outliers).toHaveLength(0);
571
+ });
572
+
573
+ it('should use custom threshold', () => {
574
+ const data: DataPoint[] = [
575
+ { x: '1', y: 10 },
576
+ { x: '2', y: 20 },
577
+ { x: '3', y: 30 },
578
+ { x: '4', y: 40 },
579
+ { x: '5', y: 100 },
580
+ ];
581
+
582
+ // More strict threshold
583
+ const { outliers: strictOutliers } = detectOutliers(data, 1.0);
584
+ // Less strict threshold
585
+ const { outliers: looseOutliers } = detectOutliers(data, 3.0);
586
+
587
+ expect(strictOutliers.length).toBeGreaterThanOrEqual(looseOutliers.length);
588
+ });
589
+
590
+ it('should handle low outliers', () => {
591
+ const data: DataPoint[] = [
592
+ { x: '1', y: 1 }, // Outlier
593
+ { x: '2', y: 50 },
594
+ { x: '3', y: 52 },
595
+ { x: '4', y: 51 },
596
+ { x: '5', y: 53 },
597
+ ];
598
+
599
+ const { outliers } = detectOutliers(data);
600
+
601
+ expect(outliers.length).toBeGreaterThanOrEqual(1);
602
+ expect(outliers[0].y).toBe(1);
603
+ });
604
+ });
605
+
606
+ describe('normalizeValues', () => {
607
+ it('should normalize values to 0-1 range', () => {
608
+ const data: DataPoint[] = [
609
+ { x: '1', y: 0 },
610
+ { x: '2', y: 50 },
611
+ { x: '3', y: 100 },
612
+ ];
613
+
614
+ const result = normalizeValues(data);
615
+
616
+ expect(result[0].y).toBe(0);
617
+ expect(result[1].y).toBe(0.5);
618
+ expect(result[2].y).toBe(1);
619
+ });
620
+
621
+ it('should use custom min and max', () => {
622
+ const data: DataPoint[] = [
623
+ { x: '1', y: 25 },
624
+ { x: '2', y: 50 },
625
+ { x: '3', y: 75 },
626
+ ];
627
+
628
+ const result = normalizeValues(data, 0, 100);
629
+
630
+ expect(result[0].y).toBe(0.25);
631
+ expect(result[1].y).toBe(0.5);
632
+ expect(result[2].y).toBe(0.75);
633
+ });
634
+
635
+ it('should handle all same values', () => {
636
+ const data: DataPoint[] = [
637
+ { x: '1', y: 50 },
638
+ { x: '2', y: 50 },
639
+ ];
640
+
641
+ const result = normalizeValues(data);
642
+
643
+ expect(result[0].y).toBe(0);
644
+ expect(result[1].y).toBe(0);
645
+ });
646
+
647
+ it('should handle negative values', () => {
648
+ const data: DataPoint[] = [
649
+ { x: '1', y: -50 },
650
+ { x: '2', y: 0 },
651
+ { x: '3', y: 50 },
652
+ ];
653
+
654
+ const result = normalizeValues(data);
655
+
656
+ expect(result[0].y).toBe(0);
657
+ expect(result[1].y).toBe(0.5);
658
+ expect(result[2].y).toBe(1);
659
+ });
660
+ });
661
+
662
+ describe('calculatePercentChange', () => {
663
+ it('should calculate percent change', () => {
664
+ const data: DataPoint[] = [
665
+ { x: '1', y: 100 },
666
+ { x: '2', y: 150 },
667
+ { x: '3', y: 200 },
668
+ ];
669
+
670
+ const result = calculatePercentChange(data);
671
+
672
+ expect(result[0].y).toBe(0); // First point has no change
673
+ expect(result[1].y).toBe(50); // (150-100)/100 * 100
674
+ expect(result[2].y).toBe(33.33333333333333); // (200-150)/150 * 100
675
+ });
676
+
677
+ it('should handle decrease', () => {
678
+ const data: DataPoint[] = [
679
+ { x: '1', y: 100 },
680
+ { x: '2', y: 50 },
681
+ ];
682
+
683
+ const result = calculatePercentChange(data);
684
+
685
+ expect(result[1].y).toBe(-50); // (50-100)/100 * 100
686
+ });
687
+
688
+ it('should handle single data point', () => {
689
+ const data: DataPoint[] = [{ x: '1', y: 100 }];
690
+
691
+ const result = calculatePercentChange(data);
692
+
693
+ expect(result).toHaveLength(1);
694
+ expect(result[0].y).toBe(0);
695
+ });
696
+
697
+ it('should handle zero previous value', () => {
698
+ const data: DataPoint[] = [
699
+ { x: '1', y: 0 },
700
+ { x: '2', y: 100 },
701
+ ];
702
+
703
+ const result = calculatePercentChange(data);
704
+
705
+ expect(result[1].y).toBe(0); // Division by zero returns 0
706
+ });
707
+
708
+ it('should preserve original x values', () => {
709
+ const data: DataPoint[] = [
710
+ { x: 'Jan', y: 100 },
711
+ { x: 'Feb', y: 150 },
712
+ ];
713
+
714
+ const result = calculatePercentChange(data);
715
+
716
+ expect(result[0].x).toBe('Jan');
717
+ expect(result[1].x).toBe('Feb');
718
+ });
719
+ });
720
+ });