@windborne/grapher 1.0.0
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/.eslintrc.js +85 -0
- package/.idea/codeStyles/Project.xml +19 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/grapher.iml +12 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/misc.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/0.bundle.js +2 -0
- package/0.bundle.js.map +1 -0
- package/1767282193a714f63082.module.wasm +0 -0
- package/537.bundle.js +2 -0
- package/537.bundle.js.map +1 -0
- package/831.bundle.js +2 -0
- package/831.bundle.js.map +1 -0
- package/bundle.js +2 -0
- package/bundle.js.map +1 -0
- package/package.json +75 -0
- package/readme.md +129 -0
- package/src/components/annotations.js +62 -0
- package/src/components/context_menu.js +73 -0
- package/src/components/draggable_points.js +114 -0
- package/src/components/graph_body.js +292 -0
- package/src/components/graph_title.js +16 -0
- package/src/components/options.js +111 -0
- package/src/components/percentile_button.js +72 -0
- package/src/components/range_graph.js +352 -0
- package/src/components/range_selection.js +175 -0
- package/src/components/range_selection_button.js +26 -0
- package/src/components/range_selection_button_base.js +51 -0
- package/src/components/series_key.js +235 -0
- package/src/components/series_key_axis_container.js +70 -0
- package/src/components/series_key_item.js +52 -0
- package/src/components/sidebar.js +76 -0
- package/src/components/tooltip.js +244 -0
- package/src/components/vertical_lines.js +70 -0
- package/src/components/x_axis.js +124 -0
- package/src/components/y_axis.js +239 -0
- package/src/eventable.js +65 -0
- package/src/grapher.js +367 -0
- package/src/grapher.scss +914 -0
- package/src/helpers/axis_sizes.js +2 -0
- package/src/helpers/binary_search.js +67 -0
- package/src/helpers/color_to_vector.js +35 -0
- package/src/helpers/colors.js +27 -0
- package/src/helpers/custom_prop_types.js +159 -0
- package/src/helpers/flatten_simple_data.js +81 -0
- package/src/helpers/format.js +233 -0
- package/src/helpers/generator_params_equal.js +10 -0
- package/src/helpers/name_for_series.js +16 -0
- package/src/helpers/place_grid.js +257 -0
- package/src/helpers/pyodide_ready.js +13 -0
- package/src/multigrapher.js +105 -0
- package/src/renderer/background.frag +7 -0
- package/src/renderer/background.vert +7 -0
- package/src/renderer/background_program.js +48 -0
- package/src/renderer/circle.frag +26 -0
- package/src/renderer/circle.vert +12 -0
- package/src/renderer/create_gl_program.js +36 -0
- package/src/renderer/draw_area.js +159 -0
- package/src/renderer/draw_background.js +15 -0
- package/src/renderer/draw_bars.js +80 -0
- package/src/renderer/draw_line.js +69 -0
- package/src/renderer/draw_zero_line.js +24 -0
- package/src/renderer/extract_vertices.js +137 -0
- package/src/renderer/graph_body_renderer.js +293 -0
- package/src/renderer/line.frag +51 -0
- package/src/renderer/line.vert +32 -0
- package/src/renderer/line_program.js +125 -0
- package/src/renderer/paths_from.js +72 -0
- package/src/renderer/scale_bounds.js +28 -0
- package/src/renderer/size_canvas.js +59 -0
- package/src/rust/Cargo.lock +233 -0
- package/src/rust/Cargo.toml +35 -0
- package/src/rust/pkg/grapher_rs.d.ts +42 -0
- package/src/rust/pkg/grapher_rs.js +351 -0
- package/src/rust/pkg/grapher_rs_bg.d.ts +11 -0
- package/src/rust/pkg/grapher_rs_bg.wasm +0 -0
- package/src/rust/pkg/index.js +342 -0
- package/src/rust/pkg/index_bg.wasm +0 -0
- package/src/rust/pkg/package.json +14 -0
- package/src/rust/src/extract_vertices.rs +83 -0
- package/src/rust/src/get_point_number.rs +50 -0
- package/src/rust/src/lib.rs +15 -0
- package/src/rust/src/selected_space_to_render_space.rs +131 -0
- package/src/state/average_loop_times.js +15 -0
- package/src/state/bound_calculator_from_selection.js +36 -0
- package/src/state/bound_calculators.js +41 -0
- package/src/state/calculate_annotations_state.js +59 -0
- package/src/state/calculate_data_bounds.js +104 -0
- package/src/state/calculate_tooltip_state.js +241 -0
- package/src/state/data_types.js +13 -0
- package/src/state/expand_bounds.js +58 -0
- package/src/state/find_matching_axis.js +31 -0
- package/src/state/get_default_bounds_calculator.js +15 -0
- package/src/state/hooks.js +164 -0
- package/src/state/infer_type.js +74 -0
- package/src/state/merge_bounds.js +64 -0
- package/src/state/multigraph_state_controller.js +334 -0
- package/src/state/selection_from_global_bounds.js +25 -0
- package/src/state/space_conversions/condense_data_space.js +115 -0
- package/src/state/space_conversions/data_space_to_selected_space.js +328 -0
- package/src/state/space_conversions/selected_space_to_background_space.js +144 -0
- package/src/state/space_conversions/selected_space_to_render_space.js +161 -0
- package/src/state/space_conversions/simple_series_to_data_space.js +229 -0
- package/src/state/state_controller.js +1770 -0
- package/src/state/sync_pool.js +101 -0
- package/test/setup.js +15 -0
- package/test/space_conversions/data_space_to_selected_space.test.js +434 -0
- package/webpack.dev.config.js +109 -0
- package/webpack.prod.config.js +60 -0
- package/webpack.test.config.js +59 -0
@@ -0,0 +1,334 @@
|
|
1
|
+
import Eventable from '../eventable.js';
|
2
|
+
import getColor from '../helpers/colors.js';
|
3
|
+
import nameForSeries from '../helpers/name_for_series.js';
|
4
|
+
|
5
|
+
export default class MultigraphStateController extends Eventable {
|
6
|
+
|
7
|
+
constructor({ id }) {
|
8
|
+
super();
|
9
|
+
|
10
|
+
this._id = id;
|
11
|
+
|
12
|
+
this._multiSeries = [];
|
13
|
+
this._seriesToGraphIndices = new Map();
|
14
|
+
this._graphIndicesToSeries = new Map();
|
15
|
+
this._modifiedSeries = new Map();
|
16
|
+
this._originalSeriesByMultigrapherIndex = new Map();
|
17
|
+
this._stateControllers = new Set();
|
18
|
+
this._prevSeries = [];
|
19
|
+
|
20
|
+
this._dataCache = new Map();
|
21
|
+
this._subscriptions = new Map();
|
22
|
+
|
23
|
+
this._draggingY = false;
|
24
|
+
|
25
|
+
this.on('multi_series_changed', () => {
|
26
|
+
for (let stateController of this._stateControllers) {
|
27
|
+
stateController._multiSeries = this._multiSeries;
|
28
|
+
}
|
29
|
+
});
|
30
|
+
}
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Takes an array of user-supplied series, then splits, modifies, and broadcasts them
|
34
|
+
*
|
35
|
+
* @param {Array<Object>} series
|
36
|
+
*/
|
37
|
+
setSeries(series) {
|
38
|
+
|
39
|
+
if (this._prevSeries.length === series.length) {
|
40
|
+
let anyDifferent = false;
|
41
|
+
for (let i = 0; i < series.length; i++) {
|
42
|
+
if (series[i] !== this._prevSeries[i]) {
|
43
|
+
anyDifferent = true;
|
44
|
+
break;
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
if (!anyDifferent) {
|
49
|
+
return;
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
this._prevSeries = series;
|
54
|
+
|
55
|
+
const graphIndices = new Set();
|
56
|
+
const currentSeriesSet = new Set(series);
|
57
|
+
|
58
|
+
for (let singleSeries of series) {
|
59
|
+
let graphIndex = singleSeries.graph || 0;
|
60
|
+
|
61
|
+
if (this._seriesToGraphIndices.has(singleSeries)) {
|
62
|
+
graphIndex = this._seriesToGraphIndices.get(singleSeries);
|
63
|
+
}
|
64
|
+
|
65
|
+
graphIndices.add(graphIndex);
|
66
|
+
|
67
|
+
let seriesSet = this._graphIndicesToSeries.get(graphIndex);
|
68
|
+
if (!seriesSet) {
|
69
|
+
seriesSet = new Set();
|
70
|
+
this._graphIndicesToSeries.set(graphIndex, seriesSet);
|
71
|
+
}
|
72
|
+
|
73
|
+
seriesSet.add(singleSeries);
|
74
|
+
}
|
75
|
+
|
76
|
+
const sortedGraphIndices = [...graphIndices].sort();
|
77
|
+
|
78
|
+
this._multiSeries = [];
|
79
|
+
let globalSeriesIndex = 0;
|
80
|
+
|
81
|
+
for (let graphIndex of sortedGraphIndices) {
|
82
|
+
const series = [];
|
83
|
+
|
84
|
+
for (let singleSeries of this._graphIndicesToSeries.get(graphIndex)) {
|
85
|
+
if (!currentSeriesSet.has(singleSeries)) {
|
86
|
+
this._graphIndicesToSeries.get(graphIndex).delete(singleSeries);
|
87
|
+
continue;
|
88
|
+
}
|
89
|
+
|
90
|
+
if (this._modifiedSeries.has(singleSeries)) {
|
91
|
+
globalSeriesIndex++;
|
92
|
+
series.push(this._modifiedSeries.get(singleSeries));
|
93
|
+
continue;
|
94
|
+
}
|
95
|
+
|
96
|
+
const color = getColor(singleSeries.color, globalSeriesIndex);
|
97
|
+
const name = nameForSeries(singleSeries, globalSeriesIndex);
|
98
|
+
const modifiedSeries = {
|
99
|
+
...singleSeries,
|
100
|
+
multigrapherSeriesIndex: globalSeriesIndex,
|
101
|
+
multigrapherGraphIndex: graphIndex,
|
102
|
+
color,
|
103
|
+
name
|
104
|
+
};
|
105
|
+
|
106
|
+
this._modifiedSeries.set(singleSeries, modifiedSeries);
|
107
|
+
this._originalSeriesByMultigrapherIndex.set(globalSeriesIndex, singleSeries);
|
108
|
+
|
109
|
+
globalSeriesIndex++;
|
110
|
+
series.push(modifiedSeries);
|
111
|
+
}
|
112
|
+
|
113
|
+
this._multiSeries.push(series);
|
114
|
+
}
|
115
|
+
|
116
|
+
if (this._nextMultigrapherSeriesIndex) {
|
117
|
+
this._nextMultigrapherSeriesIndex = this._nextMultigrapherSeriesIndex - this._multiSeriesCount + globalSeriesIndex;
|
118
|
+
} else {
|
119
|
+
this._nextMultigrapherSeriesIndex = globalSeriesIndex;
|
120
|
+
}
|
121
|
+
this._multiSeriesCount = globalSeriesIndex;
|
122
|
+
|
123
|
+
this.emit('multi_series_changed', this.multiSeries);
|
124
|
+
}
|
125
|
+
|
126
|
+
/**
|
127
|
+
* Registers a state controller for series switching
|
128
|
+
*
|
129
|
+
* @param {StateController} stateController
|
130
|
+
*/
|
131
|
+
registerStateController(stateController) {
|
132
|
+
if (this._stateControllers.has(stateController)) {
|
133
|
+
return;
|
134
|
+
}
|
135
|
+
|
136
|
+
this._stateControllers.add(stateController);
|
137
|
+
stateController._multigraphStateController = this;
|
138
|
+
stateController._multiSeries = this._multiSeries;
|
139
|
+
|
140
|
+
stateController.on('dragging_y_finalized', ({ grapherID, axisIndex, draggedSeries }={}) => {
|
141
|
+
if (grapherID === stateController.grapherID) {
|
142
|
+
return;
|
143
|
+
}
|
144
|
+
|
145
|
+
const [check, multigrapherID, graphIndex] = grapherID.split('-');
|
146
|
+
if (check !== 'multigrapher' || multigrapherID !== this._id) {
|
147
|
+
return;
|
148
|
+
}
|
149
|
+
|
150
|
+
setTimeout(() => {
|
151
|
+
this.moveSeries({ axisIndex, draggedSeries, graphIndex });
|
152
|
+
});
|
153
|
+
});
|
154
|
+
|
155
|
+
stateController.on('dragging_y_changed', (draggingY) => {
|
156
|
+
if (draggingY === this._draggingY) {
|
157
|
+
return;
|
158
|
+
}
|
159
|
+
|
160
|
+
this._draggingY = draggingY;
|
161
|
+
this.emit('dragging_y_changed', this._draggingY);
|
162
|
+
});
|
163
|
+
|
164
|
+
stateController.on('observable_modified', (observable) => {
|
165
|
+
for (let otherStateController of this._stateControllers) {
|
166
|
+
if (stateController === otherStateController) {
|
167
|
+
continue;
|
168
|
+
}
|
169
|
+
|
170
|
+
otherStateController.markObservableModified(observable);
|
171
|
+
}
|
172
|
+
});
|
173
|
+
|
174
|
+
stateController.on('dispose', () => {
|
175
|
+
this._stateControllers.delete(stateController);
|
176
|
+
});
|
177
|
+
}
|
178
|
+
|
179
|
+
/**
|
180
|
+
* Moves the given dragged series (as specified by the child state controller) to a different graph
|
181
|
+
*
|
182
|
+
* @param {String} axisIndex - the axis index on the new graph to move the series to
|
183
|
+
* @param {Object} draggedSeries - the series that got dragged. Different identity than what this may access
|
184
|
+
* @param {String} graphIndex - the index of the graph to assign it to
|
185
|
+
*/
|
186
|
+
moveSeries({ axisIndex, draggedSeries, graphIndex }) {
|
187
|
+
const prevGraphCount = this.graphCount;
|
188
|
+
|
189
|
+
const originalSeries = this._originalSeriesByMultigrapherIndex.get(draggedSeries.multigrapherSeriesIndex);
|
190
|
+
const modifiedSeries = this._modifiedSeries.get(originalSeries);
|
191
|
+
|
192
|
+
this._multiSeries[modifiedSeries.multigrapherGraphIndex].splice(
|
193
|
+
this._multiSeries[modifiedSeries.multigrapherGraphIndex].indexOf(modifiedSeries), 1
|
194
|
+
);
|
195
|
+
this._multiSeries[modifiedSeries.multigrapherGraphIndex] = [...this._multiSeries[modifiedSeries.multigrapherGraphIndex]];
|
196
|
+
|
197
|
+
if (graphIndex === 'top') {
|
198
|
+
modifiedSeries.multigrapherGraphIndex = this._createGraphAtTop();
|
199
|
+
} else if (graphIndex === 'bottom') {
|
200
|
+
modifiedSeries.multigrapherGraphIndex = this._createGraphAtBottom();
|
201
|
+
} else {
|
202
|
+
modifiedSeries.multigrapherGraphIndex = parseInt(graphIndex);
|
203
|
+
}
|
204
|
+
|
205
|
+
modifiedSeries.axisIndex = axisIndex;
|
206
|
+
// safe because stateController operates on a copy. We could also have changed the identify of modifiedSeries,
|
207
|
+
// but with that we might reset data when moving it back
|
208
|
+
delete modifiedSeries.axis;
|
209
|
+
|
210
|
+
this._multiSeries[modifiedSeries.multigrapherGraphIndex] = [...this._multiSeries[modifiedSeries.multigrapherGraphIndex], modifiedSeries];
|
211
|
+
this._multiSeries = [...this._multiSeries];
|
212
|
+
|
213
|
+
for (let graphIndex = 0; graphIndex < this._multiSeries.length; graphIndex++) {
|
214
|
+
const originalSeriesAtIndex = this._multiSeries[graphIndex].map(({ multigrapherSeriesIndex }) =>
|
215
|
+
this._originalSeriesByMultigrapherIndex.get(multigrapherSeriesIndex));
|
216
|
+
this._graphIndicesToSeries.set(graphIndex, new Set(originalSeriesAtIndex));
|
217
|
+
|
218
|
+
for (let singleSeries of originalSeriesAtIndex) {
|
219
|
+
this._seriesToGraphIndices.set(singleSeries, graphIndex);
|
220
|
+
}
|
221
|
+
}
|
222
|
+
|
223
|
+
this.emit('multi_series_changed', this.multiSeries);
|
224
|
+
this.emit('graph_count_changed', this.graphCount, prevGraphCount);
|
225
|
+
}
|
226
|
+
|
227
|
+
/**
|
228
|
+
* Finds or creates an empty graph at the beginning and returns its index
|
229
|
+
*
|
230
|
+
* @return {number}
|
231
|
+
* @private
|
232
|
+
*/
|
233
|
+
_createGraphAtTop() {
|
234
|
+
// check if there's anything at the beginning already
|
235
|
+
let emptyAtTopIndex = null;
|
236
|
+
|
237
|
+
for (let i = 0; i < this._multiSeries.length; i++) {
|
238
|
+
if (this._multiSeries[i].length === 0) {
|
239
|
+
emptyAtTopIndex = i;
|
240
|
+
} else {
|
241
|
+
break;
|
242
|
+
}
|
243
|
+
}
|
244
|
+
|
245
|
+
if (emptyAtTopIndex !== null) {
|
246
|
+
return emptyAtTopIndex;
|
247
|
+
}
|
248
|
+
|
249
|
+
// add a series at the beginning and mutate the graph index of each
|
250
|
+
this._multiSeries = [[], ...this._multiSeries];
|
251
|
+
for (let graphIndex = 0; graphIndex < this._multiSeries.length; graphIndex++) {
|
252
|
+
if (!this._multiSeries[graphIndex].length) {
|
253
|
+
continue;
|
254
|
+
}
|
255
|
+
|
256
|
+
this._multiSeries[graphIndex] = [...this._multiSeries[graphIndex]];
|
257
|
+
|
258
|
+
for (let modifiedSeries of this._multiSeries[graphIndex]) {
|
259
|
+
modifiedSeries.multigrapherGraphIndex = graphIndex;
|
260
|
+
}
|
261
|
+
}
|
262
|
+
|
263
|
+
return 0;
|
264
|
+
}
|
265
|
+
|
266
|
+
/**
|
267
|
+
* Finds or creates an empty graph at the end and returns its index
|
268
|
+
*
|
269
|
+
* @return {number}
|
270
|
+
* @private
|
271
|
+
*/
|
272
|
+
_createGraphAtBottom() {
|
273
|
+
// check if there's anything at the beginning already
|
274
|
+
let emptyAtBottomIndex = null;
|
275
|
+
|
276
|
+
for (let i = this._multiSeries.length - 1; i >= 0; i--) {
|
277
|
+
if (this._multiSeries[i].length === 0) {
|
278
|
+
emptyAtBottomIndex = i;
|
279
|
+
} else {
|
280
|
+
break;
|
281
|
+
}
|
282
|
+
}
|
283
|
+
|
284
|
+
if (emptyAtBottomIndex !== null) {
|
285
|
+
return emptyAtBottomIndex;
|
286
|
+
}
|
287
|
+
|
288
|
+
// add something at the bottom
|
289
|
+
this._multiSeries = [...this._multiSeries, []];
|
290
|
+
|
291
|
+
return this._multiSeries.length - 1;
|
292
|
+
}
|
293
|
+
|
294
|
+
get multiSeries() {
|
295
|
+
return this._multiSeries.filter((series) => series.length);
|
296
|
+
}
|
297
|
+
|
298
|
+
get series() {
|
299
|
+
return [...this._stateControllers].map((stateController) => stateController.series).flat();
|
300
|
+
}
|
301
|
+
|
302
|
+
get graphCount() {
|
303
|
+
return this.multiSeries.length;
|
304
|
+
}
|
305
|
+
|
306
|
+
get draggingY() {
|
307
|
+
return this._draggingY;
|
308
|
+
}
|
309
|
+
|
310
|
+
dispose() {
|
311
|
+
this.clearListeners();
|
312
|
+
|
313
|
+
for (let listener of this._subscriptions.values()) {
|
314
|
+
listener.unsubscribe();
|
315
|
+
}
|
316
|
+
|
317
|
+
this._subscriptions.clear();
|
318
|
+
}
|
319
|
+
|
320
|
+
get stateControllerInitialization() {
|
321
|
+
return {
|
322
|
+
sharedDataCache: this._dataCache,
|
323
|
+
sharedSubscriptions: this._subscriptions
|
324
|
+
};
|
325
|
+
}
|
326
|
+
|
327
|
+
incrementMultigrapherSeriesIndex() {
|
328
|
+
const index = this._nextMultigrapherSeriesIndex;
|
329
|
+
|
330
|
+
this._nextMultigrapherSeriesIndex ++;
|
331
|
+
|
332
|
+
return index;
|
333
|
+
}
|
334
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
export default function selectionFromGlobalBounds(globalBounds, boundsCalculator) {
|
2
|
+
const targetBounds = Object.assign({}, globalBounds, boundsCalculator(globalBounds));
|
3
|
+
|
4
|
+
if (targetBounds.maxX instanceof Date) {
|
5
|
+
targetBounds.maxX = targetBounds.maxX.valueOf();
|
6
|
+
}
|
7
|
+
|
8
|
+
if (targetBounds.minX instanceof Date) {
|
9
|
+
targetBounds.minX = targetBounds.minX.valueOf();
|
10
|
+
}
|
11
|
+
|
12
|
+
if (targetBounds.maxX < targetBounds.minX) {
|
13
|
+
targetBounds.maxX = targetBounds.minX;
|
14
|
+
}
|
15
|
+
|
16
|
+
if (targetBounds.minX < globalBounds.minX) {
|
17
|
+
targetBounds.minX = globalBounds.minX;
|
18
|
+
}
|
19
|
+
|
20
|
+
if (targetBounds.maxX > globalBounds.maxX) {
|
21
|
+
targetBounds.maxX = globalBounds.maxX;
|
22
|
+
}
|
23
|
+
|
24
|
+
return targetBounds;
|
25
|
+
}
|
@@ -0,0 +1,115 @@
|
|
1
|
+
const BUCKETS_PER_PIXEL = 2;
|
2
|
+
const CONDENSE_THRESHOLD = 2;
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Condenses the dataset down to a lower number of points to make subsequent operations more efficient
|
6
|
+
* Will only condense when data length passes a given threshold
|
7
|
+
* Note that this works best on datasets that are relatively evenly distributed across the x axis
|
8
|
+
*
|
9
|
+
* @param {Array<Array>} data
|
10
|
+
* @param {{data: [], minX: Number, maxX: Number, length: Number}} swap
|
11
|
+
* @param {Number} minX
|
12
|
+
* @param maxX
|
13
|
+
* @param renderWidth
|
14
|
+
* @param dataChanged
|
15
|
+
* @return {{data: [], minX: Number, maxX: Number, length: Number}}
|
16
|
+
*/
|
17
|
+
export default function condenseDataSpace({ data, swap, minX, maxX, renderWidth, dataChanged }) {
|
18
|
+
const targetBucketCount = renderWidth*BUCKETS_PER_PIXEL;
|
19
|
+
|
20
|
+
const useSwap = !dataChanged && swap && swap.minX === minX && swap.maxX <= maxX && swap.length <= data.length;
|
21
|
+
|
22
|
+
let partiallyCondensedData;
|
23
|
+
if (useSwap) {
|
24
|
+
partiallyCondensedData = swap.data;
|
25
|
+
|
26
|
+
if (data.length > swap.length) {
|
27
|
+
// always overwrite the last in case it was mangled by the selected space interpolation
|
28
|
+
if (data.length > 0 && partiallyCondensedData.length > 0) {
|
29
|
+
partiallyCondensedData[partiallyCondensedData.length - 1] = data[swap.length - 1];
|
30
|
+
}
|
31
|
+
|
32
|
+
partiallyCondensedData = partiallyCondensedData.concat(data.slice(swap.length));
|
33
|
+
}
|
34
|
+
} else {
|
35
|
+
partiallyCondensedData = [...data];
|
36
|
+
}
|
37
|
+
|
38
|
+
if (partiallyCondensedData.length / targetBucketCount < CONDENSE_THRESHOLD*2) { // * 2 because min and max
|
39
|
+
return {
|
40
|
+
data: partiallyCondensedData,
|
41
|
+
minX,
|
42
|
+
maxX,
|
43
|
+
length: data.length
|
44
|
+
};
|
45
|
+
}
|
46
|
+
|
47
|
+
const condensedData = [];
|
48
|
+
const bucketSize = (maxX - minX)/targetBucketCount;
|
49
|
+
let minInBucket = null;
|
50
|
+
let maxInBucket = null;
|
51
|
+
let currentBucketIndex = 0;
|
52
|
+
|
53
|
+
// always add the first point so that x ranges are preserved
|
54
|
+
if (data.length) {
|
55
|
+
condensedData.push(data[0]);
|
56
|
+
}
|
57
|
+
|
58
|
+
for (let tuple of partiallyCondensedData) {
|
59
|
+
const [x, y] = tuple;
|
60
|
+
|
61
|
+
if (y === null) {
|
62
|
+
continue;
|
63
|
+
}
|
64
|
+
|
65
|
+
const bucketIndex = Math.floor((x - minX)/bucketSize);
|
66
|
+
|
67
|
+
if (bucketIndex !== currentBucketIndex) {
|
68
|
+
if (minInBucket && maxInBucket) {
|
69
|
+
if (minInBucket === maxInBucket) {
|
70
|
+
if (condensedData[condensedData.length - 1] !== minInBucket) {
|
71
|
+
condensedData.push(minInBucket);
|
72
|
+
}
|
73
|
+
} else if (minInBucket[0] < maxInBucket[0]) {
|
74
|
+
condensedData.push(maxInBucket, minInBucket);
|
75
|
+
} else {
|
76
|
+
condensedData.push(minInBucket, maxInBucket);
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
minInBucket = null;
|
81
|
+
maxInBucket = null;
|
82
|
+
currentBucketIndex = bucketIndex;
|
83
|
+
}
|
84
|
+
|
85
|
+
if (!minInBucket || y < minInBucket[1]) {
|
86
|
+
minInBucket = tuple;
|
87
|
+
}
|
88
|
+
|
89
|
+
if (!maxInBucket || y > maxInBucket[1]) {
|
90
|
+
maxInBucket = tuple;
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
if (minInBucket && maxInBucket) {
|
95
|
+
if (minInBucket === maxInBucket) {
|
96
|
+
condensedData.push(minInBucket);
|
97
|
+
} else if (minInBucket[0] < maxInBucket[0]) {
|
98
|
+
condensedData.push(maxInBucket, minInBucket);
|
99
|
+
} else {
|
100
|
+
condensedData.push(minInBucket, maxInBucket);
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
// always add the last point so that x ranges are preserved
|
105
|
+
if (data.length >= 2 && data[data.length - 1] !== minInBucket && data[data.length - 1] !== maxInBucket) {
|
106
|
+
condensedData.push(data[data.length - 1]);
|
107
|
+
}
|
108
|
+
|
109
|
+
return {
|
110
|
+
data: condensedData,
|
111
|
+
minX,
|
112
|
+
maxX,
|
113
|
+
length: data.length
|
114
|
+
};
|
115
|
+
}
|