@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,257 @@
|
|
1
|
+
import {startOfDayInTimezone} from './format';
|
2
|
+
|
3
|
+
function placeTick(trueValue, {scale, min, max, inverted, totalSize, precision, formatter, dates, justTime, justDate, formatOptions={} }, opts={}) {
|
4
|
+
let scaledValue = trueValue;
|
5
|
+
|
6
|
+
if (scale === 'log') {
|
7
|
+
scaledValue = 10**trueValue;
|
8
|
+
}
|
9
|
+
|
10
|
+
let percent = (trueValue - min)/(max - min);
|
11
|
+
if (inverted) {
|
12
|
+
percent = 1.0 - percent;
|
13
|
+
}
|
14
|
+
|
15
|
+
let pixelValue = percent * totalSize;
|
16
|
+
if (isNaN(pixelValue)) {
|
17
|
+
pixelValue = 0;
|
18
|
+
}
|
19
|
+
|
20
|
+
return {
|
21
|
+
pixelValue,
|
22
|
+
trueValue: scaledValue,
|
23
|
+
label: formatter(scaledValue, { ...formatOptions, precision, log: scale === 'log', dates, justTime, justDate }),
|
24
|
+
size: 'major',
|
25
|
+
...opts
|
26
|
+
};
|
27
|
+
}
|
28
|
+
|
29
|
+
function placeTickByPixel(pixelValue, {scale, min, max, inverted, totalSize, precision, formatter, dates, justTime, justDate, formatOptions={} }, opts={}) {
|
30
|
+
let percent = pixelValue/totalSize;
|
31
|
+
if (inverted) {
|
32
|
+
percent = 1.0 - percent;
|
33
|
+
}
|
34
|
+
|
35
|
+
let trueValue = percent * (max - min) + min;
|
36
|
+
|
37
|
+
if (scale === 'log') {
|
38
|
+
trueValue = Math.pow(10, trueValue);
|
39
|
+
}
|
40
|
+
|
41
|
+
return {
|
42
|
+
pixelValue,
|
43
|
+
trueValue,
|
44
|
+
label: formatter(trueValue, { ...formatOptions, precision, log: scale === 'log', dates, justTime, justDate }),
|
45
|
+
size: 'major',
|
46
|
+
...opts
|
47
|
+
};
|
48
|
+
}
|
49
|
+
|
50
|
+
function roundToEvenNumber(value, tickSpacing) {
|
51
|
+
return Math.round(value/tickSpacing)*tickSpacing;
|
52
|
+
}
|
53
|
+
|
54
|
+
function getEvenTickSpacing(span, desiredCount) {
|
55
|
+
const subspan = span/desiredCount;
|
56
|
+
|
57
|
+
const precision = -Math.log10(Math.abs(subspan)) + 1;
|
58
|
+
const multiplier = (precision - Math.floor(precision)) > 0.5 ? 2 : 1;
|
59
|
+
|
60
|
+
const roundTo = 10**Math.floor(precision) * multiplier;
|
61
|
+
|
62
|
+
return Math.round(subspan * roundTo)/roundTo;
|
63
|
+
}
|
64
|
+
|
65
|
+
function roundToDivisor(value, divisor) {
|
66
|
+
if (value <= 1) {
|
67
|
+
return 1;
|
68
|
+
}
|
69
|
+
|
70
|
+
if (divisor === 1) {
|
71
|
+
return Math.round(value);
|
72
|
+
}
|
73
|
+
|
74
|
+
if (value >= divisor) {
|
75
|
+
return Math.round(value/divisor)*divisor;
|
76
|
+
}
|
77
|
+
|
78
|
+
let divisors;
|
79
|
+
if (divisor === 24) {
|
80
|
+
divisors = [1, 2, 6, 12, 24];
|
81
|
+
} else if (divisor === 60) {
|
82
|
+
divisors = [1, 2, 5, 10, 15, 30, 60];
|
83
|
+
} else {
|
84
|
+
throw new Error('Invalid divisor');
|
85
|
+
}
|
86
|
+
|
87
|
+
let bestDelta = Infinity;
|
88
|
+
let bestDivisor = 1;
|
89
|
+
for (let i = 0; i < divisors.length; i++) {
|
90
|
+
const delta = Math.abs(divisors[i] - value);
|
91
|
+
if (delta < bestDelta) {
|
92
|
+
bestDivisor = divisors[i];
|
93
|
+
bestDelta = delta;
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
return bestDivisor;
|
98
|
+
}
|
99
|
+
|
100
|
+
function getEvenDateTickSpacing(span, desiredCount, unitOverride) {
|
101
|
+
const subspan = span / desiredCount;
|
102
|
+
|
103
|
+
if (subspan < 60*1000 && (!unitOverride || unitOverride === 'second')) {
|
104
|
+
return {
|
105
|
+
amount: roundToDivisor(subspan/1000, 60),
|
106
|
+
unit: 's'
|
107
|
+
};
|
108
|
+
}
|
109
|
+
|
110
|
+
if (subspan < 60*60*1000 && (!unitOverride || unitOverride === 'minute')) {
|
111
|
+
return {
|
112
|
+
amount: roundToDivisor(subspan/(60*1000), 60),
|
113
|
+
unit: 'm'
|
114
|
+
};
|
115
|
+
}
|
116
|
+
|
117
|
+
if (subspan < 24*60*60*1000 && (!unitOverride || unitOverride === 'hour')) {
|
118
|
+
return {
|
119
|
+
amount: roundToDivisor(subspan/(60*60*1000), 24),
|
120
|
+
unit: 'h'
|
121
|
+
};
|
122
|
+
}
|
123
|
+
|
124
|
+
if (unitOverride === 'day' || (subspan < 30*24*60*60*1000 && !unitOverride)) {
|
125
|
+
return {
|
126
|
+
amount: roundToDivisor(subspan/(24*60*60*1000), 1),
|
127
|
+
unit: 'd'
|
128
|
+
};
|
129
|
+
}
|
130
|
+
|
131
|
+
if (subspan > 30*24*60*60*1000 && (!unitOverride || unitOverride === 'month')) {
|
132
|
+
return {
|
133
|
+
amount: roundToDivisor(subspan/30*24*60*60*1000, 1),
|
134
|
+
unit: 'month'
|
135
|
+
};
|
136
|
+
}
|
137
|
+
|
138
|
+
if (unitOverride === 'year') {
|
139
|
+
return {
|
140
|
+
amount: roundToDivisor(subspan/365*24*60*60*1000, 1),
|
141
|
+
unit: 'year'
|
142
|
+
};
|
143
|
+
}
|
144
|
+
|
145
|
+
return {
|
146
|
+
unit: 'm',
|
147
|
+
amount: 60
|
148
|
+
};
|
149
|
+
}
|
150
|
+
|
151
|
+
function placeNumbersGrid({ min, max, precision, expectedLabelSize, labelPadding, totalSize, scale='linear', formatter, inverted=false, formatOptions }) {
|
152
|
+
const paddedLabelSize = expectedLabelSize + 2*labelPadding;
|
153
|
+
|
154
|
+
const ticks = [];
|
155
|
+
const placementParams = {scale, min, max, inverted, totalSize, precision, formatter, formatOptions, dates: false };
|
156
|
+
|
157
|
+
const labelCount = Math.floor((totalSize - expectedLabelSize*2)/paddedLabelSize);
|
158
|
+
const tickSpacing = getEvenTickSpacing(max - min, labelCount);
|
159
|
+
if (tickSpacing > 0) {
|
160
|
+
for (let value = roundToEvenNumber(min, tickSpacing); value < max; value += tickSpacing) {
|
161
|
+
ticks.push(placeTick(value, placementParams));
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
if (ticks.length) {
|
166
|
+
if (inverted && ticks[ticks.length - 1].pixelValue > labelPadding) {
|
167
|
+
ticks.push(placeTickByPixel(expectedLabelSize / 2, placementParams));
|
168
|
+
}
|
169
|
+
}
|
170
|
+
|
171
|
+
return ticks.filter(({ pixelValue }) => pixelValue <= totalSize && pixelValue >= 0);
|
172
|
+
}
|
173
|
+
|
174
|
+
function placeDatesGrid({ min, max, precision, expectedLabelSize, labelPadding, totalSize, skipFirst=false, skipLast=false, scale='linear', formatter, inverted=false, formatOptions }) {
|
175
|
+
const paddedLabelSize = expectedLabelSize + 2*labelPadding;
|
176
|
+
|
177
|
+
const ticks = [];
|
178
|
+
const placementParams = {scale, min, max, inverted, totalSize, precision, formatter, formatOptions, dates: true };
|
179
|
+
|
180
|
+
const { amount, unit } = getEvenDateTickSpacing(max - min, totalSize/paddedLabelSize, formatOptions.unitOverride);
|
181
|
+
|
182
|
+
const justDate = unit === 'month';
|
183
|
+
|
184
|
+
if (!skipFirst) {
|
185
|
+
ticks.push(placeTickByPixel(0, {...placementParams, justDate}, {position: 'first'}));
|
186
|
+
}
|
187
|
+
|
188
|
+
let currentDate = new Date(min);
|
189
|
+
|
190
|
+
if (unit === 'h') {
|
191
|
+
currentDate.setMinutes(0, 0, 0);
|
192
|
+
} else if (unit === 'm') {
|
193
|
+
currentDate.setSeconds(0, 0);
|
194
|
+
} else if (unit === 's') {
|
195
|
+
currentDate.setMilliseconds(0);
|
196
|
+
} else if (unit === 'month') {
|
197
|
+
currentDate = startOfDayInTimezone(currentDate, formatOptions.timeZone);
|
198
|
+
currentDate.setDate(1);
|
199
|
+
} else if (unit === 'd') {
|
200
|
+
currentDate = startOfDayInTimezone(currentDate, formatOptions.timeZone);
|
201
|
+
}
|
202
|
+
|
203
|
+
let lastDateString = formatter(currentDate, {...formatOptions, ...placementParams, justDate: true });
|
204
|
+
while (currentDate < max) {
|
205
|
+
let delta = 24*60*60*1000;
|
206
|
+
|
207
|
+
if (unit === 'h') {
|
208
|
+
delta = (amount - currentDate.getHours() % amount)*60*60*1000;
|
209
|
+
} else if (unit === 'm') {
|
210
|
+
delta = (amount - currentDate.getMinutes() % amount)*60*1000;
|
211
|
+
} else if (unit === 's') {
|
212
|
+
delta = (amount - currentDate.getSeconds() % amount)*1000;
|
213
|
+
} else if (unit === 'month') {
|
214
|
+
delta = 0;
|
215
|
+
if (currentDate.getMonth() === 11) {
|
216
|
+
currentDate = new Date(currentDate.getFullYear() + 1, 0, 1);
|
217
|
+
} else {
|
218
|
+
currentDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1);
|
219
|
+
}
|
220
|
+
} else if (unit === 'year') {
|
221
|
+
currentDate = new Date(currentDate.getFullYear() + 1, 0, 0);
|
222
|
+
}
|
223
|
+
|
224
|
+
currentDate = new Date(currentDate.valueOf() + delta);
|
225
|
+
|
226
|
+
const justTime = formatter(currentDate, {...formatOptions, ...placementParams, justDate: true }) === lastDateString;
|
227
|
+
|
228
|
+
const tick = placeTick(currentDate, {...placementParams, justTime, justDate});
|
229
|
+
|
230
|
+
if (ticks.length && tick.pixelValue - ticks[ticks.length - 1].pixelValue < expectedLabelSize + labelPadding) {
|
231
|
+
continue;
|
232
|
+
}
|
233
|
+
|
234
|
+
if (tick.pixelValue + expectedLabelSize/2 >= totalSize) {
|
235
|
+
continue;
|
236
|
+
}
|
237
|
+
|
238
|
+
ticks.push(tick);
|
239
|
+
lastDateString = formatter(currentDate, {...formatOptions, ...placementParams, justDate: true });
|
240
|
+
}
|
241
|
+
|
242
|
+
const justTime = lastDateString === formatter(max, {...formatOptions, ...placementParams, justDate: true });
|
243
|
+
|
244
|
+
if (!skipLast && ticks[ticks.length - 1].pixelValue + expectedLabelSize < totalSize) {
|
245
|
+
ticks.push(placeTickByPixel(totalSize, {...placementParams, justTime, justDate}, {position: 'last'}));
|
246
|
+
}
|
247
|
+
|
248
|
+
return ticks.filter(({ pixelValue }) => pixelValue <= totalSize && pixelValue >= 0);
|
249
|
+
}
|
250
|
+
|
251
|
+
export default function placeGrid(opts) {
|
252
|
+
if (opts.dates) {
|
253
|
+
return placeDatesGrid(opts);
|
254
|
+
} else {
|
255
|
+
return placeNumbersGrid(opts);
|
256
|
+
}
|
257
|
+
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
export default async function pyodideReady() {
|
2
|
+
if (window.pyodide) {
|
3
|
+
return window.pyodide;
|
4
|
+
}
|
5
|
+
|
6
|
+
while (!window.languagePluginLoader) {
|
7
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
8
|
+
}
|
9
|
+
|
10
|
+
await window.languagePluginLoader;
|
11
|
+
|
12
|
+
return window.pyodide;
|
13
|
+
}
|
@@ -0,0 +1,105 @@
|
|
1
|
+
import React, { useEffect, useMemo } from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import Grapher from './grapher.js';
|
4
|
+
import MultigraphStateController from './state/multigraph_state_controller.js';
|
5
|
+
import {useDraggingY, useMultiSeries} from './state/hooks.js';
|
6
|
+
import SyncPool from './state/sync_pool.js';
|
7
|
+
|
8
|
+
export default React.memo(MultiGrapher);
|
9
|
+
|
10
|
+
|
11
|
+
function MultiGrapher(props) {
|
12
|
+
/* eslint-disable react/prop-types */
|
13
|
+
|
14
|
+
const multigrapherID = useMemo(() => Math.random().toString(36).slice(2), []);
|
15
|
+
|
16
|
+
const multigraphStateController = useMemo(() => new MultigraphStateController({
|
17
|
+
id: multigrapherID,
|
18
|
+
...props
|
19
|
+
}), []);
|
20
|
+
|
21
|
+
const multiSeries = useMultiSeries(multigraphStateController);
|
22
|
+
|
23
|
+
const syncPool = useMemo(() => new SyncPool({
|
24
|
+
syncBounds: props.syncBounds,
|
25
|
+
syncTooltips: props.syncTooltips,
|
26
|
+
syncDragState: true
|
27
|
+
}), []);
|
28
|
+
|
29
|
+
const registerStateController = useMemo(() => multigraphStateController.registerStateController.bind(multigraphStateController), [multigraphStateController]);
|
30
|
+
|
31
|
+
useEffect(() => {
|
32
|
+
if (process.env.NODE_ENV === 'development') {
|
33
|
+
window.multigraphStateController = multigraphStateController;
|
34
|
+
}
|
35
|
+
|
36
|
+
return () => {
|
37
|
+
multigraphStateController.dispose();
|
38
|
+
};
|
39
|
+
}, [multigraphStateController]);
|
40
|
+
|
41
|
+
useEffect(() => {
|
42
|
+
props.exportStateController && props.exportStateController(multigraphStateController);
|
43
|
+
}, [multigraphStateController, props.exportStateController]);
|
44
|
+
|
45
|
+
useEffect(() => {
|
46
|
+
multigraphStateController.setSeries(props.series);
|
47
|
+
}, [multigraphStateController, props.series]);
|
48
|
+
|
49
|
+
useEffect(() => {
|
50
|
+
if (!props.onMultiseriesChange) {
|
51
|
+
return () => {};
|
52
|
+
}
|
53
|
+
|
54
|
+
multigraphStateController.on('multi_series_changed', props.onMultiseriesChange);
|
55
|
+
return () => {
|
56
|
+
multigraphStateController.off('multi_series_changed', props.onMultiseriesChange);
|
57
|
+
};
|
58
|
+
}, [multigraphStateController, props.onMultiseriesChange]);
|
59
|
+
|
60
|
+
const draggingY = useDraggingY(multigraphStateController);
|
61
|
+
|
62
|
+
return (
|
63
|
+
<div className="multigrapher">
|
64
|
+
{
|
65
|
+
draggingY && props.newUpperEnabled &&
|
66
|
+
<div className={`new-grapher grapher-${props.theme}`} data-grapher-id={`multigrapher-${multigrapherID}-top`}>
|
67
|
+
New grapher
|
68
|
+
</div>
|
69
|
+
}
|
70
|
+
|
71
|
+
{
|
72
|
+
multiSeries.map((series, i) =>
|
73
|
+
<Grapher
|
74
|
+
key={i}
|
75
|
+
{...props}
|
76
|
+
syncPool={syncPool}
|
77
|
+
stateControllerInitialization={multigraphStateController.stateControllerInitialization}
|
78
|
+
series={series}
|
79
|
+
id={`multigrapher-${multigrapherID}-${i}`}
|
80
|
+
dragPositionYOffset={props.newUpperEnabled ? 38 : 0}
|
81
|
+
exportStateController={registerStateController}
|
82
|
+
/>
|
83
|
+
)
|
84
|
+
}
|
85
|
+
|
86
|
+
{
|
87
|
+
draggingY &&
|
88
|
+
<div className={`new-grapher grapher-${props.theme}`} data-grapher-id={`multigrapher-${multigrapherID}-bottom`}>
|
89
|
+
New grapher
|
90
|
+
</div>
|
91
|
+
}
|
92
|
+
</div>
|
93
|
+
);
|
94
|
+
}
|
95
|
+
|
96
|
+
MultiGrapher.defaultProps = {
|
97
|
+
theme: 'night'
|
98
|
+
};
|
99
|
+
|
100
|
+
MultiGrapher.propTypes = Object.assign({}, Grapher.propTypes, {
|
101
|
+
syncBounds: PropTypes.bool,
|
102
|
+
syncTooltips: PropTypes.bool,
|
103
|
+
newUpperEnabled: PropTypes.bool,
|
104
|
+
onMultiseriesChange: PropTypes.func
|
105
|
+
});
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import backgroundFrag from './background.frag';
|
2
|
+
import backgroundVert from './background.vert';
|
3
|
+
import colorToVector from '../helpers/color_to_vector';
|
4
|
+
import createGLProgram from './create_gl_program';
|
5
|
+
|
6
|
+
export default class BackgroundProgram {
|
7
|
+
|
8
|
+
constructor(gl) {
|
9
|
+
this._gl = gl;
|
10
|
+
|
11
|
+
this._program = createGLProgram(gl, backgroundVert, backgroundFrag);
|
12
|
+
|
13
|
+
this._vertexBuffer = gl.createBuffer();
|
14
|
+
this._indexBuffer = gl.createBuffer();
|
15
|
+
|
16
|
+
if (!gl.getExtension('OES_element_index_uint')) {
|
17
|
+
console.error('Your browser does not support OES_element_index_uint'); // eslint-disable-line no-console
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
draw({ data }) {
|
22
|
+
const gl = this._gl;
|
23
|
+
gl.useProgram(this._program);
|
24
|
+
|
25
|
+
// gl.disable(gl.DEPTH_TEST);
|
26
|
+
|
27
|
+
for (let { minXt, maxXt, color } of data) {
|
28
|
+
gl.uniform4f(gl.getUniformLocation(this._program, 'color'), ...colorToVector(color));
|
29
|
+
|
30
|
+
const vertices = new Float32Array([
|
31
|
+
minXt, 1, maxXt, 1, maxXt, -1,
|
32
|
+
minXt, 1, maxXt, -1, minXt, -1
|
33
|
+
]);
|
34
|
+
|
35
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer);
|
36
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
|
37
|
+
|
38
|
+
const positionLocation = gl.getAttribLocation(this._program, 'position');
|
39
|
+
gl.enableVertexAttribArray(positionLocation);
|
40
|
+
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
41
|
+
|
42
|
+
gl.enable(gl.BLEND);
|
43
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
44
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
#ifdef GL_OES_standard_derivatives
|
2
|
+
#extension GL_OES_standard_derivatives : enable
|
3
|
+
#endif
|
4
|
+
|
5
|
+
precision mediump float;
|
6
|
+
|
7
|
+
uniform vec4 color;
|
8
|
+
|
9
|
+
// Adapted and modified from https://www.desultoryquest.com/blog/drawing-anti-aliased-circular-points-using-opengl-slash-webgl/
|
10
|
+
void main() {
|
11
|
+
float delta = 0.0;
|
12
|
+
float alpha = 1.0;
|
13
|
+
vec2 center = 2.0 * gl_PointCoord - 1.0;
|
14
|
+
float r = dot(center, center);
|
15
|
+
|
16
|
+
if (r > 1.0) {
|
17
|
+
discard;
|
18
|
+
}
|
19
|
+
|
20
|
+
#ifdef GL_OES_standard_derivatives
|
21
|
+
delta = fwidth(r);
|
22
|
+
alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, r);
|
23
|
+
#endif
|
24
|
+
|
25
|
+
gl_FragColor = color * alpha;
|
26
|
+
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
precision mediump float;
|
2
|
+
|
3
|
+
attribute vec2 position;
|
4
|
+
|
5
|
+
uniform float width;
|
6
|
+
uniform float height;
|
7
|
+
uniform float pointSize;
|
8
|
+
|
9
|
+
void main() {
|
10
|
+
gl_Position = vec4(2.0*position.x/width - 1.0, 1.0 - 2.0*position.y/height, 0.0, 1.0);
|
11
|
+
gl_PointSize = pointSize;
|
12
|
+
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
/**
|
2
|
+
* Creates a webgl program, linking the shaders and checking for errors
|
3
|
+
*
|
4
|
+
* @param gl
|
5
|
+
* @param vertShaderSource
|
6
|
+
* @param fragShaderSource
|
7
|
+
* @return {WebGLProgram}
|
8
|
+
*/
|
9
|
+
export default function createGLProgram(gl, vertShaderSource, fragShaderSource) {
|
10
|
+
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
11
|
+
gl.shaderSource(vertexShader, vertShaderSource);
|
12
|
+
gl.compileShader(vertexShader);
|
13
|
+
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
|
14
|
+
console.error(gl.getShaderInfoLog(vertexShader)); // eslint-disable-line no-console
|
15
|
+
}
|
16
|
+
|
17
|
+
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
18
|
+
gl.shaderSource(fragmentShader, fragShaderSource);
|
19
|
+
gl.compileShader(fragmentShader);
|
20
|
+
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
|
21
|
+
console.error(gl.getShaderInfoLog(fragmentShader)); // eslint-disable-line no-console
|
22
|
+
}
|
23
|
+
|
24
|
+
const program = gl.createProgram();
|
25
|
+
gl.attachShader(program, vertexShader);
|
26
|
+
gl.attachShader(program, fragmentShader);
|
27
|
+
|
28
|
+
gl.linkProgram(program);
|
29
|
+
gl.validateProgram(program);
|
30
|
+
|
31
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
32
|
+
console.error(gl.getProgramInfoLog(program)); // eslint-disable-line no-console
|
33
|
+
}
|
34
|
+
|
35
|
+
return program;
|
36
|
+
}
|
@@ -0,0 +1,159 @@
|
|
1
|
+
import pathsFrom from './paths_from';
|
2
|
+
import {drawZeroLine} from './draw_zero_line';
|
3
|
+
import {DPI_INCREASE} from './size_canvas';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Draws the data on the canvas
|
7
|
+
* Assumes the data is in individual point render space, ie x and y in pixels
|
8
|
+
*
|
9
|
+
* @param {Array<[number, number]>} individualPoints - data to draw
|
10
|
+
* @param {Object} dataInRenderSpace
|
11
|
+
* @param {Object} options - set of options
|
12
|
+
* @param {Object} options.context - the context to draw on
|
13
|
+
* @param {String} options.color - color of the bar to draw
|
14
|
+
* @param {{renderWidth: Number, renderHeight: Number}} options.sizing - size of the canvas, in pixels
|
15
|
+
* @param {Number} options.zero - y coordinate that represents "zero"
|
16
|
+
* @param {Boolean} options.hasNegatives - if any values are negative (in which case should render from zero)
|
17
|
+
* @param {Array<String>} [options.gradient] - an array of stops, from top to bottom of canvas, to draw with
|
18
|
+
* @param {String} [options.zeroColor] - color of the zero line
|
19
|
+
* @param {Number} [options.zeroWidth] - width of the zero line
|
20
|
+
* @param {Boolean} [options.showIndividualPoints] - draw circles at each point
|
21
|
+
* @param {String} [options.negativeColor] - color of the area below zero
|
22
|
+
* @param {Number} [options.width] - line width
|
23
|
+
* @private
|
24
|
+
*/
|
25
|
+
export default function drawArea(individualPoints, dataInRenderSpace, {
|
26
|
+
color, context, sizing, zero, hasNegatives, gradient,
|
27
|
+
zeroColor, zeroWidth, showIndividualPoints, negativeColor, pointRadius, width, highlighted,
|
28
|
+
shadowColor='black', shadowBlur=5
|
29
|
+
}) {
|
30
|
+
context.fillStyle = color;
|
31
|
+
context.shadowColor = shadowColor;
|
32
|
+
context.shadowBlur = shadowBlur;
|
33
|
+
|
34
|
+
if (gradient && gradient.length > 2) {
|
35
|
+
const globalGradient = context.createLinearGradient(0, 0, 0, sizing.renderHeight);
|
36
|
+
|
37
|
+
for (let i = 0; i < gradient.length; i++) {
|
38
|
+
const value = gradient[i];
|
39
|
+
if (Array.isArray(value)) {
|
40
|
+
globalGradient.addColorStop(value[0], value[1]);
|
41
|
+
} else {
|
42
|
+
globalGradient.addColorStop(i / (gradient.length - 1), value);
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
context.fillStyle = globalGradient;
|
47
|
+
|
48
|
+
if (color === 'gradient') {
|
49
|
+
context.strokeStyle = globalGradient;
|
50
|
+
}
|
51
|
+
} else {
|
52
|
+
context.fillStyle = color;
|
53
|
+
}
|
54
|
+
|
55
|
+
if (!individualPoints.length) {
|
56
|
+
return;
|
57
|
+
}
|
58
|
+
|
59
|
+
// we want to draw a polygon with a flat line at areaBottom, and then follows the shape of the data
|
60
|
+
const areaBottom = hasNegatives ? zero : sizing.renderHeight;
|
61
|
+
|
62
|
+
const areaPaths = pathsFrom(dataInRenderSpace);
|
63
|
+
const linePaths = pathsFrom(dataInRenderSpace, {
|
64
|
+
splitAtY: zero
|
65
|
+
});
|
66
|
+
|
67
|
+
for (let path of areaPaths) {
|
68
|
+
context.beginPath();
|
69
|
+
|
70
|
+
const [firstX, _startY] = path[0];
|
71
|
+
const [lastX, _lastY] = path[path.length - 1];
|
72
|
+
|
73
|
+
context.moveTo(firstX, areaBottom);
|
74
|
+
|
75
|
+
for (let i = 0; i < path.length; i++) {
|
76
|
+
const [x, y] = path[i];
|
77
|
+
context.lineTo(x, y);
|
78
|
+
}
|
79
|
+
|
80
|
+
context.lineTo(lastX, areaBottom);
|
81
|
+
|
82
|
+
context.fill();
|
83
|
+
}
|
84
|
+
|
85
|
+
if (highlighted) {
|
86
|
+
width += 2;
|
87
|
+
}
|
88
|
+
|
89
|
+
width *= DPI_INCREASE;
|
90
|
+
context.strokeStyle = color;
|
91
|
+
context.lineWidth = width;
|
92
|
+
// context.shadowBlur = 1;
|
93
|
+
|
94
|
+
for (let path of linePaths) {
|
95
|
+
if (!path.length) {
|
96
|
+
continue;
|
97
|
+
}
|
98
|
+
|
99
|
+
if (hasNegatives) {
|
100
|
+
let positive = true;
|
101
|
+
if (path.length >= 2) {
|
102
|
+
positive = path[1][1] <= zero;
|
103
|
+
} else {
|
104
|
+
positive = path[0][1] <= zero;
|
105
|
+
}
|
106
|
+
|
107
|
+
if (positive) {
|
108
|
+
context.strokeStyle = color;
|
109
|
+
} else {
|
110
|
+
context.strokeStyle = negativeColor;
|
111
|
+
}
|
112
|
+
}
|
113
|
+
|
114
|
+
context.beginPath();
|
115
|
+
|
116
|
+
for (let i = 0; i < path.length; i++) {
|
117
|
+
const [x, y] = path[i];
|
118
|
+
|
119
|
+
if (i === 0) {
|
120
|
+
context.moveTo(x, y);
|
121
|
+
} else {
|
122
|
+
context.lineTo(x, y);
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
context.stroke();
|
127
|
+
}
|
128
|
+
|
129
|
+
if (zeroWidth) {
|
130
|
+
drawZeroLine(areaBottom, {
|
131
|
+
context,
|
132
|
+
sizing,
|
133
|
+
color,
|
134
|
+
zero,
|
135
|
+
zeroColor,
|
136
|
+
zeroWidth
|
137
|
+
});
|
138
|
+
}
|
139
|
+
|
140
|
+
if (showIndividualPoints) {
|
141
|
+
context.fillStyle = color;
|
142
|
+
|
143
|
+
for (let [x, y] of individualPoints) {
|
144
|
+
if (negativeColor && hasNegatives) {
|
145
|
+
if (y === zero && zeroColor) {
|
146
|
+
context.fillStyle = zeroColor;
|
147
|
+
} else if (y < zero) {
|
148
|
+
context.fillStyle = color;
|
149
|
+
} else {
|
150
|
+
context.fillStyle = negativeColor;
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
context.beginPath();
|
155
|
+
context.arc(x, y, pointRadius ||8, 0, 2 * Math.PI, false);
|
156
|
+
context.fill();
|
157
|
+
}
|
158
|
+
}
|
159
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/**
|
2
|
+
* Draws the background on a 2d canvas
|
3
|
+
*
|
4
|
+
* @param {Array<{ minXt: number, maxXt: number, color: string }>} data
|
5
|
+
* @param {CanvasRenderingContext2D} context
|
6
|
+
*/
|
7
|
+
export default function drawBackground({ data }, { context }) {
|
8
|
+
const width = context.canvas.width;
|
9
|
+
const height = context.canvas.height;
|
10
|
+
|
11
|
+
for (let { minXt, maxXt, color } of data) {
|
12
|
+
context.fillStyle = color;
|
13
|
+
context.fillRect(minXt*width, 0, (maxXt - minXt)*width, height);
|
14
|
+
}
|
15
|
+
}
|