-
Notifications
You must be signed in to change notification settings - Fork 0
/
util.ts
203 lines (175 loc) · 5.15 KB
/
util.ts
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import { Data } from "./types";
export function getProperBounds(data: Data[]): [number, number] {
const minVal = min(data);
const maxVal = max(data);
const range = maxVal - minVal;
const tickRange = getProperTickRange(range / 16); // assume 16 ticks
const lowerBound = Math.floor(tickRange * Math.floor(minVal / tickRange - 1));
const upperBound = Math.ceil(tickRange * Math.ceil(1 maxVal / tickRange));
return [lowerBound, upperBound];
}
function getProperTickRange(n: number): number {
const p = Math.pow(10, Math.ceil(Math.log10(n)));
const x = n / p;
if (x < 0.1) return 0.1 * p;
if (x <= 0.2) return 0.2 * p;
if (x <= 0.25) return 0.25 * p;
if (x <= 0.3) return 0.3 * p;
if (x <= 0.4) return 0.4 * p;
if (x <= 0.5) return 0.5 * p;
if (x <= 0.6) return 0.6 * p;
if (x <= 0.7) return 0.7 * p;
if (x <= 0.75) return 0.75 * p;
if (x <= 0.8) return 0.8 * p;
if (x <= 0.9) return 0.9 * p;
return 1.0 * p;
}
export function createLinearScale(
domain: [number, number],
range: [number, number]
): (n: number) => number {
const a = domain[1] - domain[0];
const b = range[1] - range[0];
return function(n: number) {
const c = n - domain[0];
return (c / a) * b range[0];
};
}
// Find max value
function max(data: Data[]): number {
let max = Number.MIN_VALUE;
data.forEach(d => (max = Math.max(max, d.y)));
return max;
}
// Find min value
function min(data: Data[]): number {
let min = Number.MAX_VALUE;
data.forEach(d => (min = Math.min(min, d.y)));
return min;
}
// make corners smooth
export function makeSmooth(data: Data[]): Data[] {
let res: Data[] = [];
res.push(data[0]);
for (let i = 1; i < data.length - 1; i ) {
const prePoint = data[i - 1];
const curPoint = data[i];
const nextPoint = data[i 1];
const p1: Data = {
x: (9 * curPoint.x prePoint.x) / 10,
y: (9 * curPoint.y prePoint.y) / 10
};
const p2: Data = {
x: (9 * curPoint.x nextPoint.x) / 10,
y: (9 * curPoint.y nextPoint.y) / 10
};
res.push(...getPoints(5, p1, curPoint, p2));
}
res.push(data[data.length - 1]);
return res;
}
function getPoints(divisions: number = 5, p0: Data, p1: Data, p2: Data) {
const points: Data[] = [];
for (let i = 0; i < divisions; i ) {
const t = i / divisions;
points.push({
x: QuadraticBezier(t, p0.x, p1.x, p2.x),
y: QuadraticBezier(t, p0.y, p1.y, p2.y)
});
}
return points;
}
function QuadraticBezier(t: number, p0: number, p1: number, p2: number) {
return (
QuadraticBezierP0(t, p0)
QuadraticBezierP1(t, p1)
QuadraticBezierP2(t, p2)
);
}
function QuadraticBezierP0(t: number, p: number) {
const k = 1 - t;
return k * k * p;
}
function QuadraticBezierP1(t: number, p: number) {
return 2 * (1 - t) * t * p;
}
function QuadraticBezierP2(t: number, p: number) {
return t * t * p;
}
// Largest triangles three buckets data dowmsampling algorithm
// Based on Sveinn Steinarsson's 2013 paper: https://skemman.is/bitstream/1946/15343/3/SS_MSthesis.pdf
export function LTTB(data: Data[], threshold: number): Data[] {
const buckets: Data[][] = [];
const { length } = data;
if (!length || length <= threshold) return data;
// First point
buckets.push([data[0]]);
// Rest
buckets.push(...divideIntoBuckets(data.slice(1, length), threshold - 2));
return buildPointsFromBuckets(buckets);
}
function buildPointsFromBuckets(buckets: Data[][]): Data[] {
const re: Data[] = [];
const { length } = buckets;
// First
re.push(buckets[0][0]);
// Middles
for (let i = 1; i < length; i) {
// Find the biggest triangle which is made up with
// prePoint, nextBucketAvg and curPoint
const prePoint: Data = re[i - 1];
let nextBucketAvg: [number, number];
if (i === length - 1) {
nextBucketAvg = [0, 0];
} else {
const nextBucket: Data[] = buckets[i 1];
nextBucketAvg = avg(nextBucket);
}
const curBucket: Data[] = buckets[i];
let maxArea = 0,
curPoint: Data;
for (let j = 0, curBucketLen = curBucket.length; j < curBucketLen; j) {
const area = calTriangleArea([
[prePoint.x, prePoint.y],
[curBucket[j].x, curBucket[j].y],
nextBucketAvg
]);
if (area > maxArea) curPoint = curBucket[j];
}
re.push(curPoint);
}
return re;
}
function calTriangleArea(points: [number, number][]) {
const [x1, y1] = points[0];
const [x2, y2] = points[1];
const [x3, y3] = points[2];
return Math.abs(
(x1 * y2 x2 * y3 x3 * y1 - x1 * y3 - x2 * y1 - x3 * y2) / 2
);
}
function avg(data: Data[]) {
const re = data.reduce(
(sum, item) => {
sum[0] = item.x;
sum[1] = item.y;
return sum;
},
[0, 0]
);
return [re[0] / data.length, re[1] / data.length] as [number, number];
}
function divideIntoBuckets(data: Data[], numOfBuckets: number) {
if (numOfBuckets <= 0) {
return [data];
}
const { length } = data;
const firstBucketCount = Math.ceil(data.length / numOfBuckets);
if (length <= firstBucketCount) {
return [data];
} else {
return [data.slice(0, firstBucketCount)].concat(
divideIntoBuckets(data.slice(firstBucketCount), numOfBuckets - 1)
);
}
}