forked from chartjs/Chart.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
scale.timeseries.js
158 lines (138 loc) · 4.5 KB
/
scale.timeseries.js
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
import TimeScale from './scale.time.js';
import {_lookupByKey} from '../helpers/helpers.collection.js';
/**
* Linearly interpolates the given source `val` using the table. If value is out of bounds, values
* at edges are used for the interpolation.
* @param {object} table
* @param {number} val
* @param {boolean} [reverse] lookup time based on position instead of vice versa
* @return {object}
*/
function interpolate(table, val, reverse) {
let lo = 0;
let hi = table.length - 1;
let prevSource, nextSource, prevTarget, nextTarget;
if (reverse) {
if (val >= table[lo].pos && val <= table[hi].pos) {
({lo, hi} = _lookupByKey(table, 'pos', val));
}
({pos: prevSource, time: prevTarget} = table[lo]);
({pos: nextSource, time: nextTarget} = table[hi]);
} else {
if (val >= table[lo].time && val <= table[hi].time) {
({lo, hi} = _lookupByKey(table, 'time', val));
}
({time: prevSource, pos: prevTarget} = table[lo]);
({time: nextSource, pos: nextTarget} = table[hi]);
}
const span = nextSource - prevSource;
return span ? prevTarget + (nextTarget - prevTarget) * (val - prevSource) / span : prevTarget;
}
class TimeSeriesScale extends TimeScale {
static id = 'timeseries';
/**
* @type {any}
*/
static defaults = TimeScale.defaults;
/**
* @param {object} props
*/
constructor(props) {
super(props);
/** @type {object[]} */
this._table = [];
/** @type {number} */
this._minPos = undefined;
/** @type {number} */
this._tableRange = undefined;
}
/**
* @protected
*/
initOffsets() {
const timestamps = this._getTimestampsForTable();
const table = this._table = this.buildLookupTable(timestamps);
this._minPos = interpolate(table, this.min);
this._tableRange = interpolate(table, this.max) - this._minPos;
super.initOffsets(timestamps);
}
/**
* Returns an array of {time, pos} objects used to interpolate a specific `time` or position
* (`pos`) on the scale, by searching entries before and after the requested value. `pos` is
* a decimal between 0 and 1: 0 being the start of the scale (left or top) and 1 the other
* extremity (left + width or top + height). Note that it would be more optimized to directly
* store pre-computed pixels, but the scale dimensions are not guaranteed at the time we need
* to create the lookup table. The table ALWAYS contains at least two items: min and max.
* @param {number[]} timestamps
* @return {object[]}
* @protected
*/
buildLookupTable(timestamps) {
const {min, max} = this;
const items = [];
const table = [];
let i, ilen, prev, curr, next;
for (i = 0, ilen = timestamps.length; i < ilen; ++i) {
curr = timestamps[i];
if (curr >= min && curr <= max) {
items.push(curr);
}
}
if (items.length < 2) {
// In case there is less that 2 timestamps between min and max, the scale is defined by min and max
return [
{time: min, pos: 0},
{time: max, pos: 1}
];
}
for (i = 0, ilen = items.length; i < ilen; ++i) {
next = items[i + 1];
prev = items[i - 1];
curr = items[i];
// only add points that breaks the scale linearity
if (Math.round((next + prev) / 2) !== curr) {
table.push({time: curr, pos: i / (ilen - 1)});
}
}
return table;
}
/**
* Returns all timestamps
* @return {number[]}
* @private
*/
_getTimestampsForTable() {
let timestamps = this._cache.all || [];
if (timestamps.length) {
return timestamps;
}
const data = this.getDataTimestamps();
const label = this.getLabelTimestamps();
if (data.length && label.length) {
// If combining labels and data (data might not contain all labels),
// we need to recheck uniqueness and sort
timestamps = this.normalize(data.concat(label));
} else {
timestamps = data.length ? data : label;
}
timestamps = this._cache.all = timestamps;
return timestamps;
}
/**
* @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC)
* @return {number}
*/
getDecimalForValue(value) {
return (interpolate(this._table, value) - this._minPos) / this._tableRange;
}
/**
* @param {number} pixel
* @return {number}
*/
getValueForPixel(pixel) {
const offsets = this._offsets;
const decimal = this.getDecimalForPixel(pixel) / offsets.factor - offsets.end;
return interpolate(this._table, decimal * this._tableRange + this._minPos, true);
}
}
export default TimeSeriesScale;