abstract-chart 3.1.1 → 3.1.5

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/src/chart.ts CHANGED
@@ -1,1020 +1,758 @@
1
- import * as AbstractImage from "abstract-image";
2
- import * as Axis from "./axis";
3
- import { exhaustiveCheck } from "ts-exhaustive-check";
4
-
5
- // tslint:disable:max-file-line-count
6
-
7
- export type Partial<T> = { [P in keyof T]?: T[P] };
8
-
9
- export type LabelLayout = "original" | "end" | "center";
10
-
11
- export interface Chart {
12
- readonly width: number;
13
- readonly height: number;
14
- readonly chartPoints: Array<ChartPoint>;
15
- readonly chartLines: Array<ChartLine>;
16
- readonly chartStack: ChartStack;
17
- readonly xAxisBottom: Axis.Axis | undefined;
18
- readonly xAxisTop: Axis.Axis | undefined;
19
- readonly yAxisLeft: Axis.Axis | undefined;
20
- readonly yAxisRight: Axis.Axis | undefined;
21
- readonly backgroundColor: AbstractImage.Color;
22
- readonly gridColor: AbstractImage.Color;
23
- readonly gridThickness: number;
24
- readonly fontSize: number;
25
- readonly labelLayout: LabelLayout;
26
- }
27
-
28
- export type ChartProps = Partial<Chart>;
29
-
30
- export function createChart(props: ChartProps): Chart {
31
- const {
32
- width = 600,
33
- height = 400,
34
- chartPoints = [],
35
- chartLines = [],
36
- chartStack = createChartStack({}),
37
- xAxisBottom = Axis.createLinearAxis(0, 100, ""),
38
- xAxisTop = undefined,
39
- yAxisLeft = Axis.createLinearAxis(0, 100, ""),
40
- yAxisRight = undefined,
41
- backgroundColor = AbstractImage.white,
42
- gridColor = AbstractImage.gray,
43
- gridThickness = 1,
44
- fontSize = 12,
45
- labelLayout = "original"
46
- } =
47
- props || {};
48
- return {
49
- width,
50
- height,
51
- chartPoints,
52
- chartLines,
53
- chartStack,
54
- xAxisBottom,
55
- xAxisTop,
56
- yAxisLeft,
57
- yAxisRight,
58
- backgroundColor,
59
- gridColor,
60
- gridThickness,
61
- fontSize,
62
- labelLayout
63
- };
64
- }
65
-
66
- export type XAxis = "bottom" | "top";
67
- export type YAxis = "left" | "right";
68
-
69
- export type ChartPointShape = "circle" | "triangle" | "square";
70
-
71
- export interface ChartPoint {
72
- readonly shape: ChartPointShape;
73
- readonly position: AbstractImage.Point;
74
- readonly color: AbstractImage.Color;
75
- readonly size: AbstractImage.Size;
76
- readonly label: string;
77
- readonly xAxis: XAxis;
78
- readonly yAxis: YAxis;
79
- }
80
-
81
- export type ChartPointProps = Partial<ChartPoint>;
82
-
83
- export function createChartPoint(props?: ChartPointProps): ChartPoint {
84
- const {
85
- shape = "circle",
86
- position = AbstractImage.createPoint(0, 0),
87
- color = AbstractImage.black,
88
- size = AbstractImage.createSize(6, 6),
89
- label = "",
90
- xAxis = "bottom",
91
- yAxis = "left"
92
- } =
93
- props || {};
94
- return {
95
- shape,
96
- position,
97
- color,
98
- size,
99
- label,
100
- xAxis,
101
- yAxis
102
- };
103
- }
104
-
105
- export interface ChartLine {
106
- readonly points: Array<AbstractImage.Point>;
107
- readonly color: AbstractImage.Color;
108
- readonly thickness: number;
109
- readonly label: string;
110
- readonly xAxis: XAxis;
111
- readonly yAxis: YAxis;
112
- }
113
-
114
- export type ChartLineProps = Partial<ChartLine>;
115
-
116
- export function createChartLine(props: ChartLineProps): ChartLine {
117
- const {
118
- points = [],
119
- color = AbstractImage.black,
120
- thickness = 1,
121
- label = "",
122
- xAxis = "bottom",
123
- yAxis = "left"
124
- } =
125
- props || {};
126
- return {
127
- points,
128
- color,
129
- thickness,
130
- label,
131
- xAxis,
132
- yAxis
133
- };
134
- }
135
-
136
- export interface ChartStackConfig {
137
- readonly color: AbstractImage.Color;
138
- readonly label: string;
139
- }
140
-
141
- export type ChartStackConfigProps = Partial<ChartStackConfig>;
142
-
143
- export function createChartStackConfig(
144
- props: ChartStackConfigProps
145
- ): ChartStackConfig {
146
- const { color = AbstractImage.black, label = "" } = props || {};
147
- return {
148
- color,
149
- label
150
- };
151
- }
152
-
153
- export interface StackPoints {
154
- readonly x: number;
155
- readonly ys: ReadonlyArray<number>;
156
- }
157
-
158
- export interface ChartStack {
159
- readonly points: Array<StackPoints>;
160
- readonly xAxis: XAxis;
161
- readonly yAxis: YAxis;
162
- readonly config: ReadonlyArray<ChartStackConfig>;
163
- }
164
-
165
- export type ChartStackProps = Partial<ChartStack>;
166
-
167
- export function createChartStack(props: ChartStackProps): ChartStack {
168
- const {
169
- points = [],
170
- xAxis = "bottom",
171
- yAxis = "left",
172
- config = [createChartStackConfig({})]
173
- } =
174
- props || {};
175
- return {
176
- points,
177
- xAxis,
178
- yAxis,
179
- config
180
- };
181
- }
182
-
183
- const padding = 80;
184
-
185
- export function inverseTransformPoint(
186
- point: AbstractImage.Point,
187
- chart: Chart,
188
- xAxis: XAxis,
189
- yAxis: YAxis
190
- ): AbstractImage.Point | undefined {
191
- const xMin = padding;
192
- const xMax = chart.width - padding;
193
- const yMin = chart.height - 0.5 * padding;
194
- const yMax = 0.5 * padding;
195
- const x = Axis.inverseTransformValue(
196
- point.x,
197
- xMin,
198
- xMax,
199
- xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom
200
- );
201
- const y = Axis.inverseTransformValue(
202
- point.y,
203
- yMin,
204
- yMax,
205
- yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft
206
- );
207
- if (x === undefined || y === undefined) {
208
- return undefined;
209
- }
210
- return AbstractImage.createPoint(x, y);
211
- }
212
-
213
- export function renderChart(chart: Chart): AbstractImage.AbstractImage {
214
- const { width, height, xAxisBottom, xAxisTop, yAxisLeft, yAxisRight } = chart;
215
-
216
- const gridWidth = width - 2 * padding;
217
- const gridHeight = height - padding;
218
-
219
- const xMin = padding;
220
- const xMax = width - padding;
221
- const yMin = height - 0.5 * padding;
222
- const yMax = 0.5 * padding;
223
-
224
- const renderedBackground = generateBackground(xMin, xMax, yMin, yMax, chart);
225
-
226
- const xNumTicks = gridWidth / 40;
227
- const renderedXAxisBottom = generateXAxisBottom(
228
- xNumTicks,
229
- xAxisBottom,
230
- xMin,
231
- xMax,
232
- yMin,
233
- yMax,
234
- chart
235
- );
236
- const renderedXAxisTop = generateXAxisTop(
237
- xNumTicks,
238
- xAxisTop,
239
- xMin,
240
- xMax,
241
- yMax,
242
- chart
243
- );
244
-
245
- const yNumTicks = gridHeight / 40;
246
- const renderedYAxisLeft = generateYAxisLeft(
247
- yNumTicks,
248
- yAxisLeft,
249
- xMin,
250
- xMax,
251
- yMin,
252
- yMax,
253
- chart
254
- );
255
- const renderedYAxisRight = generateYAxisRight(
256
- yNumTicks,
257
- yAxisRight,
258
- xMax,
259
- yMin,
260
- yMax,
261
- chart
262
- );
263
-
264
- const renderedPoints = generatePoints(xMin, xMax, yMin, yMax, chart);
265
- const renderedLines = generateLines(xMin, xMax, yMin, yMax, chart);
266
- const renderedStack = generateStack(xMin, xMax, yMin, yMax, chart);
267
-
268
- const components = [
269
- renderedBackground,
270
- renderedXAxisBottom,
271
- renderedXAxisTop,
272
- renderedYAxisLeft,
273
- renderedYAxisRight,
274
- renderedStack,
275
- renderedLines,
276
- renderedPoints
277
- ];
278
- const topLeft = AbstractImage.createPoint(0, 0);
279
- const size = AbstractImage.createSize(width, height);
280
- return AbstractImage.createAbstractImage(
281
- topLeft,
282
- size,
283
- AbstractImage.white,
284
- components
285
- );
286
- }
287
-
288
- export function generateBackground(
289
- xMin: number,
290
- xMax: number,
291
- yMin: number,
292
- yMax: number,
293
- chart: Chart
294
- ): AbstractImage.Component {
295
- const topLeft = AbstractImage.createPoint(xMin, yMax);
296
- const bottomRight = AbstractImage.createPoint(xMax, yMin);
297
- return AbstractImage.createRectangle(
298
- topLeft,
299
- bottomRight,
300
- chart.gridColor,
301
- chart.gridThickness,
302
- chart.backgroundColor
303
- );
304
- }
305
-
306
- export function generateXAxisBottom(
307
- xNumTicks: number,
308
- xAxisBottom: Axis.Axis | undefined,
309
- xMin: number,
310
- xMax: number,
311
- yMin: number,
312
- yMax: number,
313
- chart: Chart
314
- ): AbstractImage.Component {
315
- if (!xAxisBottom) {
316
- return AbstractImage.createGroup("XAxisBottom", []);
317
- }
318
- const xTicks = Axis.getTicks(xNumTicks, xAxisBottom);
319
- const xLines = generateXAxisGridLines(
320
- xMin,
321
- xMax,
322
- yMin + 10,
323
- yMax,
324
- xTicks,
325
- xAxisBottom,
326
- chart
327
- );
328
- const xLabels = generateXAxisLabels(
329
- xMin,
330
- xMax,
331
- yMin + 10,
332
- "down",
333
- xTicks,
334
- xAxisBottom,
335
- chart
336
- );
337
-
338
- let xLabel: AbstractImage.Component;
339
- switch (chart.labelLayout) {
340
- case "original":
341
- xLabel = generateXAxisLabel(
342
- xMax + 0.5 * padding,
343
- yMin + 10,
344
- "uniform",
345
- "down",
346
- xAxisBottom.label,
347
- chart
348
- );
349
- break;
350
-
351
- case "end":
352
- xLabel = generateXAxisLabel(
353
- xMax,
354
- yMin + 25,
355
- "left",
356
- "down",
357
- xAxisBottom.label,
358
- chart
359
- );
360
- break;
361
-
362
- case "center":
363
- xLabel = generateXAxisLabel(
364
- (xMin + xMax) / 2,
365
- yMin + 25,
366
- "uniform",
367
- "down",
368
- xAxisBottom.label,
369
- chart
370
- );
371
- break;
372
-
373
- default:
374
- return exhaustiveCheck(chart.labelLayout);
375
- }
376
-
377
- return AbstractImage.createGroup("XAxisBottom", [xLines, xLabels, xLabel]);
378
- }
379
-
380
- export function generateXAxisTop(
381
- xNumTicks: number,
382
- xAxisTop: Axis.Axis | undefined,
383
- xMin: number,
384
- xMax: number,
385
- yMax: number,
386
- chart: Chart
387
- ): AbstractImage.Component {
388
- if (!xAxisTop) {
389
- return AbstractImage.createGroup("XAxisTop", []);
390
- }
391
- const xTicks2 = Axis.getTicks(xNumTicks, xAxisTop);
392
- const xLines2 = generateXAxisGridLines(
393
- xMin,
394
- xMax,
395
- yMax - 10,
396
- yMax,
397
- xTicks2,
398
- xAxisTop,
399
- chart
400
- );
401
- const xLabels2 = generateXAxisLabels(
402
- xMin,
403
- xMax,
404
- yMax - 13,
405
- "up",
406
- xTicks2,
407
- xAxisTop,
408
- chart
409
- );
410
-
411
- let xLabel2: AbstractImage.Component;
412
- switch (chart.labelLayout) {
413
- case "original":
414
- xLabel2 = generateXAxisLabel(
415
- xMax + 0.5 * padding,
416
- yMax - 13,
417
- "uniform",
418
- "up",
419
- xAxisTop.label,
420
- chart
421
- );
422
- break;
423
-
424
- case "end":
425
- xLabel2 = generateXAxisLabel(
426
- xMax,
427
- yMax - 30,
428
- "left",
429
- "up",
430
- xAxisTop.label,
431
- chart
432
- );
433
- break;
434
-
435
- case "center":
436
- xLabel2 = generateXAxisLabel(
437
- (xMin + xMax) / 2,
438
- yMax - 30,
439
- "uniform",
440
- "up",
441
- xAxisTop.label,
442
- chart
443
- );
444
- break;
445
-
446
- default:
447
- return exhaustiveCheck(chart.labelLayout);
448
- }
449
-
450
- return AbstractImage.createGroup("XAxisTop", [xLines2, xLabels2, xLabel2]);
451
- }
452
-
453
- export function generateYAxisLeft(
454
- yNumTicks: number,
455
- yAxisLeft: Axis.Axis | undefined,
456
- xMin: number,
457
- xMax: number,
458
- yMin: number,
459
- yMax: number,
460
- chart: Chart
461
- ): AbstractImage.Component {
462
- if (!yAxisLeft) {
463
- return AbstractImage.createGroup("YAxisLeft", []);
464
- }
465
- const yTicks = Axis.getTicks(yNumTicks, yAxisLeft);
466
- const yLines = generateYAxisLines(
467
- xMin - 5,
468
- xMax,
469
- yMin,
470
- yMax,
471
- yTicks,
472
- yAxisLeft,
473
- chart
474
- );
475
- const yLabels = generateYAxisLabels(
476
- xMin - 7,
477
- yMin,
478
- yMax,
479
- "left",
480
- yTicks,
481
- yAxisLeft,
482
- chart
483
- );
484
-
485
- const labelPaddingLeft =
486
- 5 + labelPadding(formatNumber(yAxisLeft.max).length, chart.fontSize, 0.5);
487
-
488
- let yLabel: AbstractImage.Component;
489
- switch (chart.labelLayout) {
490
- case "original":
491
- yLabel = generateYAxisLabel(
492
- xMin - labelPaddingLeft,
493
- yMax + 0.5 * padding,
494
- "uniform",
495
- "up",
496
- yAxisLeft.label,
497
- chart
498
- );
499
- break;
500
-
501
- case "end":
502
- yLabel = generateYAxisLabel(
503
- xMin - labelPaddingLeft,
504
- yMax,
505
- "left",
506
- "up",
507
- yAxisLeft.label,
508
- chart
509
- );
510
- break;
511
-
512
- case "center":
513
- yLabel = generateYAxisLabel(
514
- xMin - labelPaddingLeft,
515
- (yMin + yMax) / 2,
516
- "uniform",
517
- "up",
518
- yAxisLeft.label,
519
- chart
520
- );
521
- break;
522
-
523
- default:
524
- return exhaustiveCheck(chart.labelLayout);
525
- }
526
-
527
- return AbstractImage.createGroup("YAxisLeft", [yLines, yLabels, yLabel]);
528
- }
529
-
530
- export function generateYAxisRight(
531
- yNumTicks: number,
532
- yAxisRight: Axis.Axis | undefined,
533
- xMax: number,
534
- yMin: number,
535
- yMax: number,
536
- chart: Chart
537
- ): AbstractImage.Component {
538
- if (!yAxisRight) {
539
- return AbstractImage.createGroup("YAxisRight", []);
540
- }
541
- const yTicks2 = Axis.getTicks(yNumTicks, yAxisRight);
542
- const yLines2 = generateYAxisLines(
543
- xMax - 5,
544
- xMax + 5,
545
- yMin,
546
- yMax,
547
- yTicks2,
548
- yAxisRight,
549
- chart
550
- );
551
- const yLabels2 = generateYAxisLabels(
552
- xMax + 7,
553
- yMin,
554
- yMax,
555
- "right",
556
- yTicks2,
557
- yAxisRight,
558
- chart
559
- );
560
-
561
- const labelPaddingRight =
562
- 7 + labelPadding(formatNumber(yAxisRight.max).length, chart.fontSize, 1.5);
563
-
564
- let yLabel2: AbstractImage.Component;
565
- switch (chart.labelLayout) {
566
- case "original":
567
- yLabel2 = generateYAxisLabel(
568
- xMax + labelPaddingRight,
569
- yMax + 0.5 * padding,
570
- "uniform",
571
- "up",
572
- yAxisRight.label,
573
- chart
574
- );
575
- break;
576
-
577
- case "end":
578
- yLabel2 = generateYAxisLabel(
579
- xMax + labelPaddingRight,
580
- yMax,
581
- "left",
582
- "up",
583
- yAxisRight.label,
584
- chart
585
- );
586
- break;
587
-
588
- case "center":
589
- yLabel2 = generateYAxisLabel(
590
- xMax + labelPaddingRight,
591
- (yMin + yMax) / 2,
592
- "uniform",
593
- "up",
594
- yAxisRight.label,
595
- chart
596
- );
597
- break;
598
-
599
- default:
600
- return exhaustiveCheck(chart.labelLayout);
601
- }
602
-
603
- return AbstractImage.createGroup("YAxisRight", [yLines2, yLabels2, yLabel2]);
604
- }
605
-
606
- export function generateStack(
607
- xMin: number,
608
- xMax: number,
609
- yMin: number,
610
- yMax: number,
611
- chart: Chart
612
- ): AbstractImage.Component {
613
- const pointsPos = chart.chartStack.points.map(stackPoint => ({
614
- x: stackPoint.x,
615
- ys: [...stackPoint.ys.map(y => Math.min(0, y))]
616
- }));
617
-
618
- const stackPos = generateUnsignedStack(xMin, xMax, yMin, yMax, {
619
- ...chart,
620
- chartStack: { ...chart.chartStack, points: pointsPos }
621
- });
622
-
623
- const pointsNeg = chart.chartStack.points.map(stackPoint => ({
624
- x: stackPoint.x,
625
- ys: [...stackPoint.ys.map(y => Math.max(0, y))]
626
- }));
627
-
628
- const stackNeg = generateUnsignedStack(xMin, xMax, yMin, yMax, {
629
- ...chart,
630
- chartStack: { ...chart.chartStack, points: pointsNeg }
631
- });
632
-
633
- return AbstractImage.createGroup("Stacks", [stackPos, stackNeg]);
634
- }
635
-
636
- function generateUnsignedStack(
637
- xMin: number,
638
- xMax: number,
639
- yMin: number,
640
- yMax: number,
641
- chart: Chart
642
- ): AbstractImage.Component {
643
- if (chart.chartStack.points.length < 2) {
644
- return AbstractImage.createGroup("stack", []);
645
- }
646
-
647
- const xAxis =
648
- chart.chartStack.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
649
- const yAxis =
650
- chart.chartStack.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
651
-
652
- const xPoints = chart.chartStack.points.map(stackPoints => {
653
- let sumY = 0;
654
- const points = stackPoints.ys.map(y => {
655
- sumY += y;
656
- return Axis.transformPoint(
657
- AbstractImage.createPoint(stackPoints.x, sumY),
658
- xMin,
659
- xMax,
660
- yMin,
661
- yMax,
662
- xAxis,
663
- yAxis
664
- );
665
- });
666
- return points;
667
- });
668
-
669
- // Transpose the xPoints data to lines.
670
- const lines: Array<Array<AbstractImage.Point>> = [];
671
- for (let i = 0; i < xPoints[0].length; ++i) {
672
- lines[i] = [];
673
- for (const points of xPoints) {
674
- lines[i].push(points[i]);
675
- }
676
- }
677
-
678
- const polygons: Array<AbstractImage.Polygon> = [];
679
- let lastLine = chart.chartStack.points.map(stackPoint =>
680
- Axis.transformPoint(
681
- AbstractImage.createPoint(stackPoint.x, 0),
682
- xMin,
683
- xMax,
684
- yMin,
685
- yMax,
686
- xAxis,
687
- yAxis
688
- )
689
- );
690
- lines.forEach((line, index) => {
691
- const config = chart.chartStack.config[index];
692
- if (!config) {
693
- throw new Error("Missing config for series " + index);
694
- }
695
- const color = config.color;
696
- const points = [...line, ...lastLine.slice().reverse()];
697
- lastLine = line;
698
- polygons.push(AbstractImage.createPolygon(points, color, 0, color));
699
- });
700
-
701
- return AbstractImage.createGroup("Stack", polygons);
702
- }
703
-
704
- export function generateLines(
705
- xMin: number,
706
- xMax: number,
707
- yMin: number,
708
- yMax: number,
709
- chart: Chart
710
- ): AbstractImage.Component {
711
- const lines = chart.chartLines.map((l: ChartLine) => {
712
- if (l.points.length < 2) {
713
- return AbstractImage.createGroup(l.label, []);
714
- }
715
- const xAxis = l.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
716
- const yAxis = l.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
717
- const points = l.points.map(p =>
718
- Axis.transformPoint(p, xMin, xMax, yMin, yMax, xAxis, yAxis)
719
- );
720
- const last = points[points.length - 1];
721
- return AbstractImage.createGroup(l.label, [
722
- AbstractImage.createPolyLine(points, l.color, l.thickness),
723
- AbstractImage.createText(
724
- last,
725
- l.label,
726
- "Arial",
727
- chart.fontSize,
728
- AbstractImage.black,
729
- "normal",
730
- 0,
731
- "center",
732
- "right",
733
- "down",
734
- 0,
735
- AbstractImage.black
736
- )
737
- ]);
738
- });
739
- return AbstractImage.createGroup("Lines", lines);
740
- }
741
-
742
- export function generatePoints(
743
- xMin: number,
744
- xMax: number,
745
- yMin: number,
746
- yMax: number,
747
- chart: Chart
748
- ): AbstractImage.Component {
749
- const points = chart.chartPoints.map(p => {
750
- const xAxis = p.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
751
- const yAxis = p.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
752
- const position = Axis.transformPoint(
753
- p.position,
754
- xMin,
755
- xMax,
756
- yMin,
757
- yMax,
758
- xAxis,
759
- yAxis
760
- );
761
- const shape = generatePointShape(p, position);
762
- return AbstractImage.createGroup(p.label, [
763
- shape,
764
- AbstractImage.createText(
765
- position,
766
- p.label,
767
- "Arial",
768
- chart.fontSize,
769
- AbstractImage.black,
770
- "normal",
771
- 0,
772
- "center",
773
- "right",
774
- "down",
775
- 0,
776
- AbstractImage.black
777
- )
778
- ]);
779
- });
780
- return AbstractImage.createGroup("Points", points);
781
- }
782
-
783
- function generatePointShape(
784
- p: ChartPoint,
785
- position: AbstractImage.Point
786
- ): AbstractImage.Component {
787
- const halfWidth = p.size.width * 0.5;
788
- const halfHeight = p.size.height * 0.5;
789
- if (p.shape === "triangle") {
790
- const trianglePoints = [
791
- AbstractImage.createPoint(position.x, position.y + halfHeight),
792
- AbstractImage.createPoint(
793
- position.x - halfWidth,
794
- position.y - halfHeight
795
- ),
796
- AbstractImage.createPoint(position.x + halfWidth, position.y - halfHeight)
797
- ];
798
- return AbstractImage.createPolygon(
799
- trianglePoints,
800
- AbstractImage.black,
801
- 1,
802
- p.color
803
- );
804
- } else if (p.shape === "square") {
805
- const topLeft = AbstractImage.createPoint(
806
- position.x - halfWidth,
807
- position.y - halfHeight
808
- );
809
- const bottomRight = AbstractImage.createPoint(
810
- position.x + halfWidth,
811
- position.y + halfHeight
812
- );
813
- return AbstractImage.createRectangle(
814
- topLeft,
815
- bottomRight,
816
- AbstractImage.black,
817
- 1,
818
- p.color
819
- );
820
- } else {
821
- const topLeft = AbstractImage.createPoint(
822
- position.x - halfWidth,
823
- position.y - halfHeight
824
- );
825
- const bottomRight = AbstractImage.createPoint(
826
- position.x + halfWidth,
827
- position.y + halfHeight
828
- );
829
- return AbstractImage.createEllipse(
830
- topLeft,
831
- bottomRight,
832
- AbstractImage.black,
833
- 1,
834
- p.color
835
- );
836
- }
837
- }
838
-
839
- export function generateXAxisGridLines(
840
- xMin: number,
841
- xMax: number,
842
- yMin: number,
843
- yMax: number,
844
- xTicks: Array<number>,
845
- xAxis: Axis.Axis,
846
- chart: Chart
847
- ): AbstractImage.Component {
848
- const xLines = xTicks.map(l => {
849
- const x = Axis.transformValue(l, xMin, xMax, xAxis);
850
- const start = AbstractImage.createPoint(x, yMin);
851
- const end = AbstractImage.createPoint(x, yMax);
852
- return AbstractImage.createLine(
853
- start,
854
- end,
855
- chart.gridColor,
856
- chart.gridThickness
857
- );
858
- });
859
-
860
- return AbstractImage.createGroup("Lines", xLines);
861
- }
862
-
863
- export function generateXAxisLabels(
864
- xMin: number,
865
- xMax: number,
866
- y: number,
867
- growVertical: AbstractImage.GrowthDirection,
868
- xTicks: Array<number>,
869
- xAxis: Axis.Axis,
870
- chart: Chart
871
- ): AbstractImage.Component {
872
- const xLabels = xTicks.map(l => {
873
- const position = AbstractImage.createPoint(
874
- Axis.transformValue(l, xMin, xMax, xAxis),
875
- y
876
- );
877
- return AbstractImage.createText(
878
- position,
879
- formatNumber(l),
880
- "Arial",
881
- chart.fontSize,
882
- AbstractImage.black,
883
- "normal",
884
- 0,
885
- "center",
886
- "uniform",
887
- growVertical,
888
- 0,
889
- AbstractImage.black
890
- );
891
- });
892
- return AbstractImage.createGroup("Labels", xLabels);
893
- }
894
-
895
- export function generateXAxisLabel(
896
- x: number,
897
- y: number,
898
- horizontalGrowthDirection: AbstractImage.GrowthDirection,
899
- verticalGrowthDirection: AbstractImage.GrowthDirection,
900
- label: string,
901
- chart: Chart
902
- ): AbstractImage.Component {
903
- const position = AbstractImage.createPoint(x, y);
904
- return AbstractImage.createText(
905
- position,
906
- label,
907
- "Arial",
908
- chart.fontSize,
909
- AbstractImage.black,
910
- "normal",
911
- 0,
912
- "center",
913
- horizontalGrowthDirection,
914
- verticalGrowthDirection,
915
- 0,
916
- AbstractImage.black
917
- );
918
- }
919
-
920
- export function generateYAxisLines(
921
- xMin: number,
922
- xMax: number,
923
- yMin: number,
924
- yMax: number,
925
- yTicks: Array<number>,
926
- yAxis: Axis.Axis,
927
- chart: Chart
928
- ): AbstractImage.Component {
929
- const yLines = yTicks.map(l => {
930
- const y = Axis.transformValue(l, yMin, yMax, yAxis);
931
- const start = AbstractImage.createPoint(xMin, y);
932
- const end = AbstractImage.createPoint(xMax, y);
933
- return AbstractImage.createLine(
934
- start,
935
- end,
936
- chart.gridColor,
937
- chart.gridThickness
938
- );
939
- });
940
- return AbstractImage.createGroup("Lines", yLines);
941
- }
942
-
943
- export function generateYAxisLabels(
944
- x: number,
945
- yMin: number,
946
- yMax: number,
947
- growHorizontal: AbstractImage.GrowthDirection,
948
- yTicks: Array<number>,
949
- yAxis: Axis.Axis,
950
- chart: Chart
951
- ): AbstractImage.Component {
952
- const yLabels = yTicks.map(l => {
953
- const position = AbstractImage.createPoint(
954
- x,
955
- Axis.transformValue(l, yMin, yMax, yAxis)
956
- );
957
- return AbstractImage.createText(
958
- position,
959
- formatNumber(l),
960
- "Arial",
961
- chart.fontSize,
962
- AbstractImage.black,
963
- "normal",
964
- 0,
965
- "center",
966
- growHorizontal,
967
- "uniform",
968
- 0,
969
- AbstractImage.black
970
- );
971
- });
972
- return AbstractImage.createGroup("Labels", yLabels);
973
- }
974
-
975
- export function generateYAxisLabel(
976
- x: number,
977
- y: number,
978
- horizontalGrowthDirection: AbstractImage.GrowthDirection,
979
- verticalGrowthDirection: AbstractImage.GrowthDirection,
980
- label: string,
981
- chart: Chart
982
- ): AbstractImage.Component {
983
- const position = AbstractImage.createPoint(x, y);
984
- return AbstractImage.createText(
985
- position,
986
- label,
987
- "Arial",
988
- chart.fontSize,
989
- AbstractImage.black,
990
- "normal",
991
- -90,
992
- "center",
993
- horizontalGrowthDirection,
994
- verticalGrowthDirection,
995
- 0,
996
- AbstractImage.black
997
- );
998
- }
999
-
1000
- function formatNumber(n: number): string {
1001
- if (n >= 10000000) {
1002
- return numberToString(n / 1000000) + "m";
1003
- }
1004
- if (n >= 10000) {
1005
- return numberToString(n / 1000) + "k";
1006
- }
1007
- return numberToString(n);
1008
- }
1009
-
1010
- function numberToString(n: number): string {
1011
- return parseFloat(n.toPrecision(5)).toString();
1012
- }
1013
-
1014
- function labelPadding(
1015
- numberOfCharacters: number,
1016
- fontSize: number,
1017
- characterOffset: number
1018
- ): number {
1019
- return (numberOfCharacters + 1 + characterOffset) * fontSize * 3 / 4;
1020
- }
1
+ import * as AbstractImage from "abstract-image";
2
+ import * as Axis from "./axis";
3
+ import { exhaustiveCheck } from "ts-exhaustive-check";
4
+
5
+ // tslint:disable:max-file-line-count
6
+
7
+ export type Partial<T> = { [P in keyof T]?: T[P] };
8
+
9
+ export type LabelLayout = "original" | "end" | "center";
10
+
11
+ export interface Chart {
12
+ readonly width: number;
13
+ readonly height: number;
14
+ readonly chartPoints: Array<ChartPoint>;
15
+ readonly chartLines: Array<ChartLine>;
16
+ readonly chartStack: ChartStack;
17
+ readonly xAxisBottom: Axis.Axis | undefined;
18
+ readonly xAxisTop: Axis.Axis | undefined;
19
+ readonly yAxisLeft: Axis.Axis | undefined;
20
+ readonly yAxisRight: Axis.Axis | undefined;
21
+ readonly backgroundColor: AbstractImage.Color;
22
+ readonly gridColor: AbstractImage.Color;
23
+ readonly gridThickness: number;
24
+ readonly fontSize: number;
25
+ readonly labelLayout: LabelLayout;
26
+ }
27
+
28
+ export type ChartProps = Partial<Chart>;
29
+
30
+ export function createChart(props: ChartProps): Chart {
31
+ const {
32
+ width = 600,
33
+ height = 400,
34
+ chartPoints = [],
35
+ chartLines = [],
36
+ chartStack = createChartStack({}),
37
+ xAxisBottom = Axis.createLinearAxis(0, 100, ""),
38
+ xAxisTop = undefined,
39
+ yAxisLeft = Axis.createLinearAxis(0, 100, ""),
40
+ yAxisRight = undefined,
41
+ backgroundColor = AbstractImage.white,
42
+ gridColor = AbstractImage.gray,
43
+ gridThickness = 1,
44
+ fontSize = 12,
45
+ labelLayout = "original",
46
+ } = props || {};
47
+ return {
48
+ width,
49
+ height,
50
+ chartPoints,
51
+ chartLines,
52
+ chartStack,
53
+ xAxisBottom,
54
+ xAxisTop,
55
+ yAxisLeft,
56
+ yAxisRight,
57
+ backgroundColor,
58
+ gridColor,
59
+ gridThickness,
60
+ fontSize,
61
+ labelLayout,
62
+ };
63
+ }
64
+
65
+ export type XAxis = "bottom" | "top";
66
+ export type YAxis = "left" | "right";
67
+
68
+ export type ChartPointShape = "circle" | "triangle" | "square";
69
+
70
+ export interface ChartPoint {
71
+ readonly shape: ChartPointShape;
72
+ readonly position: AbstractImage.Point;
73
+ readonly color: AbstractImage.Color;
74
+ readonly size: AbstractImage.Size;
75
+ readonly label: string;
76
+ readonly xAxis: XAxis;
77
+ readonly yAxis: YAxis;
78
+ }
79
+
80
+ export type ChartPointProps = Partial<ChartPoint>;
81
+
82
+ export function createChartPoint(props?: ChartPointProps): ChartPoint {
83
+ const {
84
+ shape = "circle",
85
+ position = AbstractImage.createPoint(0, 0),
86
+ color = AbstractImage.black,
87
+ size = AbstractImage.createSize(6, 6),
88
+ label = "",
89
+ xAxis = "bottom",
90
+ yAxis = "left",
91
+ } = props || {};
92
+ return {
93
+ shape,
94
+ position,
95
+ color,
96
+ size,
97
+ label,
98
+ xAxis,
99
+ yAxis,
100
+ };
101
+ }
102
+
103
+ export interface ChartLine {
104
+ readonly points: Array<AbstractImage.Point>;
105
+ readonly color: AbstractImage.Color;
106
+ readonly thickness: number;
107
+ readonly label: string;
108
+ readonly xAxis: XAxis;
109
+ readonly yAxis: YAxis;
110
+ }
111
+
112
+ export type ChartLineProps = Partial<ChartLine>;
113
+
114
+ export function createChartLine(props: ChartLineProps): ChartLine {
115
+ const {
116
+ points = [],
117
+ color = AbstractImage.black,
118
+ thickness = 1,
119
+ label = "",
120
+ xAxis = "bottom",
121
+ yAxis = "left",
122
+ } = props || {};
123
+ return {
124
+ points,
125
+ color,
126
+ thickness,
127
+ label,
128
+ xAxis,
129
+ yAxis,
130
+ };
131
+ }
132
+
133
+ export interface ChartStackConfig {
134
+ readonly color: AbstractImage.Color;
135
+ readonly label: string;
136
+ }
137
+
138
+ export type ChartStackConfigProps = Partial<ChartStackConfig>;
139
+
140
+ export function createChartStackConfig(props: ChartStackConfigProps): ChartStackConfig {
141
+ const { color = AbstractImage.black, label = "" } = props || {};
142
+ return {
143
+ color,
144
+ label,
145
+ };
146
+ }
147
+
148
+ export interface StackPoints {
149
+ readonly x: number;
150
+ readonly ys: ReadonlyArray<number>;
151
+ }
152
+
153
+ export interface ChartStack {
154
+ readonly points: Array<StackPoints>;
155
+ readonly xAxis: XAxis;
156
+ readonly yAxis: YAxis;
157
+ readonly config: ReadonlyArray<ChartStackConfig>;
158
+ }
159
+
160
+ export type ChartStackProps = Partial<ChartStack>;
161
+
162
+ export function createChartStack(props: ChartStackProps): ChartStack {
163
+ const { points = [], xAxis = "bottom", yAxis = "left", config = [createChartStackConfig({})] } = props || {};
164
+ return {
165
+ points,
166
+ xAxis,
167
+ yAxis,
168
+ config,
169
+ };
170
+ }
171
+
172
+ const padding = 80;
173
+
174
+ export function inverseTransformPoint(
175
+ point: AbstractImage.Point,
176
+ chart: Chart,
177
+ xAxis: XAxis,
178
+ yAxis: YAxis
179
+ ): AbstractImage.Point | undefined {
180
+ const xMin = padding;
181
+ const xMax = chart.width - padding;
182
+ const yMin = chart.height - 0.5 * padding;
183
+ const yMax = 0.5 * padding;
184
+ const x = Axis.inverseTransformValue(point.x, xMin, xMax, xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom);
185
+ const y = Axis.inverseTransformValue(point.y, yMin, yMax, yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft);
186
+ if (x === undefined || y === undefined) {
187
+ return undefined;
188
+ }
189
+ return AbstractImage.createPoint(x, y);
190
+ }
191
+
192
+ export function renderChart(chart: Chart): AbstractImage.AbstractImage {
193
+ const { width, height, xAxisBottom, xAxisTop, yAxisLeft, yAxisRight } = chart;
194
+
195
+ const gridWidth = width - 2 * padding;
196
+ const gridHeight = height - padding;
197
+
198
+ const xMin = padding;
199
+ const xMax = width - padding;
200
+ const yMin = height - 0.5 * padding;
201
+ const yMax = 0.5 * padding;
202
+
203
+ const renderedBackground = generateBackground(xMin, xMax, yMin, yMax, chart);
204
+
205
+ const xNumTicks = gridWidth / 40;
206
+ const renderedXAxisBottom = generateXAxisBottom(xNumTicks, xAxisBottom, xMin, xMax, yMin, yMax, chart);
207
+ const renderedXAxisTop = generateXAxisTop(xNumTicks, xAxisTop, xMin, xMax, yMax, chart);
208
+
209
+ const yNumTicks = gridHeight / 40;
210
+ const renderedYAxisLeft = generateYAxisLeft(yNumTicks, yAxisLeft, xMin, xMax, yMin, yMax, chart);
211
+ const renderedYAxisRight = generateYAxisRight(yNumTicks, yAxisRight, xMax, yMin, yMax, chart);
212
+
213
+ const renderedPoints = generatePoints(xMin, xMax, yMin, yMax, chart);
214
+ const renderedLines = generateLines(xMin, xMax, yMin, yMax, chart);
215
+ const renderedStack = generateStack(xMin, xMax, yMin, yMax, chart);
216
+
217
+ const components = [
218
+ renderedBackground,
219
+ renderedXAxisBottom,
220
+ renderedXAxisTop,
221
+ renderedYAxisLeft,
222
+ renderedYAxisRight,
223
+ renderedStack,
224
+ renderedLines,
225
+ renderedPoints,
226
+ ];
227
+ const topLeft = AbstractImage.createPoint(0, 0);
228
+ const size = AbstractImage.createSize(width, height);
229
+ return AbstractImage.createAbstractImage(topLeft, size, AbstractImage.white, components);
230
+ }
231
+
232
+ export function generateBackground(
233
+ xMin: number,
234
+ xMax: number,
235
+ yMin: number,
236
+ yMax: number,
237
+ chart: Chart
238
+ ): AbstractImage.Component {
239
+ const topLeft = AbstractImage.createPoint(xMin, yMax);
240
+ const bottomRight = AbstractImage.createPoint(xMax, yMin);
241
+ return AbstractImage.createRectangle(
242
+ topLeft,
243
+ bottomRight,
244
+ chart.gridColor,
245
+ chart.gridThickness,
246
+ chart.backgroundColor
247
+ );
248
+ }
249
+
250
+ export function generateXAxisBottom(
251
+ xNumTicks: number,
252
+ xAxisBottom: Axis.Axis | undefined,
253
+ xMin: number,
254
+ xMax: number,
255
+ yMin: number,
256
+ yMax: number,
257
+ chart: Chart
258
+ ): AbstractImage.Component {
259
+ if (!xAxisBottom) {
260
+ return AbstractImage.createGroup("XAxisBottom", []);
261
+ }
262
+ const xTicks = Axis.getTicks(xNumTicks, xAxisBottom);
263
+ const xLines = generateXAxisGridLines(xMin, xMax, yMin + 10, yMax, xTicks, xAxisBottom, chart);
264
+ const xLabels = generateXAxisLabels(xMin, xMax, yMin + 10, "down", xTicks, xAxisBottom, chart);
265
+
266
+ let xLabel: AbstractImage.Component;
267
+ switch (chart.labelLayout) {
268
+ case "original":
269
+ xLabel = generateXAxisLabel(xMax + 0.5 * padding, yMin + 10, "uniform", "down", xAxisBottom.label, chart);
270
+ break;
271
+
272
+ case "end":
273
+ xLabel = generateXAxisLabel(xMax, yMin + 25, "left", "down", xAxisBottom.label, chart);
274
+ break;
275
+
276
+ case "center":
277
+ xLabel = generateXAxisLabel((xMin + xMax) / 2, yMin + 25, "uniform", "down", xAxisBottom.label, chart);
278
+ break;
279
+
280
+ default:
281
+ return exhaustiveCheck(chart.labelLayout);
282
+ }
283
+
284
+ return AbstractImage.createGroup("XAxisBottom", [xLines, xLabels, xLabel]);
285
+ }
286
+
287
+ export function generateXAxisTop(
288
+ xNumTicks: number,
289
+ xAxisTop: Axis.Axis | undefined,
290
+ xMin: number,
291
+ xMax: number,
292
+ yMax: number,
293
+ chart: Chart
294
+ ): AbstractImage.Component {
295
+ if (!xAxisTop) {
296
+ return AbstractImage.createGroup("XAxisTop", []);
297
+ }
298
+ const xTicks2 = Axis.getTicks(xNumTicks, xAxisTop);
299
+ const xLines2 = generateXAxisGridLines(xMin, xMax, yMax - 10, yMax, xTicks2, xAxisTop, chart);
300
+ const xLabels2 = generateXAxisLabels(xMin, xMax, yMax - 13, "up", xTicks2, xAxisTop, chart);
301
+
302
+ let xLabel2: AbstractImage.Component;
303
+ switch (chart.labelLayout) {
304
+ case "original":
305
+ xLabel2 = generateXAxisLabel(xMax + 0.5 * padding, yMax - 13, "uniform", "up", xAxisTop.label, chart);
306
+ break;
307
+
308
+ case "end":
309
+ xLabel2 = generateXAxisLabel(xMax, yMax - 30, "left", "up", xAxisTop.label, chart);
310
+ break;
311
+
312
+ case "center":
313
+ xLabel2 = generateXAxisLabel((xMin + xMax) / 2, yMax - 30, "uniform", "up", xAxisTop.label, chart);
314
+ break;
315
+
316
+ default:
317
+ return exhaustiveCheck(chart.labelLayout);
318
+ }
319
+
320
+ return AbstractImage.createGroup("XAxisTop", [xLines2, xLabels2, xLabel2]);
321
+ }
322
+
323
+ export function generateYAxisLeft(
324
+ yNumTicks: number,
325
+ yAxisLeft: Axis.Axis | undefined,
326
+ xMin: number,
327
+ xMax: number,
328
+ yMin: number,
329
+ yMax: number,
330
+ chart: Chart
331
+ ): AbstractImage.Component {
332
+ if (!yAxisLeft) {
333
+ return AbstractImage.createGroup("YAxisLeft", []);
334
+ }
335
+ const yTicks = Axis.getTicks(yNumTicks, yAxisLeft);
336
+ const yLines = generateYAxisLines(xMin - 5, xMax, yMin, yMax, yTicks, yAxisLeft, chart);
337
+ const yLabels = generateYAxisLabels(xMin - 7, yMin, yMax, "left", yTicks, yAxisLeft, chart);
338
+
339
+ const labelPaddingLeft = 5 + labelPadding(formatNumber(yAxisLeft.max).length, chart.fontSize, 0.5);
340
+
341
+ let yLabel: AbstractImage.Component;
342
+ switch (chart.labelLayout) {
343
+ case "original":
344
+ yLabel = generateYAxisLabel(
345
+ xMin - labelPaddingLeft,
346
+ yMax + 0.5 * padding,
347
+ "uniform",
348
+ "up",
349
+ yAxisLeft.label,
350
+ chart
351
+ );
352
+ break;
353
+
354
+ case "end":
355
+ yLabel = generateYAxisLabel(xMin - labelPaddingLeft, yMax, "left", "up", yAxisLeft.label, chart);
356
+ break;
357
+
358
+ case "center":
359
+ yLabel = generateYAxisLabel(xMin - labelPaddingLeft, (yMin + yMax) / 2, "uniform", "up", yAxisLeft.label, chart);
360
+ break;
361
+
362
+ default:
363
+ return exhaustiveCheck(chart.labelLayout);
364
+ }
365
+
366
+ return AbstractImage.createGroup("YAxisLeft", [yLines, yLabels, yLabel]);
367
+ }
368
+
369
+ export function generateYAxisRight(
370
+ yNumTicks: number,
371
+ yAxisRight: Axis.Axis | undefined,
372
+ xMax: number,
373
+ yMin: number,
374
+ yMax: number,
375
+ chart: Chart
376
+ ): AbstractImage.Component {
377
+ if (!yAxisRight) {
378
+ return AbstractImage.createGroup("YAxisRight", []);
379
+ }
380
+ const yTicks2 = Axis.getTicks(yNumTicks, yAxisRight);
381
+ const yLines2 = generateYAxisLines(xMax - 5, xMax + 5, yMin, yMax, yTicks2, yAxisRight, chart);
382
+ const yLabels2 = generateYAxisLabels(xMax + 7, yMin, yMax, "right", yTicks2, yAxisRight, chart);
383
+
384
+ const labelPaddingRight = 7 + labelPadding(formatNumber(yAxisRight.max).length, chart.fontSize, 1.5);
385
+
386
+ let yLabel2: AbstractImage.Component;
387
+ switch (chart.labelLayout) {
388
+ case "original":
389
+ yLabel2 = generateYAxisLabel(
390
+ xMax + labelPaddingRight,
391
+ yMax + 0.5 * padding,
392
+ "uniform",
393
+ "up",
394
+ yAxisRight.label,
395
+ chart
396
+ );
397
+ break;
398
+
399
+ case "end":
400
+ yLabel2 = generateYAxisLabel(xMax + labelPaddingRight, yMax, "left", "up", yAxisRight.label, chart);
401
+ break;
402
+
403
+ case "center":
404
+ yLabel2 = generateYAxisLabel(
405
+ xMax + labelPaddingRight,
406
+ (yMin + yMax) / 2,
407
+ "uniform",
408
+ "up",
409
+ yAxisRight.label,
410
+ chart
411
+ );
412
+ break;
413
+
414
+ default:
415
+ return exhaustiveCheck(chart.labelLayout);
416
+ }
417
+
418
+ return AbstractImage.createGroup("YAxisRight", [yLines2, yLabels2, yLabel2]);
419
+ }
420
+
421
+ export function generateStack(
422
+ xMin: number,
423
+ xMax: number,
424
+ yMin: number,
425
+ yMax: number,
426
+ chart: Chart
427
+ ): AbstractImage.Component {
428
+ const pointsPos = chart.chartStack.points.map((stackPoint) => ({
429
+ x: stackPoint.x,
430
+ ys: [...stackPoint.ys.map((y) => Math.min(0, y))],
431
+ }));
432
+
433
+ const stackPos = generateUnsignedStack(xMin, xMax, yMin, yMax, {
434
+ ...chart,
435
+ chartStack: { ...chart.chartStack, points: pointsPos },
436
+ });
437
+
438
+ const pointsNeg = chart.chartStack.points.map((stackPoint) => ({
439
+ x: stackPoint.x,
440
+ ys: [...stackPoint.ys.map((y) => Math.max(0, y))],
441
+ }));
442
+
443
+ const stackNeg = generateUnsignedStack(xMin, xMax, yMin, yMax, {
444
+ ...chart,
445
+ chartStack: { ...chart.chartStack, points: pointsNeg },
446
+ });
447
+
448
+ return AbstractImage.createGroup("Stacks", [stackPos, stackNeg]);
449
+ }
450
+
451
+ function generateUnsignedStack(
452
+ xMin: number,
453
+ xMax: number,
454
+ yMin: number,
455
+ yMax: number,
456
+ chart: Chart
457
+ ): AbstractImage.Component {
458
+ if (chart.chartStack.points.length < 2) {
459
+ return AbstractImage.createGroup("stack", []);
460
+ }
461
+
462
+ const xAxis = chart.chartStack.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
463
+ const yAxis = chart.chartStack.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
464
+
465
+ const xPoints = chart.chartStack.points.map((stackPoints) => {
466
+ let sumY = 0;
467
+ const points = stackPoints.ys.map((y) => {
468
+ sumY += y;
469
+ return Axis.transformPoint(AbstractImage.createPoint(stackPoints.x, sumY), xMin, xMax, yMin, yMax, xAxis, yAxis);
470
+ });
471
+ return points;
472
+ });
473
+
474
+ // Transpose the xPoints data to lines.
475
+ const lines: Array<Array<AbstractImage.Point>> = [];
476
+ for (let i = 0; i < xPoints[0].length; ++i) {
477
+ lines[i] = [];
478
+ for (const points of xPoints) {
479
+ lines[i].push(points[i]);
480
+ }
481
+ }
482
+
483
+ const polygons: Array<AbstractImage.Polygon> = [];
484
+ let lastLine = chart.chartStack.points.map((stackPoint) =>
485
+ Axis.transformPoint(AbstractImage.createPoint(stackPoint.x, 0), xMin, xMax, yMin, yMax, xAxis, yAxis)
486
+ );
487
+ lines.forEach((line, index) => {
488
+ const config = chart.chartStack.config[index];
489
+ if (!config) {
490
+ throw new Error("Missing config for series " + index);
491
+ }
492
+ const color = config.color;
493
+ const points = [...line, ...lastLine.slice().reverse()];
494
+ lastLine = line;
495
+ polygons.push(AbstractImage.createPolygon(points, color, 0, color));
496
+ });
497
+
498
+ return AbstractImage.createGroup("Stack", polygons);
499
+ }
500
+
501
+ export function generateLines(
502
+ xMin: number,
503
+ xMax: number,
504
+ yMin: number,
505
+ yMax: number,
506
+ chart: Chart
507
+ ): AbstractImage.Component {
508
+ const lines = chart.chartLines.map((l: ChartLine) => {
509
+ if (l.points.length < 2) {
510
+ return AbstractImage.createGroup(l.label, []);
511
+ }
512
+ const xAxis = l.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
513
+ const yAxis = l.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
514
+ const points = l.points.map((p) => Axis.transformPoint(p, xMin, xMax, yMin, yMax, xAxis, yAxis));
515
+ const last = points[points.length - 1];
516
+ return AbstractImage.createGroup(l.label, [
517
+ AbstractImage.createPolyLine(points, l.color, l.thickness),
518
+ AbstractImage.createText(
519
+ last,
520
+ l.label,
521
+ "Arial",
522
+ chart.fontSize,
523
+ AbstractImage.black,
524
+ "normal",
525
+ 0,
526
+ "center",
527
+ "right",
528
+ "down",
529
+ 0,
530
+ AbstractImage.black,
531
+ false
532
+ ),
533
+ ]);
534
+ });
535
+ return AbstractImage.createGroup("Lines", lines);
536
+ }
537
+
538
+ export function generatePoints(
539
+ xMin: number,
540
+ xMax: number,
541
+ yMin: number,
542
+ yMax: number,
543
+ chart: Chart
544
+ ): AbstractImage.Component {
545
+ const points = chart.chartPoints.map((p) => {
546
+ const xAxis = p.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
547
+ const yAxis = p.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
548
+ const position = Axis.transformPoint(p.position, xMin, xMax, yMin, yMax, xAxis, yAxis);
549
+ const shape = generatePointShape(p, position);
550
+ return AbstractImage.createGroup(p.label, [
551
+ shape,
552
+ AbstractImage.createText(
553
+ position,
554
+ p.label,
555
+ "Arial",
556
+ chart.fontSize,
557
+ AbstractImage.black,
558
+ "normal",
559
+ 0,
560
+ "center",
561
+ "right",
562
+ "down",
563
+ 0,
564
+ AbstractImage.black,
565
+ false
566
+ ),
567
+ ]);
568
+ });
569
+ return AbstractImage.createGroup("Points", points);
570
+ }
571
+
572
+ function generatePointShape(p: ChartPoint, position: AbstractImage.Point): AbstractImage.Component {
573
+ const halfWidth = p.size.width * 0.5;
574
+ const halfHeight = p.size.height * 0.5;
575
+ if (p.shape === "triangle") {
576
+ const trianglePoints = [
577
+ AbstractImage.createPoint(position.x, position.y + halfHeight),
578
+ AbstractImage.createPoint(position.x - halfWidth, position.y - halfHeight),
579
+ AbstractImage.createPoint(position.x + halfWidth, position.y - halfHeight),
580
+ ];
581
+ return AbstractImage.createPolygon(trianglePoints, AbstractImage.black, 1, p.color);
582
+ } else if (p.shape === "square") {
583
+ const topLeft = AbstractImage.createPoint(position.x - halfWidth, position.y - halfHeight);
584
+ const bottomRight = AbstractImage.createPoint(position.x + halfWidth, position.y + halfHeight);
585
+ return AbstractImage.createRectangle(topLeft, bottomRight, AbstractImage.black, 1, p.color);
586
+ } else {
587
+ const topLeft = AbstractImage.createPoint(position.x - halfWidth, position.y - halfHeight);
588
+ const bottomRight = AbstractImage.createPoint(position.x + halfWidth, position.y + halfHeight);
589
+ return AbstractImage.createEllipse(topLeft, bottomRight, AbstractImage.black, 1, p.color);
590
+ }
591
+ }
592
+
593
+ export function generateXAxisGridLines(
594
+ xMin: number,
595
+ xMax: number,
596
+ yMin: number,
597
+ yMax: number,
598
+ xTicks: Array<number>,
599
+ xAxis: Axis.Axis,
600
+ chart: Chart
601
+ ): AbstractImage.Component {
602
+ const xLines = xTicks.map((l) => {
603
+ const x = Axis.transformValue(l, xMin, xMax, xAxis);
604
+ const start = AbstractImage.createPoint(x, yMin);
605
+ const end = AbstractImage.createPoint(x, yMax);
606
+ return AbstractImage.createLine(start, end, chart.gridColor, chart.gridThickness);
607
+ });
608
+
609
+ return AbstractImage.createGroup("Lines", xLines);
610
+ }
611
+
612
+ export function generateXAxisLabels(
613
+ xMin: number,
614
+ xMax: number,
615
+ y: number,
616
+ growVertical: AbstractImage.GrowthDirection,
617
+ xTicks: Array<number>,
618
+ xAxis: Axis.Axis,
619
+ chart: Chart
620
+ ): AbstractImage.Component {
621
+ const xLabels = xTicks.map((l) => {
622
+ const position = AbstractImage.createPoint(Axis.transformValue(l, xMin, xMax, xAxis), y);
623
+ return AbstractImage.createText(
624
+ position,
625
+ formatNumber(l),
626
+ "Arial",
627
+ chart.fontSize,
628
+ AbstractImage.black,
629
+ "normal",
630
+ 0,
631
+ "center",
632
+ "uniform",
633
+ growVertical,
634
+ 0,
635
+ AbstractImage.black,
636
+ false
637
+ );
638
+ });
639
+ return AbstractImage.createGroup("Labels", xLabels);
640
+ }
641
+
642
+ export function generateXAxisLabel(
643
+ x: number,
644
+ y: number,
645
+ horizontalGrowthDirection: AbstractImage.GrowthDirection,
646
+ verticalGrowthDirection: AbstractImage.GrowthDirection,
647
+ label: string,
648
+ chart: Chart
649
+ ): AbstractImage.Component {
650
+ const position = AbstractImage.createPoint(x, y);
651
+ return AbstractImage.createText(
652
+ position,
653
+ label,
654
+ "Arial",
655
+ chart.fontSize,
656
+ AbstractImage.black,
657
+ "normal",
658
+ 0,
659
+ "center",
660
+ horizontalGrowthDirection,
661
+ verticalGrowthDirection,
662
+ 0,
663
+ AbstractImage.black,
664
+ false
665
+ );
666
+ }
667
+
668
+ export function generateYAxisLines(
669
+ xMin: number,
670
+ xMax: number,
671
+ yMin: number,
672
+ yMax: number,
673
+ yTicks: Array<number>,
674
+ yAxis: Axis.Axis,
675
+ chart: Chart
676
+ ): AbstractImage.Component {
677
+ const yLines = yTicks.map((l) => {
678
+ const y = Axis.transformValue(l, yMin, yMax, yAxis);
679
+ const start = AbstractImage.createPoint(xMin, y);
680
+ const end = AbstractImage.createPoint(xMax, y);
681
+ return AbstractImage.createLine(start, end, chart.gridColor, chart.gridThickness);
682
+ });
683
+ return AbstractImage.createGroup("Lines", yLines);
684
+ }
685
+
686
+ export function generateYAxisLabels(
687
+ x: number,
688
+ yMin: number,
689
+ yMax: number,
690
+ growHorizontal: AbstractImage.GrowthDirection,
691
+ yTicks: Array<number>,
692
+ yAxis: Axis.Axis,
693
+ chart: Chart
694
+ ): AbstractImage.Component {
695
+ const yLabels = yTicks.map((l) => {
696
+ const position = AbstractImage.createPoint(x, Axis.transformValue(l, yMin, yMax, yAxis));
697
+ return AbstractImage.createText(
698
+ position,
699
+ formatNumber(l),
700
+ "Arial",
701
+ chart.fontSize,
702
+ AbstractImage.black,
703
+ "normal",
704
+ 0,
705
+ "center",
706
+ growHorizontal,
707
+ "uniform",
708
+ 0,
709
+ AbstractImage.black,
710
+ false
711
+ );
712
+ });
713
+ return AbstractImage.createGroup("Labels", yLabels);
714
+ }
715
+
716
+ export function generateYAxisLabel(
717
+ x: number,
718
+ y: number,
719
+ horizontalGrowthDirection: AbstractImage.GrowthDirection,
720
+ verticalGrowthDirection: AbstractImage.GrowthDirection,
721
+ label: string,
722
+ chart: Chart
723
+ ): AbstractImage.Component {
724
+ const position = AbstractImage.createPoint(x, y);
725
+ return AbstractImage.createText(
726
+ position,
727
+ label,
728
+ "Arial",
729
+ chart.fontSize,
730
+ AbstractImage.black,
731
+ "normal",
732
+ -90,
733
+ "center",
734
+ horizontalGrowthDirection,
735
+ verticalGrowthDirection,
736
+ 0,
737
+ AbstractImage.black,
738
+ false
739
+ );
740
+ }
741
+
742
+ function formatNumber(n: number): string {
743
+ if (n >= 10000000) {
744
+ return numberToString(n / 1000000) + "m";
745
+ }
746
+ if (n >= 10000) {
747
+ return numberToString(n / 1000) + "k";
748
+ }
749
+ return numberToString(n);
750
+ }
751
+
752
+ function numberToString(n: number): string {
753
+ return parseFloat(n.toPrecision(5)).toString();
754
+ }
755
+
756
+ function labelPadding(numberOfCharacters: number, fontSize: number, characterOffset: number): number {
757
+ return ((numberOfCharacters + 1 + characterOffset) * fontSize * 3) / 4;
758
+ }