Repository navigation
Expand file tree
/
Copy pathchart_series.ts
More file actions
102 lines (93 loc) · 3.02 KB
/
Copy pathchart_series.ts
File metadata and controls
102 lines (93 loc) · 3.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
export type PlotValue = string | number | null;
export type XValue = PlotValue | Date;
export type ChartPoint = {
x: XValue;
y: PlotValue | PlotValue[];
z?: PlotValue;
fillColor?: string;
link?: string;
};
export type ChartSeries = { name: string | number; data: ChartPoint[] };
export type Series = Map<ChartSeries["name"], ChartSeries>;
const NUMERIC_X_CHART_TYPES = ["line", "area", "bar", "scatter", "bubble"];
const Y_WHEN_A_SERIES_SKIPS_A_LABEL = new Map<string, number | null>([
["bar", 0],
["line", null],
["area", null],
["scatter", null],
["bubble", null],
["heatmap", null],
]);
/** equal x values share a key */
const x_key = (x: XValue): PlotValue => (x instanceof Date ? x.getTime() : x);
/** A missing x sorts as zero, which is how JavaScript compares it. */
const is_lower = (x: XValue, than: XValue) => (x ?? 0) < (than ?? 0);
const x_is_text = (series: ChartSeries[]) =>
typeof series[0]?.data?.[0]?.x === "string";
export function xaxis_type_for(
series: ChartSeries[],
chart_type: string,
is_timeseries: boolean,
is_horizontal: boolean,
) {
if (is_timeseries) return "datetime";
if (x_is_text(series)) return "category";
if (
typeof series[0]?.data?.[0]?.x === "number" &&
!is_horizontal &&
NUMERIC_X_CHART_TYPES.includes(chart_type)
)
return "numeric";
return undefined;
}
/**
* @returns every x the series hold, in their own order where they agree and in
* ascending order where they diverge
*/
export function merged_x_values(series: ChartSeries[]): XValue[] {
const unread = series.map(({ data }) => data.map(({ x }) => x));
const merged = new Map<PlotValue, XValue>();
while (unread.some((xs) => xs.length > 0)) {
const with_lowest_x = unread
.filter((xs) => xs.length > 0)
.reduce((a, b) => (is_lower(b[0], a[0]) ? b : a));
const x = with_lowest_x.shift() as XValue;
merged.set(x_key(x), x);
}
return [...merged.values()];
}
/**
* ApexCharts pairs points across series by index rather than by x, so a
* series that skips an x lands on the wrong one. Give every series the same
* amount of x values.
*
* @param y_when_missing what a series with no value at an x is worth there:
* zero to add nothing to a stack, null to leave a gap.
*/
export function align_series(
series: ChartSeries[],
y_when_missing: number | null,
): ChartSeries[] {
const all_x = merged_x_values(series);
return series.map(({ name, data }) => {
const by_x = new Map(data.map((point) => [x_key(point.x), point]));
return {
name,
data: all_x.map((x) => {
const point = by_x.get(x_key(x));
return { ...point, x, y: point?.y ?? y_when_missing };
}),
};
});
}
export function align_series_for(
series: ChartSeries[],
chart_type: string,
is_stacked: boolean,
): ChartSeries[] {
if (is_stacked) return align_series(series, 0);
const y_when_missing = Y_WHEN_A_SERIES_SKIPS_A_LABEL.get(chart_type);
if (x_is_text(series) && y_when_missing !== undefined)
return align_series(series, y_when_missing);
return series;
}