abstract-chart 3.2.1 → 3.2.3
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/CHANGELOG.md +15 -15
- package/LICENSE +21 -21
- package/README.md +106 -106
- package/package.json +3 -3
- package/src/axis.ts +234 -234
- package/src/chart.ts +761 -761
- package/src/index.ts +2 -2
package/src/chart.ts
CHANGED
|
@@ -1,761 +1,761 @@
|
|
|
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 font: string;
|
|
25
|
-
readonly fontSize: number;
|
|
26
|
-
readonly labelLayout: LabelLayout;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type ChartProps = Partial<Chart>;
|
|
30
|
-
|
|
31
|
-
export function createChart(props: ChartProps): Chart {
|
|
32
|
-
const {
|
|
33
|
-
width = 600,
|
|
34
|
-
height = 400,
|
|
35
|
-
chartPoints = [],
|
|
36
|
-
chartLines = [],
|
|
37
|
-
chartStack = createChartStack({}),
|
|
38
|
-
xAxisBottom = Axis.createLinearAxis(0, 100, ""),
|
|
39
|
-
xAxisTop = undefined,
|
|
40
|
-
yAxisLeft = Axis.createLinearAxis(0, 100, ""),
|
|
41
|
-
yAxisRight = undefined,
|
|
42
|
-
backgroundColor = AbstractImage.white,
|
|
43
|
-
gridColor = AbstractImage.gray,
|
|
44
|
-
gridThickness = 1,
|
|
45
|
-
font = "Arial",
|
|
46
|
-
fontSize = 12,
|
|
47
|
-
labelLayout = "original",
|
|
48
|
-
} = props || {};
|
|
49
|
-
return {
|
|
50
|
-
width,
|
|
51
|
-
height,
|
|
52
|
-
chartPoints,
|
|
53
|
-
chartLines,
|
|
54
|
-
chartStack,
|
|
55
|
-
xAxisBottom,
|
|
56
|
-
xAxisTop,
|
|
57
|
-
yAxisLeft,
|
|
58
|
-
yAxisRight,
|
|
59
|
-
backgroundColor,
|
|
60
|
-
gridColor,
|
|
61
|
-
gridThickness,
|
|
62
|
-
font,
|
|
63
|
-
fontSize,
|
|
64
|
-
labelLayout,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export type XAxis = "bottom" | "top";
|
|
69
|
-
export type YAxis = "left" | "right";
|
|
70
|
-
|
|
71
|
-
export type ChartPointShape = "circle" | "triangle" | "square";
|
|
72
|
-
|
|
73
|
-
export interface ChartPoint {
|
|
74
|
-
readonly shape: ChartPointShape;
|
|
75
|
-
readonly position: AbstractImage.Point;
|
|
76
|
-
readonly color: AbstractImage.Color;
|
|
77
|
-
readonly size: AbstractImage.Size;
|
|
78
|
-
readonly label: string;
|
|
79
|
-
readonly xAxis: XAxis;
|
|
80
|
-
readonly yAxis: YAxis;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export type ChartPointProps = Partial<ChartPoint>;
|
|
84
|
-
|
|
85
|
-
export function createChartPoint(props?: ChartPointProps): ChartPoint {
|
|
86
|
-
const {
|
|
87
|
-
shape = "circle",
|
|
88
|
-
position = AbstractImage.createPoint(0, 0),
|
|
89
|
-
color = AbstractImage.black,
|
|
90
|
-
size = AbstractImage.createSize(6, 6),
|
|
91
|
-
label = "",
|
|
92
|
-
xAxis = "bottom",
|
|
93
|
-
yAxis = "left",
|
|
94
|
-
} = props || {};
|
|
95
|
-
return {
|
|
96
|
-
shape,
|
|
97
|
-
position,
|
|
98
|
-
color,
|
|
99
|
-
size,
|
|
100
|
-
label,
|
|
101
|
-
xAxis,
|
|
102
|
-
yAxis,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface ChartLine {
|
|
107
|
-
readonly points: Array<AbstractImage.Point>;
|
|
108
|
-
readonly color: AbstractImage.Color;
|
|
109
|
-
readonly thickness: number;
|
|
110
|
-
readonly label: string;
|
|
111
|
-
readonly xAxis: XAxis;
|
|
112
|
-
readonly yAxis: YAxis;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export type ChartLineProps = Partial<ChartLine>;
|
|
116
|
-
|
|
117
|
-
export function createChartLine(props: ChartLineProps): ChartLine {
|
|
118
|
-
const {
|
|
119
|
-
points = [],
|
|
120
|
-
color = AbstractImage.black,
|
|
121
|
-
thickness = 1,
|
|
122
|
-
label = "",
|
|
123
|
-
xAxis = "bottom",
|
|
124
|
-
yAxis = "left",
|
|
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(props: ChartStackConfigProps): ChartStackConfig {
|
|
144
|
-
const { color = AbstractImage.black, label = "" } = props || {};
|
|
145
|
-
return {
|
|
146
|
-
color,
|
|
147
|
-
label,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export interface StackPoints {
|
|
152
|
-
readonly x: number;
|
|
153
|
-
readonly ys: ReadonlyArray<number>;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export interface ChartStack {
|
|
157
|
-
readonly points: Array<StackPoints>;
|
|
158
|
-
readonly xAxis: XAxis;
|
|
159
|
-
readonly yAxis: YAxis;
|
|
160
|
-
readonly config: ReadonlyArray<ChartStackConfig>;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export type ChartStackProps = Partial<ChartStack>;
|
|
164
|
-
|
|
165
|
-
export function createChartStack(props: ChartStackProps): ChartStack {
|
|
166
|
-
const { points = [], xAxis = "bottom", yAxis = "left", config = [createChartStackConfig({})] } = props || {};
|
|
167
|
-
return {
|
|
168
|
-
points,
|
|
169
|
-
xAxis,
|
|
170
|
-
yAxis,
|
|
171
|
-
config,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const padding = 80;
|
|
176
|
-
|
|
177
|
-
export function inverseTransformPoint(
|
|
178
|
-
point: AbstractImage.Point,
|
|
179
|
-
chart: Chart,
|
|
180
|
-
xAxis: XAxis,
|
|
181
|
-
yAxis: YAxis
|
|
182
|
-
): AbstractImage.Point | undefined {
|
|
183
|
-
const xMin = padding;
|
|
184
|
-
const xMax = chart.width - padding;
|
|
185
|
-
const yMin = chart.height - 0.5 * padding;
|
|
186
|
-
const yMax = 0.5 * padding;
|
|
187
|
-
const x = Axis.inverseTransformValue(point.x, xMin, xMax, xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom);
|
|
188
|
-
const y = Axis.inverseTransformValue(point.y, yMin, yMax, yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft);
|
|
189
|
-
if (x === undefined || y === undefined) {
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
192
|
-
return AbstractImage.createPoint(x, y);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function renderChart(chart: Chart): AbstractImage.AbstractImage {
|
|
196
|
-
const { width, height, xAxisBottom, xAxisTop, yAxisLeft, yAxisRight } = chart;
|
|
197
|
-
|
|
198
|
-
const gridWidth = width - 2 * padding;
|
|
199
|
-
const gridHeight = height - padding;
|
|
200
|
-
|
|
201
|
-
const xMin = padding;
|
|
202
|
-
const xMax = width - padding;
|
|
203
|
-
const yMin = height - 0.5 * padding;
|
|
204
|
-
const yMax = 0.5 * padding;
|
|
205
|
-
|
|
206
|
-
const renderedBackground = generateBackground(xMin, xMax, yMin, yMax, chart);
|
|
207
|
-
|
|
208
|
-
const xNumTicks = gridWidth / 40;
|
|
209
|
-
const renderedXAxisBottom = generateXAxisBottom(xNumTicks, xAxisBottom, xMin, xMax, yMin, yMax, chart);
|
|
210
|
-
const renderedXAxisTop = generateXAxisTop(xNumTicks, xAxisTop, xMin, xMax, yMax, chart);
|
|
211
|
-
|
|
212
|
-
const yNumTicks = gridHeight / 40;
|
|
213
|
-
const renderedYAxisLeft = generateYAxisLeft(yNumTicks, yAxisLeft, xMin, xMax, yMin, yMax, chart);
|
|
214
|
-
const renderedYAxisRight = generateYAxisRight(yNumTicks, yAxisRight, xMax, yMin, yMax, chart);
|
|
215
|
-
|
|
216
|
-
const renderedPoints = generatePoints(xMin, xMax, yMin, yMax, chart);
|
|
217
|
-
const renderedLines = generateLines(xMin, xMax, yMin, yMax, chart);
|
|
218
|
-
const renderedStack = generateStack(xMin, xMax, yMin, yMax, chart);
|
|
219
|
-
|
|
220
|
-
const components = [
|
|
221
|
-
renderedBackground,
|
|
222
|
-
renderedXAxisBottom,
|
|
223
|
-
renderedXAxisTop,
|
|
224
|
-
renderedYAxisLeft,
|
|
225
|
-
renderedYAxisRight,
|
|
226
|
-
renderedStack,
|
|
227
|
-
renderedLines,
|
|
228
|
-
renderedPoints,
|
|
229
|
-
];
|
|
230
|
-
const topLeft = AbstractImage.createPoint(0, 0);
|
|
231
|
-
const size = AbstractImage.createSize(width, height);
|
|
232
|
-
return AbstractImage.createAbstractImage(topLeft, size, AbstractImage.white, components);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export function generateBackground(
|
|
236
|
-
xMin: number,
|
|
237
|
-
xMax: number,
|
|
238
|
-
yMin: number,
|
|
239
|
-
yMax: number,
|
|
240
|
-
chart: Chart
|
|
241
|
-
): AbstractImage.Component {
|
|
242
|
-
const topLeft = AbstractImage.createPoint(xMin, yMax);
|
|
243
|
-
const bottomRight = AbstractImage.createPoint(xMax, yMin);
|
|
244
|
-
return AbstractImage.createRectangle(
|
|
245
|
-
topLeft,
|
|
246
|
-
bottomRight,
|
|
247
|
-
chart.gridColor,
|
|
248
|
-
chart.gridThickness,
|
|
249
|
-
chart.backgroundColor
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export function generateXAxisBottom(
|
|
254
|
-
xNumTicks: number,
|
|
255
|
-
xAxisBottom: Axis.Axis | undefined,
|
|
256
|
-
xMin: number,
|
|
257
|
-
xMax: number,
|
|
258
|
-
yMin: number,
|
|
259
|
-
yMax: number,
|
|
260
|
-
chart: Chart
|
|
261
|
-
): AbstractImage.Component {
|
|
262
|
-
if (!xAxisBottom) {
|
|
263
|
-
return AbstractImage.createGroup("XAxisBottom", []);
|
|
264
|
-
}
|
|
265
|
-
const xTicks = Axis.getTicks(xNumTicks, xAxisBottom);
|
|
266
|
-
const xLines = generateXAxisGridLines(xMin, xMax, yMin + 10, yMax, xTicks, xAxisBottom, chart);
|
|
267
|
-
const xLabels = generateXAxisLabels(xMin, xMax, yMin + 10, "down", xTicks, xAxisBottom, chart);
|
|
268
|
-
|
|
269
|
-
let xLabel: AbstractImage.Component;
|
|
270
|
-
switch (chart.labelLayout) {
|
|
271
|
-
case "original":
|
|
272
|
-
xLabel = generateXAxisLabel(xMax + 0.5 * padding, yMin + 10, "uniform", "down", xAxisBottom.label, chart);
|
|
273
|
-
break;
|
|
274
|
-
|
|
275
|
-
case "end":
|
|
276
|
-
xLabel = generateXAxisLabel(xMax, yMin + 25, "left", "down", xAxisBottom.label, chart);
|
|
277
|
-
break;
|
|
278
|
-
|
|
279
|
-
case "center":
|
|
280
|
-
xLabel = generateXAxisLabel((xMin + xMax) / 2, yMin + 25, "uniform", "down", xAxisBottom.label, chart);
|
|
281
|
-
break;
|
|
282
|
-
|
|
283
|
-
default:
|
|
284
|
-
return exhaustiveCheck(chart.labelLayout);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return AbstractImage.createGroup("XAxisBottom", [xLines, xLabels, xLabel]);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export function generateXAxisTop(
|
|
291
|
-
xNumTicks: number,
|
|
292
|
-
xAxisTop: Axis.Axis | undefined,
|
|
293
|
-
xMin: number,
|
|
294
|
-
xMax: number,
|
|
295
|
-
yMax: number,
|
|
296
|
-
chart: Chart
|
|
297
|
-
): AbstractImage.Component {
|
|
298
|
-
if (!xAxisTop) {
|
|
299
|
-
return AbstractImage.createGroup("XAxisTop", []);
|
|
300
|
-
}
|
|
301
|
-
const xTicks2 = Axis.getTicks(xNumTicks, xAxisTop);
|
|
302
|
-
const xLines2 = generateXAxisGridLines(xMin, xMax, yMax - 10, yMax, xTicks2, xAxisTop, chart);
|
|
303
|
-
const xLabels2 = generateXAxisLabels(xMin, xMax, yMax - 13, "up", xTicks2, xAxisTop, chart);
|
|
304
|
-
|
|
305
|
-
let xLabel2: AbstractImage.Component;
|
|
306
|
-
switch (chart.labelLayout) {
|
|
307
|
-
case "original":
|
|
308
|
-
xLabel2 = generateXAxisLabel(xMax + 0.5 * padding, yMax - 13, "uniform", "up", xAxisTop.label, chart);
|
|
309
|
-
break;
|
|
310
|
-
|
|
311
|
-
case "end":
|
|
312
|
-
xLabel2 = generateXAxisLabel(xMax, yMax - 30, "left", "up", xAxisTop.label, chart);
|
|
313
|
-
break;
|
|
314
|
-
|
|
315
|
-
case "center":
|
|
316
|
-
xLabel2 = generateXAxisLabel((xMin + xMax) / 2, yMax - 30, "uniform", "up", xAxisTop.label, chart);
|
|
317
|
-
break;
|
|
318
|
-
|
|
319
|
-
default:
|
|
320
|
-
return exhaustiveCheck(chart.labelLayout);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return AbstractImage.createGroup("XAxisTop", [xLines2, xLabels2, xLabel2]);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export function generateYAxisLeft(
|
|
327
|
-
yNumTicks: number,
|
|
328
|
-
yAxisLeft: Axis.Axis | undefined,
|
|
329
|
-
xMin: number,
|
|
330
|
-
xMax: number,
|
|
331
|
-
yMin: number,
|
|
332
|
-
yMax: number,
|
|
333
|
-
chart: Chart
|
|
334
|
-
): AbstractImage.Component {
|
|
335
|
-
if (!yAxisLeft) {
|
|
336
|
-
return AbstractImage.createGroup("YAxisLeft", []);
|
|
337
|
-
}
|
|
338
|
-
const yTicks = Axis.getTicks(yNumTicks, yAxisLeft);
|
|
339
|
-
const yLines = generateYAxisLines(xMin - 5, xMax, yMin, yMax, yTicks, yAxisLeft, chart);
|
|
340
|
-
const yLabels = generateYAxisLabels(xMin - 7, yMin, yMax, "left", yTicks, yAxisLeft, chart);
|
|
341
|
-
|
|
342
|
-
const labelPaddingLeft = 5 + labelPadding(formatNumber(yAxisLeft.max).length, chart.fontSize, 0.5);
|
|
343
|
-
|
|
344
|
-
let yLabel: AbstractImage.Component;
|
|
345
|
-
switch (chart.labelLayout) {
|
|
346
|
-
case "original":
|
|
347
|
-
yLabel = generateYAxisLabel(
|
|
348
|
-
xMin - labelPaddingLeft,
|
|
349
|
-
yMax + 0.5 * padding,
|
|
350
|
-
"uniform",
|
|
351
|
-
"up",
|
|
352
|
-
yAxisLeft.label,
|
|
353
|
-
chart
|
|
354
|
-
);
|
|
355
|
-
break;
|
|
356
|
-
|
|
357
|
-
case "end":
|
|
358
|
-
yLabel = generateYAxisLabel(xMin - labelPaddingLeft, yMax, "left", "up", yAxisLeft.label, chart);
|
|
359
|
-
break;
|
|
360
|
-
|
|
361
|
-
case "center":
|
|
362
|
-
yLabel = generateYAxisLabel(xMin - labelPaddingLeft, (yMin + yMax) / 2, "uniform", "up", yAxisLeft.label, chart);
|
|
363
|
-
break;
|
|
364
|
-
|
|
365
|
-
default:
|
|
366
|
-
return exhaustiveCheck(chart.labelLayout);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return AbstractImage.createGroup("YAxisLeft", [yLines, yLabels, yLabel]);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
export function generateYAxisRight(
|
|
373
|
-
yNumTicks: number,
|
|
374
|
-
yAxisRight: Axis.Axis | undefined,
|
|
375
|
-
xMax: number,
|
|
376
|
-
yMin: number,
|
|
377
|
-
yMax: number,
|
|
378
|
-
chart: Chart
|
|
379
|
-
): AbstractImage.Component {
|
|
380
|
-
if (!yAxisRight) {
|
|
381
|
-
return AbstractImage.createGroup("YAxisRight", []);
|
|
382
|
-
}
|
|
383
|
-
const yTicks2 = Axis.getTicks(yNumTicks, yAxisRight);
|
|
384
|
-
const yLines2 = generateYAxisLines(xMax - 5, xMax + 5, yMin, yMax, yTicks2, yAxisRight, chart);
|
|
385
|
-
const yLabels2 = generateYAxisLabels(xMax + 7, yMin, yMax, "right", yTicks2, yAxisRight, chart);
|
|
386
|
-
|
|
387
|
-
const labelPaddingRight = 7 + labelPadding(formatNumber(yAxisRight.max).length, chart.fontSize, 1.5);
|
|
388
|
-
|
|
389
|
-
let yLabel2: AbstractImage.Component;
|
|
390
|
-
switch (chart.labelLayout) {
|
|
391
|
-
case "original":
|
|
392
|
-
yLabel2 = generateYAxisLabel(
|
|
393
|
-
xMax + labelPaddingRight,
|
|
394
|
-
yMax + 0.5 * padding,
|
|
395
|
-
"uniform",
|
|
396
|
-
"up",
|
|
397
|
-
yAxisRight.label,
|
|
398
|
-
chart
|
|
399
|
-
);
|
|
400
|
-
break;
|
|
401
|
-
|
|
402
|
-
case "end":
|
|
403
|
-
yLabel2 = generateYAxisLabel(xMax + labelPaddingRight, yMax, "left", "up", yAxisRight.label, chart);
|
|
404
|
-
break;
|
|
405
|
-
|
|
406
|
-
case "center":
|
|
407
|
-
yLabel2 = generateYAxisLabel(
|
|
408
|
-
xMax + labelPaddingRight,
|
|
409
|
-
(yMin + yMax) / 2,
|
|
410
|
-
"uniform",
|
|
411
|
-
"up",
|
|
412
|
-
yAxisRight.label,
|
|
413
|
-
chart
|
|
414
|
-
);
|
|
415
|
-
break;
|
|
416
|
-
|
|
417
|
-
default:
|
|
418
|
-
return exhaustiveCheck(chart.labelLayout);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
return AbstractImage.createGroup("YAxisRight", [yLines2, yLabels2, yLabel2]);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export function generateStack(
|
|
425
|
-
xMin: number,
|
|
426
|
-
xMax: number,
|
|
427
|
-
yMin: number,
|
|
428
|
-
yMax: number,
|
|
429
|
-
chart: Chart
|
|
430
|
-
): AbstractImage.Component {
|
|
431
|
-
const pointsPos = chart.chartStack.points.map((stackPoint) => ({
|
|
432
|
-
x: stackPoint.x,
|
|
433
|
-
ys: [...stackPoint.ys.map((y) => Math.min(0, y))],
|
|
434
|
-
}));
|
|
435
|
-
|
|
436
|
-
const stackPos = generateUnsignedStack(xMin, xMax, yMin, yMax, {
|
|
437
|
-
...chart,
|
|
438
|
-
chartStack: { ...chart.chartStack, points: pointsPos },
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
const pointsNeg = chart.chartStack.points.map((stackPoint) => ({
|
|
442
|
-
x: stackPoint.x,
|
|
443
|
-
ys: [...stackPoint.ys.map((y) => Math.max(0, y))],
|
|
444
|
-
}));
|
|
445
|
-
|
|
446
|
-
const stackNeg = generateUnsignedStack(xMin, xMax, yMin, yMax, {
|
|
447
|
-
...chart,
|
|
448
|
-
chartStack: { ...chart.chartStack, points: pointsNeg },
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
return AbstractImage.createGroup("Stacks", [stackPos, stackNeg]);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function generateUnsignedStack(
|
|
455
|
-
xMin: number,
|
|
456
|
-
xMax: number,
|
|
457
|
-
yMin: number,
|
|
458
|
-
yMax: number,
|
|
459
|
-
chart: Chart
|
|
460
|
-
): AbstractImage.Component {
|
|
461
|
-
if (chart.chartStack.points.length < 2) {
|
|
462
|
-
return AbstractImage.createGroup("stack", []);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const xAxis = chart.chartStack.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
|
|
466
|
-
const yAxis = chart.chartStack.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
|
|
467
|
-
|
|
468
|
-
const xPoints = chart.chartStack.points.map((stackPoints) => {
|
|
469
|
-
let sumY = 0;
|
|
470
|
-
const points = stackPoints.ys.map((y) => {
|
|
471
|
-
sumY += y;
|
|
472
|
-
return Axis.transformPoint(AbstractImage.createPoint(stackPoints.x, sumY), xMin, xMax, yMin, yMax, xAxis, yAxis);
|
|
473
|
-
});
|
|
474
|
-
return points;
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
// Transpose the xPoints data to lines.
|
|
478
|
-
const lines: Array<Array<AbstractImage.Point>> = [];
|
|
479
|
-
for (let i = 0; i < xPoints[0].length; ++i) {
|
|
480
|
-
lines[i] = [];
|
|
481
|
-
for (const points of xPoints) {
|
|
482
|
-
lines[i].push(points[i]);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const polygons: Array<AbstractImage.Polygon> = [];
|
|
487
|
-
let lastLine = chart.chartStack.points.map((stackPoint) =>
|
|
488
|
-
Axis.transformPoint(AbstractImage.createPoint(stackPoint.x, 0), xMin, xMax, yMin, yMax, xAxis, yAxis)
|
|
489
|
-
);
|
|
490
|
-
lines.forEach((line, index) => {
|
|
491
|
-
const config = chart.chartStack.config[index];
|
|
492
|
-
if (!config) {
|
|
493
|
-
throw new Error("Missing config for series " + index);
|
|
494
|
-
}
|
|
495
|
-
const color = config.color;
|
|
496
|
-
const points = [...line, ...lastLine.slice().reverse()];
|
|
497
|
-
lastLine = line;
|
|
498
|
-
polygons.push(AbstractImage.createPolygon(points, color, 0, color));
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
return AbstractImage.createGroup("Stack", polygons);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
export function generateLines(
|
|
505
|
-
xMin: number,
|
|
506
|
-
xMax: number,
|
|
507
|
-
yMin: number,
|
|
508
|
-
yMax: number,
|
|
509
|
-
chart: Chart
|
|
510
|
-
): AbstractImage.Component {
|
|
511
|
-
const lines = chart.chartLines.map((l: ChartLine) => {
|
|
512
|
-
if (l.points.length < 2) {
|
|
513
|
-
return AbstractImage.createGroup(l.label, []);
|
|
514
|
-
}
|
|
515
|
-
const xAxis = l.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
|
|
516
|
-
const yAxis = l.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
|
|
517
|
-
const points = l.points.map((p) => Axis.transformPoint(p, xMin, xMax, yMin, yMax, xAxis, yAxis));
|
|
518
|
-
const last = points[points.length - 1];
|
|
519
|
-
return AbstractImage.createGroup(l.label, [
|
|
520
|
-
AbstractImage.createPolyLine(points, l.color, l.thickness),
|
|
521
|
-
AbstractImage.createText(
|
|
522
|
-
last,
|
|
523
|
-
l.label,
|
|
524
|
-
chart.font,
|
|
525
|
-
chart.fontSize,
|
|
526
|
-
AbstractImage.black,
|
|
527
|
-
"normal",
|
|
528
|
-
0,
|
|
529
|
-
"center",
|
|
530
|
-
"right",
|
|
531
|
-
"down",
|
|
532
|
-
0,
|
|
533
|
-
AbstractImage.black,
|
|
534
|
-
false
|
|
535
|
-
),
|
|
536
|
-
]);
|
|
537
|
-
});
|
|
538
|
-
return AbstractImage.createGroup("Lines", lines);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
export function generatePoints(
|
|
542
|
-
xMin: number,
|
|
543
|
-
xMax: number,
|
|
544
|
-
yMin: number,
|
|
545
|
-
yMax: number,
|
|
546
|
-
chart: Chart
|
|
547
|
-
): AbstractImage.Component {
|
|
548
|
-
const points = chart.chartPoints.map((p) => {
|
|
549
|
-
const xAxis = p.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
|
|
550
|
-
const yAxis = p.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
|
|
551
|
-
const position = Axis.transformPoint(p.position, xMin, xMax, yMin, yMax, xAxis, yAxis);
|
|
552
|
-
const shape = generatePointShape(p, position);
|
|
553
|
-
return AbstractImage.createGroup(p.label, [
|
|
554
|
-
shape,
|
|
555
|
-
AbstractImage.createText(
|
|
556
|
-
position,
|
|
557
|
-
p.label,
|
|
558
|
-
chart.font,
|
|
559
|
-
chart.fontSize,
|
|
560
|
-
AbstractImage.black,
|
|
561
|
-
"normal",
|
|
562
|
-
0,
|
|
563
|
-
"center",
|
|
564
|
-
"right",
|
|
565
|
-
"down",
|
|
566
|
-
0,
|
|
567
|
-
AbstractImage.black,
|
|
568
|
-
false
|
|
569
|
-
),
|
|
570
|
-
]);
|
|
571
|
-
});
|
|
572
|
-
return AbstractImage.createGroup("Points", points);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function generatePointShape(p: ChartPoint, position: AbstractImage.Point): AbstractImage.Component {
|
|
576
|
-
const halfWidth = p.size.width * 0.5;
|
|
577
|
-
const halfHeight = p.size.height * 0.5;
|
|
578
|
-
if (p.shape === "triangle") {
|
|
579
|
-
const trianglePoints = [
|
|
580
|
-
AbstractImage.createPoint(position.x, position.y + halfHeight),
|
|
581
|
-
AbstractImage.createPoint(position.x - halfWidth, position.y - halfHeight),
|
|
582
|
-
AbstractImage.createPoint(position.x + halfWidth, position.y - halfHeight),
|
|
583
|
-
];
|
|
584
|
-
return AbstractImage.createPolygon(trianglePoints, AbstractImage.black, 1, p.color);
|
|
585
|
-
} else if (p.shape === "square") {
|
|
586
|
-
const topLeft = AbstractImage.createPoint(position.x - halfWidth, position.y - halfHeight);
|
|
587
|
-
const bottomRight = AbstractImage.createPoint(position.x + halfWidth, position.y + halfHeight);
|
|
588
|
-
return AbstractImage.createRectangle(topLeft, bottomRight, AbstractImage.black, 1, p.color);
|
|
589
|
-
} else {
|
|
590
|
-
const topLeft = AbstractImage.createPoint(position.x - halfWidth, position.y - halfHeight);
|
|
591
|
-
const bottomRight = AbstractImage.createPoint(position.x + halfWidth, position.y + halfHeight);
|
|
592
|
-
return AbstractImage.createEllipse(topLeft, bottomRight, AbstractImage.black, 1, p.color);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
export function generateXAxisGridLines(
|
|
597
|
-
xMin: number,
|
|
598
|
-
xMax: number,
|
|
599
|
-
yMin: number,
|
|
600
|
-
yMax: number,
|
|
601
|
-
xTicks: Array<number>,
|
|
602
|
-
xAxis: Axis.Axis,
|
|
603
|
-
chart: Chart
|
|
604
|
-
): AbstractImage.Component {
|
|
605
|
-
const xLines = xTicks.map((l) => {
|
|
606
|
-
const x = Axis.transformValue(l, xMin, xMax, xAxis);
|
|
607
|
-
const start = AbstractImage.createPoint(x, yMin);
|
|
608
|
-
const end = AbstractImage.createPoint(x, yMax);
|
|
609
|
-
return AbstractImage.createLine(start, end, chart.gridColor, chart.gridThickness);
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
return AbstractImage.createGroup("Lines", xLines);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
export function generateXAxisLabels(
|
|
616
|
-
xMin: number,
|
|
617
|
-
xMax: number,
|
|
618
|
-
y: number,
|
|
619
|
-
growVertical: AbstractImage.GrowthDirection,
|
|
620
|
-
xTicks: Array<number>,
|
|
621
|
-
xAxis: Axis.Axis,
|
|
622
|
-
chart: Chart
|
|
623
|
-
): AbstractImage.Component {
|
|
624
|
-
const xLabels = xTicks.map((l) => {
|
|
625
|
-
const position = AbstractImage.createPoint(Axis.transformValue(l, xMin, xMax, xAxis), y);
|
|
626
|
-
return AbstractImage.createText(
|
|
627
|
-
position,
|
|
628
|
-
formatNumber(l),
|
|
629
|
-
chart.font,
|
|
630
|
-
chart.fontSize,
|
|
631
|
-
AbstractImage.black,
|
|
632
|
-
"normal",
|
|
633
|
-
0,
|
|
634
|
-
"center",
|
|
635
|
-
"uniform",
|
|
636
|
-
growVertical,
|
|
637
|
-
0,
|
|
638
|
-
AbstractImage.black,
|
|
639
|
-
false
|
|
640
|
-
);
|
|
641
|
-
});
|
|
642
|
-
return AbstractImage.createGroup("Labels", xLabels);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
export function generateXAxisLabel(
|
|
646
|
-
x: number,
|
|
647
|
-
y: number,
|
|
648
|
-
horizontalGrowthDirection: AbstractImage.GrowthDirection,
|
|
649
|
-
verticalGrowthDirection: AbstractImage.GrowthDirection,
|
|
650
|
-
label: string,
|
|
651
|
-
chart: Chart
|
|
652
|
-
): AbstractImage.Component {
|
|
653
|
-
const position = AbstractImage.createPoint(x, y);
|
|
654
|
-
return AbstractImage.createText(
|
|
655
|
-
position,
|
|
656
|
-
label,
|
|
657
|
-
chart.font,
|
|
658
|
-
chart.fontSize,
|
|
659
|
-
AbstractImage.black,
|
|
660
|
-
"normal",
|
|
661
|
-
0,
|
|
662
|
-
"center",
|
|
663
|
-
horizontalGrowthDirection,
|
|
664
|
-
verticalGrowthDirection,
|
|
665
|
-
0,
|
|
666
|
-
AbstractImage.black,
|
|
667
|
-
false
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
export function generateYAxisLines(
|
|
672
|
-
xMin: number,
|
|
673
|
-
xMax: number,
|
|
674
|
-
yMin: number,
|
|
675
|
-
yMax: number,
|
|
676
|
-
yTicks: Array<number>,
|
|
677
|
-
yAxis: Axis.Axis,
|
|
678
|
-
chart: Chart
|
|
679
|
-
): AbstractImage.Component {
|
|
680
|
-
const yLines = yTicks.map((l) => {
|
|
681
|
-
const y = Axis.transformValue(l, yMin, yMax, yAxis);
|
|
682
|
-
const start = AbstractImage.createPoint(xMin, y);
|
|
683
|
-
const end = AbstractImage.createPoint(xMax, y);
|
|
684
|
-
return AbstractImage.createLine(start, end, chart.gridColor, chart.gridThickness);
|
|
685
|
-
});
|
|
686
|
-
return AbstractImage.createGroup("Lines", yLines);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
export function generateYAxisLabels(
|
|
690
|
-
x: number,
|
|
691
|
-
yMin: number,
|
|
692
|
-
yMax: number,
|
|
693
|
-
growHorizontal: AbstractImage.GrowthDirection,
|
|
694
|
-
yTicks: Array<number>,
|
|
695
|
-
yAxis: Axis.Axis,
|
|
696
|
-
chart: Chart
|
|
697
|
-
): AbstractImage.Component {
|
|
698
|
-
const yLabels = yTicks.map((l) => {
|
|
699
|
-
const position = AbstractImage.createPoint(x, Axis.transformValue(l, yMin, yMax, yAxis));
|
|
700
|
-
return AbstractImage.createText(
|
|
701
|
-
position,
|
|
702
|
-
formatNumber(l),
|
|
703
|
-
chart.font,
|
|
704
|
-
chart.fontSize,
|
|
705
|
-
AbstractImage.black,
|
|
706
|
-
"normal",
|
|
707
|
-
0,
|
|
708
|
-
"center",
|
|
709
|
-
growHorizontal,
|
|
710
|
-
"uniform",
|
|
711
|
-
0,
|
|
712
|
-
AbstractImage.black,
|
|
713
|
-
false
|
|
714
|
-
);
|
|
715
|
-
});
|
|
716
|
-
return AbstractImage.createGroup("Labels", yLabels);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
export function generateYAxisLabel(
|
|
720
|
-
x: number,
|
|
721
|
-
y: number,
|
|
722
|
-
horizontalGrowthDirection: AbstractImage.GrowthDirection,
|
|
723
|
-
verticalGrowthDirection: AbstractImage.GrowthDirection,
|
|
724
|
-
label: string,
|
|
725
|
-
chart: Chart
|
|
726
|
-
): AbstractImage.Component {
|
|
727
|
-
const position = AbstractImage.createPoint(x, y);
|
|
728
|
-
return AbstractImage.createText(
|
|
729
|
-
position,
|
|
730
|
-
label,
|
|
731
|
-
chart.font,
|
|
732
|
-
chart.fontSize,
|
|
733
|
-
AbstractImage.black,
|
|
734
|
-
"normal",
|
|
735
|
-
-90,
|
|
736
|
-
"center",
|
|
737
|
-
horizontalGrowthDirection,
|
|
738
|
-
verticalGrowthDirection,
|
|
739
|
-
0,
|
|
740
|
-
AbstractImage.black,
|
|
741
|
-
false
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function formatNumber(n: number): string {
|
|
746
|
-
if (n >= 10000000) {
|
|
747
|
-
return numberToString(n / 1000000) + "m";
|
|
748
|
-
}
|
|
749
|
-
if (n >= 10000) {
|
|
750
|
-
return numberToString(n / 1000) + "k";
|
|
751
|
-
}
|
|
752
|
-
return numberToString(n);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
function numberToString(n: number): string {
|
|
756
|
-
return parseFloat(n.toPrecision(5)).toString();
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function labelPadding(numberOfCharacters: number, fontSize: number, characterOffset: number): number {
|
|
760
|
-
return ((numberOfCharacters + 1 + characterOffset) * fontSize * 3) / 4;
|
|
761
|
-
}
|
|
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 font: string;
|
|
25
|
+
readonly fontSize: number;
|
|
26
|
+
readonly labelLayout: LabelLayout;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ChartProps = Partial<Chart>;
|
|
30
|
+
|
|
31
|
+
export function createChart(props: ChartProps): Chart {
|
|
32
|
+
const {
|
|
33
|
+
width = 600,
|
|
34
|
+
height = 400,
|
|
35
|
+
chartPoints = [],
|
|
36
|
+
chartLines = [],
|
|
37
|
+
chartStack = createChartStack({}),
|
|
38
|
+
xAxisBottom = Axis.createLinearAxis(0, 100, ""),
|
|
39
|
+
xAxisTop = undefined,
|
|
40
|
+
yAxisLeft = Axis.createLinearAxis(0, 100, ""),
|
|
41
|
+
yAxisRight = undefined,
|
|
42
|
+
backgroundColor = AbstractImage.white,
|
|
43
|
+
gridColor = AbstractImage.gray,
|
|
44
|
+
gridThickness = 1,
|
|
45
|
+
font = "Arial",
|
|
46
|
+
fontSize = 12,
|
|
47
|
+
labelLayout = "original",
|
|
48
|
+
} = props || {};
|
|
49
|
+
return {
|
|
50
|
+
width,
|
|
51
|
+
height,
|
|
52
|
+
chartPoints,
|
|
53
|
+
chartLines,
|
|
54
|
+
chartStack,
|
|
55
|
+
xAxisBottom,
|
|
56
|
+
xAxisTop,
|
|
57
|
+
yAxisLeft,
|
|
58
|
+
yAxisRight,
|
|
59
|
+
backgroundColor,
|
|
60
|
+
gridColor,
|
|
61
|
+
gridThickness,
|
|
62
|
+
font,
|
|
63
|
+
fontSize,
|
|
64
|
+
labelLayout,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type XAxis = "bottom" | "top";
|
|
69
|
+
export type YAxis = "left" | "right";
|
|
70
|
+
|
|
71
|
+
export type ChartPointShape = "circle" | "triangle" | "square";
|
|
72
|
+
|
|
73
|
+
export interface ChartPoint {
|
|
74
|
+
readonly shape: ChartPointShape;
|
|
75
|
+
readonly position: AbstractImage.Point;
|
|
76
|
+
readonly color: AbstractImage.Color;
|
|
77
|
+
readonly size: AbstractImage.Size;
|
|
78
|
+
readonly label: string;
|
|
79
|
+
readonly xAxis: XAxis;
|
|
80
|
+
readonly yAxis: YAxis;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type ChartPointProps = Partial<ChartPoint>;
|
|
84
|
+
|
|
85
|
+
export function createChartPoint(props?: ChartPointProps): ChartPoint {
|
|
86
|
+
const {
|
|
87
|
+
shape = "circle",
|
|
88
|
+
position = AbstractImage.createPoint(0, 0),
|
|
89
|
+
color = AbstractImage.black,
|
|
90
|
+
size = AbstractImage.createSize(6, 6),
|
|
91
|
+
label = "",
|
|
92
|
+
xAxis = "bottom",
|
|
93
|
+
yAxis = "left",
|
|
94
|
+
} = props || {};
|
|
95
|
+
return {
|
|
96
|
+
shape,
|
|
97
|
+
position,
|
|
98
|
+
color,
|
|
99
|
+
size,
|
|
100
|
+
label,
|
|
101
|
+
xAxis,
|
|
102
|
+
yAxis,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ChartLine {
|
|
107
|
+
readonly points: Array<AbstractImage.Point>;
|
|
108
|
+
readonly color: AbstractImage.Color;
|
|
109
|
+
readonly thickness: number;
|
|
110
|
+
readonly label: string;
|
|
111
|
+
readonly xAxis: XAxis;
|
|
112
|
+
readonly yAxis: YAxis;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type ChartLineProps = Partial<ChartLine>;
|
|
116
|
+
|
|
117
|
+
export function createChartLine(props: ChartLineProps): ChartLine {
|
|
118
|
+
const {
|
|
119
|
+
points = [],
|
|
120
|
+
color = AbstractImage.black,
|
|
121
|
+
thickness = 1,
|
|
122
|
+
label = "",
|
|
123
|
+
xAxis = "bottom",
|
|
124
|
+
yAxis = "left",
|
|
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(props: ChartStackConfigProps): ChartStackConfig {
|
|
144
|
+
const { color = AbstractImage.black, label = "" } = props || {};
|
|
145
|
+
return {
|
|
146
|
+
color,
|
|
147
|
+
label,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface StackPoints {
|
|
152
|
+
readonly x: number;
|
|
153
|
+
readonly ys: ReadonlyArray<number>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface ChartStack {
|
|
157
|
+
readonly points: Array<StackPoints>;
|
|
158
|
+
readonly xAxis: XAxis;
|
|
159
|
+
readonly yAxis: YAxis;
|
|
160
|
+
readonly config: ReadonlyArray<ChartStackConfig>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type ChartStackProps = Partial<ChartStack>;
|
|
164
|
+
|
|
165
|
+
export function createChartStack(props: ChartStackProps): ChartStack {
|
|
166
|
+
const { points = [], xAxis = "bottom", yAxis = "left", config = [createChartStackConfig({})] } = props || {};
|
|
167
|
+
return {
|
|
168
|
+
points,
|
|
169
|
+
xAxis,
|
|
170
|
+
yAxis,
|
|
171
|
+
config,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const padding = 80;
|
|
176
|
+
|
|
177
|
+
export function inverseTransformPoint(
|
|
178
|
+
point: AbstractImage.Point,
|
|
179
|
+
chart: Chart,
|
|
180
|
+
xAxis: XAxis,
|
|
181
|
+
yAxis: YAxis
|
|
182
|
+
): AbstractImage.Point | undefined {
|
|
183
|
+
const xMin = padding;
|
|
184
|
+
const xMax = chart.width - padding;
|
|
185
|
+
const yMin = chart.height - 0.5 * padding;
|
|
186
|
+
const yMax = 0.5 * padding;
|
|
187
|
+
const x = Axis.inverseTransformValue(point.x, xMin, xMax, xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom);
|
|
188
|
+
const y = Axis.inverseTransformValue(point.y, yMin, yMax, yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft);
|
|
189
|
+
if (x === undefined || y === undefined) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
return AbstractImage.createPoint(x, y);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function renderChart(chart: Chart): AbstractImage.AbstractImage {
|
|
196
|
+
const { width, height, xAxisBottom, xAxisTop, yAxisLeft, yAxisRight } = chart;
|
|
197
|
+
|
|
198
|
+
const gridWidth = width - 2 * padding;
|
|
199
|
+
const gridHeight = height - padding;
|
|
200
|
+
|
|
201
|
+
const xMin = padding;
|
|
202
|
+
const xMax = width - padding;
|
|
203
|
+
const yMin = height - 0.5 * padding;
|
|
204
|
+
const yMax = 0.5 * padding;
|
|
205
|
+
|
|
206
|
+
const renderedBackground = generateBackground(xMin, xMax, yMin, yMax, chart);
|
|
207
|
+
|
|
208
|
+
const xNumTicks = gridWidth / 40;
|
|
209
|
+
const renderedXAxisBottom = generateXAxisBottom(xNumTicks, xAxisBottom, xMin, xMax, yMin, yMax, chart);
|
|
210
|
+
const renderedXAxisTop = generateXAxisTop(xNumTicks, xAxisTop, xMin, xMax, yMax, chart);
|
|
211
|
+
|
|
212
|
+
const yNumTicks = gridHeight / 40;
|
|
213
|
+
const renderedYAxisLeft = generateYAxisLeft(yNumTicks, yAxisLeft, xMin, xMax, yMin, yMax, chart);
|
|
214
|
+
const renderedYAxisRight = generateYAxisRight(yNumTicks, yAxisRight, xMax, yMin, yMax, chart);
|
|
215
|
+
|
|
216
|
+
const renderedPoints = generatePoints(xMin, xMax, yMin, yMax, chart);
|
|
217
|
+
const renderedLines = generateLines(xMin, xMax, yMin, yMax, chart);
|
|
218
|
+
const renderedStack = generateStack(xMin, xMax, yMin, yMax, chart);
|
|
219
|
+
|
|
220
|
+
const components = [
|
|
221
|
+
renderedBackground,
|
|
222
|
+
renderedXAxisBottom,
|
|
223
|
+
renderedXAxisTop,
|
|
224
|
+
renderedYAxisLeft,
|
|
225
|
+
renderedYAxisRight,
|
|
226
|
+
renderedStack,
|
|
227
|
+
renderedLines,
|
|
228
|
+
renderedPoints,
|
|
229
|
+
];
|
|
230
|
+
const topLeft = AbstractImage.createPoint(0, 0);
|
|
231
|
+
const size = AbstractImage.createSize(width, height);
|
|
232
|
+
return AbstractImage.createAbstractImage(topLeft, size, AbstractImage.white, components);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function generateBackground(
|
|
236
|
+
xMin: number,
|
|
237
|
+
xMax: number,
|
|
238
|
+
yMin: number,
|
|
239
|
+
yMax: number,
|
|
240
|
+
chart: Chart
|
|
241
|
+
): AbstractImage.Component {
|
|
242
|
+
const topLeft = AbstractImage.createPoint(xMin, yMax);
|
|
243
|
+
const bottomRight = AbstractImage.createPoint(xMax, yMin);
|
|
244
|
+
return AbstractImage.createRectangle(
|
|
245
|
+
topLeft,
|
|
246
|
+
bottomRight,
|
|
247
|
+
chart.gridColor,
|
|
248
|
+
chart.gridThickness,
|
|
249
|
+
chart.backgroundColor
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function generateXAxisBottom(
|
|
254
|
+
xNumTicks: number,
|
|
255
|
+
xAxisBottom: Axis.Axis | undefined,
|
|
256
|
+
xMin: number,
|
|
257
|
+
xMax: number,
|
|
258
|
+
yMin: number,
|
|
259
|
+
yMax: number,
|
|
260
|
+
chart: Chart
|
|
261
|
+
): AbstractImage.Component {
|
|
262
|
+
if (!xAxisBottom) {
|
|
263
|
+
return AbstractImage.createGroup("XAxisBottom", []);
|
|
264
|
+
}
|
|
265
|
+
const xTicks = Axis.getTicks(xNumTicks, xAxisBottom);
|
|
266
|
+
const xLines = generateXAxisGridLines(xMin, xMax, yMin + 10, yMax, xTicks, xAxisBottom, chart);
|
|
267
|
+
const xLabels = generateXAxisLabels(xMin, xMax, yMin + 10, "down", xTicks, xAxisBottom, chart);
|
|
268
|
+
|
|
269
|
+
let xLabel: AbstractImage.Component;
|
|
270
|
+
switch (chart.labelLayout) {
|
|
271
|
+
case "original":
|
|
272
|
+
xLabel = generateXAxisLabel(xMax + 0.5 * padding, yMin + 10, "uniform", "down", xAxisBottom.label, chart);
|
|
273
|
+
break;
|
|
274
|
+
|
|
275
|
+
case "end":
|
|
276
|
+
xLabel = generateXAxisLabel(xMax, yMin + 25, "left", "down", xAxisBottom.label, chart);
|
|
277
|
+
break;
|
|
278
|
+
|
|
279
|
+
case "center":
|
|
280
|
+
xLabel = generateXAxisLabel((xMin + xMax) / 2, yMin + 25, "uniform", "down", xAxisBottom.label, chart);
|
|
281
|
+
break;
|
|
282
|
+
|
|
283
|
+
default:
|
|
284
|
+
return exhaustiveCheck(chart.labelLayout);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return AbstractImage.createGroup("XAxisBottom", [xLines, xLabels, xLabel]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function generateXAxisTop(
|
|
291
|
+
xNumTicks: number,
|
|
292
|
+
xAxisTop: Axis.Axis | undefined,
|
|
293
|
+
xMin: number,
|
|
294
|
+
xMax: number,
|
|
295
|
+
yMax: number,
|
|
296
|
+
chart: Chart
|
|
297
|
+
): AbstractImage.Component {
|
|
298
|
+
if (!xAxisTop) {
|
|
299
|
+
return AbstractImage.createGroup("XAxisTop", []);
|
|
300
|
+
}
|
|
301
|
+
const xTicks2 = Axis.getTicks(xNumTicks, xAxisTop);
|
|
302
|
+
const xLines2 = generateXAxisGridLines(xMin, xMax, yMax - 10, yMax, xTicks2, xAxisTop, chart);
|
|
303
|
+
const xLabels2 = generateXAxisLabels(xMin, xMax, yMax - 13, "up", xTicks2, xAxisTop, chart);
|
|
304
|
+
|
|
305
|
+
let xLabel2: AbstractImage.Component;
|
|
306
|
+
switch (chart.labelLayout) {
|
|
307
|
+
case "original":
|
|
308
|
+
xLabel2 = generateXAxisLabel(xMax + 0.5 * padding, yMax - 13, "uniform", "up", xAxisTop.label, chart);
|
|
309
|
+
break;
|
|
310
|
+
|
|
311
|
+
case "end":
|
|
312
|
+
xLabel2 = generateXAxisLabel(xMax, yMax - 30, "left", "up", xAxisTop.label, chart);
|
|
313
|
+
break;
|
|
314
|
+
|
|
315
|
+
case "center":
|
|
316
|
+
xLabel2 = generateXAxisLabel((xMin + xMax) / 2, yMax - 30, "uniform", "up", xAxisTop.label, chart);
|
|
317
|
+
break;
|
|
318
|
+
|
|
319
|
+
default:
|
|
320
|
+
return exhaustiveCheck(chart.labelLayout);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return AbstractImage.createGroup("XAxisTop", [xLines2, xLabels2, xLabel2]);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function generateYAxisLeft(
|
|
327
|
+
yNumTicks: number,
|
|
328
|
+
yAxisLeft: Axis.Axis | undefined,
|
|
329
|
+
xMin: number,
|
|
330
|
+
xMax: number,
|
|
331
|
+
yMin: number,
|
|
332
|
+
yMax: number,
|
|
333
|
+
chart: Chart
|
|
334
|
+
): AbstractImage.Component {
|
|
335
|
+
if (!yAxisLeft) {
|
|
336
|
+
return AbstractImage.createGroup("YAxisLeft", []);
|
|
337
|
+
}
|
|
338
|
+
const yTicks = Axis.getTicks(yNumTicks, yAxisLeft);
|
|
339
|
+
const yLines = generateYAxisLines(xMin - 5, xMax, yMin, yMax, yTicks, yAxisLeft, chart);
|
|
340
|
+
const yLabels = generateYAxisLabels(xMin - 7, yMin, yMax, "left", yTicks, yAxisLeft, chart);
|
|
341
|
+
|
|
342
|
+
const labelPaddingLeft = 5 + labelPadding(formatNumber(yAxisLeft.max).length, chart.fontSize, 0.5);
|
|
343
|
+
|
|
344
|
+
let yLabel: AbstractImage.Component;
|
|
345
|
+
switch (chart.labelLayout) {
|
|
346
|
+
case "original":
|
|
347
|
+
yLabel = generateYAxisLabel(
|
|
348
|
+
xMin - labelPaddingLeft,
|
|
349
|
+
yMax + 0.5 * padding,
|
|
350
|
+
"uniform",
|
|
351
|
+
"up",
|
|
352
|
+
yAxisLeft.label,
|
|
353
|
+
chart
|
|
354
|
+
);
|
|
355
|
+
break;
|
|
356
|
+
|
|
357
|
+
case "end":
|
|
358
|
+
yLabel = generateYAxisLabel(xMin - labelPaddingLeft, yMax, "left", "up", yAxisLeft.label, chart);
|
|
359
|
+
break;
|
|
360
|
+
|
|
361
|
+
case "center":
|
|
362
|
+
yLabel = generateYAxisLabel(xMin - labelPaddingLeft, (yMin + yMax) / 2, "uniform", "up", yAxisLeft.label, chart);
|
|
363
|
+
break;
|
|
364
|
+
|
|
365
|
+
default:
|
|
366
|
+
return exhaustiveCheck(chart.labelLayout);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return AbstractImage.createGroup("YAxisLeft", [yLines, yLabels, yLabel]);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function generateYAxisRight(
|
|
373
|
+
yNumTicks: number,
|
|
374
|
+
yAxisRight: Axis.Axis | undefined,
|
|
375
|
+
xMax: number,
|
|
376
|
+
yMin: number,
|
|
377
|
+
yMax: number,
|
|
378
|
+
chart: Chart
|
|
379
|
+
): AbstractImage.Component {
|
|
380
|
+
if (!yAxisRight) {
|
|
381
|
+
return AbstractImage.createGroup("YAxisRight", []);
|
|
382
|
+
}
|
|
383
|
+
const yTicks2 = Axis.getTicks(yNumTicks, yAxisRight);
|
|
384
|
+
const yLines2 = generateYAxisLines(xMax - 5, xMax + 5, yMin, yMax, yTicks2, yAxisRight, chart);
|
|
385
|
+
const yLabels2 = generateYAxisLabels(xMax + 7, yMin, yMax, "right", yTicks2, yAxisRight, chart);
|
|
386
|
+
|
|
387
|
+
const labelPaddingRight = 7 + labelPadding(formatNumber(yAxisRight.max).length, chart.fontSize, 1.5);
|
|
388
|
+
|
|
389
|
+
let yLabel2: AbstractImage.Component;
|
|
390
|
+
switch (chart.labelLayout) {
|
|
391
|
+
case "original":
|
|
392
|
+
yLabel2 = generateYAxisLabel(
|
|
393
|
+
xMax + labelPaddingRight,
|
|
394
|
+
yMax + 0.5 * padding,
|
|
395
|
+
"uniform",
|
|
396
|
+
"up",
|
|
397
|
+
yAxisRight.label,
|
|
398
|
+
chart
|
|
399
|
+
);
|
|
400
|
+
break;
|
|
401
|
+
|
|
402
|
+
case "end":
|
|
403
|
+
yLabel2 = generateYAxisLabel(xMax + labelPaddingRight, yMax, "left", "up", yAxisRight.label, chart);
|
|
404
|
+
break;
|
|
405
|
+
|
|
406
|
+
case "center":
|
|
407
|
+
yLabel2 = generateYAxisLabel(
|
|
408
|
+
xMax + labelPaddingRight,
|
|
409
|
+
(yMin + yMax) / 2,
|
|
410
|
+
"uniform",
|
|
411
|
+
"up",
|
|
412
|
+
yAxisRight.label,
|
|
413
|
+
chart
|
|
414
|
+
);
|
|
415
|
+
break;
|
|
416
|
+
|
|
417
|
+
default:
|
|
418
|
+
return exhaustiveCheck(chart.labelLayout);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return AbstractImage.createGroup("YAxisRight", [yLines2, yLabels2, yLabel2]);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function generateStack(
|
|
425
|
+
xMin: number,
|
|
426
|
+
xMax: number,
|
|
427
|
+
yMin: number,
|
|
428
|
+
yMax: number,
|
|
429
|
+
chart: Chart
|
|
430
|
+
): AbstractImage.Component {
|
|
431
|
+
const pointsPos = chart.chartStack.points.map((stackPoint) => ({
|
|
432
|
+
x: stackPoint.x,
|
|
433
|
+
ys: [...stackPoint.ys.map((y) => Math.min(0, y))],
|
|
434
|
+
}));
|
|
435
|
+
|
|
436
|
+
const stackPos = generateUnsignedStack(xMin, xMax, yMin, yMax, {
|
|
437
|
+
...chart,
|
|
438
|
+
chartStack: { ...chart.chartStack, points: pointsPos },
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const pointsNeg = chart.chartStack.points.map((stackPoint) => ({
|
|
442
|
+
x: stackPoint.x,
|
|
443
|
+
ys: [...stackPoint.ys.map((y) => Math.max(0, y))],
|
|
444
|
+
}));
|
|
445
|
+
|
|
446
|
+
const stackNeg = generateUnsignedStack(xMin, xMax, yMin, yMax, {
|
|
447
|
+
...chart,
|
|
448
|
+
chartStack: { ...chart.chartStack, points: pointsNeg },
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
return AbstractImage.createGroup("Stacks", [stackPos, stackNeg]);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function generateUnsignedStack(
|
|
455
|
+
xMin: number,
|
|
456
|
+
xMax: number,
|
|
457
|
+
yMin: number,
|
|
458
|
+
yMax: number,
|
|
459
|
+
chart: Chart
|
|
460
|
+
): AbstractImage.Component {
|
|
461
|
+
if (chart.chartStack.points.length < 2) {
|
|
462
|
+
return AbstractImage.createGroup("stack", []);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const xAxis = chart.chartStack.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
|
|
466
|
+
const yAxis = chart.chartStack.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
|
|
467
|
+
|
|
468
|
+
const xPoints = chart.chartStack.points.map((stackPoints) => {
|
|
469
|
+
let sumY = 0;
|
|
470
|
+
const points = stackPoints.ys.map((y) => {
|
|
471
|
+
sumY += y;
|
|
472
|
+
return Axis.transformPoint(AbstractImage.createPoint(stackPoints.x, sumY), xMin, xMax, yMin, yMax, xAxis, yAxis);
|
|
473
|
+
});
|
|
474
|
+
return points;
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Transpose the xPoints data to lines.
|
|
478
|
+
const lines: Array<Array<AbstractImage.Point>> = [];
|
|
479
|
+
for (let i = 0; i < xPoints[0].length; ++i) {
|
|
480
|
+
lines[i] = [];
|
|
481
|
+
for (const points of xPoints) {
|
|
482
|
+
lines[i].push(points[i]);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const polygons: Array<AbstractImage.Polygon> = [];
|
|
487
|
+
let lastLine = chart.chartStack.points.map((stackPoint) =>
|
|
488
|
+
Axis.transformPoint(AbstractImage.createPoint(stackPoint.x, 0), xMin, xMax, yMin, yMax, xAxis, yAxis)
|
|
489
|
+
);
|
|
490
|
+
lines.forEach((line, index) => {
|
|
491
|
+
const config = chart.chartStack.config[index];
|
|
492
|
+
if (!config) {
|
|
493
|
+
throw new Error("Missing config for series " + index);
|
|
494
|
+
}
|
|
495
|
+
const color = config.color;
|
|
496
|
+
const points = [...line, ...lastLine.slice().reverse()];
|
|
497
|
+
lastLine = line;
|
|
498
|
+
polygons.push(AbstractImage.createPolygon(points, color, 0, color));
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
return AbstractImage.createGroup("Stack", polygons);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function generateLines(
|
|
505
|
+
xMin: number,
|
|
506
|
+
xMax: number,
|
|
507
|
+
yMin: number,
|
|
508
|
+
yMax: number,
|
|
509
|
+
chart: Chart
|
|
510
|
+
): AbstractImage.Component {
|
|
511
|
+
const lines = chart.chartLines.map((l: ChartLine) => {
|
|
512
|
+
if (l.points.length < 2) {
|
|
513
|
+
return AbstractImage.createGroup(l.label, []);
|
|
514
|
+
}
|
|
515
|
+
const xAxis = l.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
|
|
516
|
+
const yAxis = l.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
|
|
517
|
+
const points = l.points.map((p) => Axis.transformPoint(p, xMin, xMax, yMin, yMax, xAxis, yAxis));
|
|
518
|
+
const last = points[points.length - 1];
|
|
519
|
+
return AbstractImage.createGroup(l.label, [
|
|
520
|
+
AbstractImage.createPolyLine(points, l.color, l.thickness),
|
|
521
|
+
AbstractImage.createText(
|
|
522
|
+
last,
|
|
523
|
+
l.label,
|
|
524
|
+
chart.font,
|
|
525
|
+
chart.fontSize,
|
|
526
|
+
AbstractImage.black,
|
|
527
|
+
"normal",
|
|
528
|
+
0,
|
|
529
|
+
"center",
|
|
530
|
+
"right",
|
|
531
|
+
"down",
|
|
532
|
+
0,
|
|
533
|
+
AbstractImage.black,
|
|
534
|
+
false
|
|
535
|
+
),
|
|
536
|
+
]);
|
|
537
|
+
});
|
|
538
|
+
return AbstractImage.createGroup("Lines", lines);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function generatePoints(
|
|
542
|
+
xMin: number,
|
|
543
|
+
xMax: number,
|
|
544
|
+
yMin: number,
|
|
545
|
+
yMax: number,
|
|
546
|
+
chart: Chart
|
|
547
|
+
): AbstractImage.Component {
|
|
548
|
+
const points = chart.chartPoints.map((p) => {
|
|
549
|
+
const xAxis = p.xAxis === "top" ? chart.xAxisTop : chart.xAxisBottom;
|
|
550
|
+
const yAxis = p.yAxis === "right" ? chart.yAxisRight : chart.yAxisLeft;
|
|
551
|
+
const position = Axis.transformPoint(p.position, xMin, xMax, yMin, yMax, xAxis, yAxis);
|
|
552
|
+
const shape = generatePointShape(p, position);
|
|
553
|
+
return AbstractImage.createGroup(p.label, [
|
|
554
|
+
shape,
|
|
555
|
+
AbstractImage.createText(
|
|
556
|
+
position,
|
|
557
|
+
p.label,
|
|
558
|
+
chart.font,
|
|
559
|
+
chart.fontSize,
|
|
560
|
+
AbstractImage.black,
|
|
561
|
+
"normal",
|
|
562
|
+
0,
|
|
563
|
+
"center",
|
|
564
|
+
"right",
|
|
565
|
+
"down",
|
|
566
|
+
0,
|
|
567
|
+
AbstractImage.black,
|
|
568
|
+
false
|
|
569
|
+
),
|
|
570
|
+
]);
|
|
571
|
+
});
|
|
572
|
+
return AbstractImage.createGroup("Points", points);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function generatePointShape(p: ChartPoint, position: AbstractImage.Point): AbstractImage.Component {
|
|
576
|
+
const halfWidth = p.size.width * 0.5;
|
|
577
|
+
const halfHeight = p.size.height * 0.5;
|
|
578
|
+
if (p.shape === "triangle") {
|
|
579
|
+
const trianglePoints = [
|
|
580
|
+
AbstractImage.createPoint(position.x, position.y + halfHeight),
|
|
581
|
+
AbstractImage.createPoint(position.x - halfWidth, position.y - halfHeight),
|
|
582
|
+
AbstractImage.createPoint(position.x + halfWidth, position.y - halfHeight),
|
|
583
|
+
];
|
|
584
|
+
return AbstractImage.createPolygon(trianglePoints, AbstractImage.black, 1, p.color);
|
|
585
|
+
} else if (p.shape === "square") {
|
|
586
|
+
const topLeft = AbstractImage.createPoint(position.x - halfWidth, position.y - halfHeight);
|
|
587
|
+
const bottomRight = AbstractImage.createPoint(position.x + halfWidth, position.y + halfHeight);
|
|
588
|
+
return AbstractImage.createRectangle(topLeft, bottomRight, AbstractImage.black, 1, p.color);
|
|
589
|
+
} else {
|
|
590
|
+
const topLeft = AbstractImage.createPoint(position.x - halfWidth, position.y - halfHeight);
|
|
591
|
+
const bottomRight = AbstractImage.createPoint(position.x + halfWidth, position.y + halfHeight);
|
|
592
|
+
return AbstractImage.createEllipse(topLeft, bottomRight, AbstractImage.black, 1, p.color);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export function generateXAxisGridLines(
|
|
597
|
+
xMin: number,
|
|
598
|
+
xMax: number,
|
|
599
|
+
yMin: number,
|
|
600
|
+
yMax: number,
|
|
601
|
+
xTicks: Array<number>,
|
|
602
|
+
xAxis: Axis.Axis,
|
|
603
|
+
chart: Chart
|
|
604
|
+
): AbstractImage.Component {
|
|
605
|
+
const xLines = xTicks.map((l) => {
|
|
606
|
+
const x = Axis.transformValue(l, xMin, xMax, xAxis);
|
|
607
|
+
const start = AbstractImage.createPoint(x, yMin);
|
|
608
|
+
const end = AbstractImage.createPoint(x, yMax);
|
|
609
|
+
return AbstractImage.createLine(start, end, chart.gridColor, chart.gridThickness);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
return AbstractImage.createGroup("Lines", xLines);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export function generateXAxisLabels(
|
|
616
|
+
xMin: number,
|
|
617
|
+
xMax: number,
|
|
618
|
+
y: number,
|
|
619
|
+
growVertical: AbstractImage.GrowthDirection,
|
|
620
|
+
xTicks: Array<number>,
|
|
621
|
+
xAxis: Axis.Axis,
|
|
622
|
+
chart: Chart
|
|
623
|
+
): AbstractImage.Component {
|
|
624
|
+
const xLabels = xTicks.map((l) => {
|
|
625
|
+
const position = AbstractImage.createPoint(Axis.transformValue(l, xMin, xMax, xAxis), y);
|
|
626
|
+
return AbstractImage.createText(
|
|
627
|
+
position,
|
|
628
|
+
formatNumber(l),
|
|
629
|
+
chart.font,
|
|
630
|
+
chart.fontSize,
|
|
631
|
+
AbstractImage.black,
|
|
632
|
+
"normal",
|
|
633
|
+
0,
|
|
634
|
+
"center",
|
|
635
|
+
"uniform",
|
|
636
|
+
growVertical,
|
|
637
|
+
0,
|
|
638
|
+
AbstractImage.black,
|
|
639
|
+
false
|
|
640
|
+
);
|
|
641
|
+
});
|
|
642
|
+
return AbstractImage.createGroup("Labels", xLabels);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export function generateXAxisLabel(
|
|
646
|
+
x: number,
|
|
647
|
+
y: number,
|
|
648
|
+
horizontalGrowthDirection: AbstractImage.GrowthDirection,
|
|
649
|
+
verticalGrowthDirection: AbstractImage.GrowthDirection,
|
|
650
|
+
label: string,
|
|
651
|
+
chart: Chart
|
|
652
|
+
): AbstractImage.Component {
|
|
653
|
+
const position = AbstractImage.createPoint(x, y);
|
|
654
|
+
return AbstractImage.createText(
|
|
655
|
+
position,
|
|
656
|
+
label,
|
|
657
|
+
chart.font,
|
|
658
|
+
chart.fontSize,
|
|
659
|
+
AbstractImage.black,
|
|
660
|
+
"normal",
|
|
661
|
+
0,
|
|
662
|
+
"center",
|
|
663
|
+
horizontalGrowthDirection,
|
|
664
|
+
verticalGrowthDirection,
|
|
665
|
+
0,
|
|
666
|
+
AbstractImage.black,
|
|
667
|
+
false
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function generateYAxisLines(
|
|
672
|
+
xMin: number,
|
|
673
|
+
xMax: number,
|
|
674
|
+
yMin: number,
|
|
675
|
+
yMax: number,
|
|
676
|
+
yTicks: Array<number>,
|
|
677
|
+
yAxis: Axis.Axis,
|
|
678
|
+
chart: Chart
|
|
679
|
+
): AbstractImage.Component {
|
|
680
|
+
const yLines = yTicks.map((l) => {
|
|
681
|
+
const y = Axis.transformValue(l, yMin, yMax, yAxis);
|
|
682
|
+
const start = AbstractImage.createPoint(xMin, y);
|
|
683
|
+
const end = AbstractImage.createPoint(xMax, y);
|
|
684
|
+
return AbstractImage.createLine(start, end, chart.gridColor, chart.gridThickness);
|
|
685
|
+
});
|
|
686
|
+
return AbstractImage.createGroup("Lines", yLines);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export function generateYAxisLabels(
|
|
690
|
+
x: number,
|
|
691
|
+
yMin: number,
|
|
692
|
+
yMax: number,
|
|
693
|
+
growHorizontal: AbstractImage.GrowthDirection,
|
|
694
|
+
yTicks: Array<number>,
|
|
695
|
+
yAxis: Axis.Axis,
|
|
696
|
+
chart: Chart
|
|
697
|
+
): AbstractImage.Component {
|
|
698
|
+
const yLabels = yTicks.map((l) => {
|
|
699
|
+
const position = AbstractImage.createPoint(x, Axis.transformValue(l, yMin, yMax, yAxis));
|
|
700
|
+
return AbstractImage.createText(
|
|
701
|
+
position,
|
|
702
|
+
formatNumber(l),
|
|
703
|
+
chart.font,
|
|
704
|
+
chart.fontSize,
|
|
705
|
+
AbstractImage.black,
|
|
706
|
+
"normal",
|
|
707
|
+
0,
|
|
708
|
+
"center",
|
|
709
|
+
growHorizontal,
|
|
710
|
+
"uniform",
|
|
711
|
+
0,
|
|
712
|
+
AbstractImage.black,
|
|
713
|
+
false
|
|
714
|
+
);
|
|
715
|
+
});
|
|
716
|
+
return AbstractImage.createGroup("Labels", yLabels);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export function generateYAxisLabel(
|
|
720
|
+
x: number,
|
|
721
|
+
y: number,
|
|
722
|
+
horizontalGrowthDirection: AbstractImage.GrowthDirection,
|
|
723
|
+
verticalGrowthDirection: AbstractImage.GrowthDirection,
|
|
724
|
+
label: string,
|
|
725
|
+
chart: Chart
|
|
726
|
+
): AbstractImage.Component {
|
|
727
|
+
const position = AbstractImage.createPoint(x, y);
|
|
728
|
+
return AbstractImage.createText(
|
|
729
|
+
position,
|
|
730
|
+
label,
|
|
731
|
+
chart.font,
|
|
732
|
+
chart.fontSize,
|
|
733
|
+
AbstractImage.black,
|
|
734
|
+
"normal",
|
|
735
|
+
-90,
|
|
736
|
+
"center",
|
|
737
|
+
horizontalGrowthDirection,
|
|
738
|
+
verticalGrowthDirection,
|
|
739
|
+
0,
|
|
740
|
+
AbstractImage.black,
|
|
741
|
+
false
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function formatNumber(n: number): string {
|
|
746
|
+
if (n >= 10000000) {
|
|
747
|
+
return numberToString(n / 1000000) + "m";
|
|
748
|
+
}
|
|
749
|
+
if (n >= 10000) {
|
|
750
|
+
return numberToString(n / 1000) + "k";
|
|
751
|
+
}
|
|
752
|
+
return numberToString(n);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function numberToString(n: number): string {
|
|
756
|
+
return parseFloat(n.toPrecision(5)).toString();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function labelPadding(numberOfCharacters: number, fontSize: number, characterOffset: number): number {
|
|
760
|
+
return ((numberOfCharacters + 1 + characterOffset) * fontSize * 3) / 4;
|
|
761
|
+
}
|