@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.
Files changed (112) hide show
  1. package/.eslintrc.js +85 -0
  2. package/.idea/codeStyles/Project.xml +19 -0
  3. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  4. package/.idea/grapher.iml +12 -0
  5. package/.idea/inspectionProfiles/Project_Default.xml +6 -0
  6. package/.idea/misc.xml +6 -0
  7. package/.idea/modules.xml +8 -0
  8. package/.idea/vcs.xml +6 -0
  9. package/0.bundle.js +2 -0
  10. package/0.bundle.js.map +1 -0
  11. package/1767282193a714f63082.module.wasm +0 -0
  12. package/537.bundle.js +2 -0
  13. package/537.bundle.js.map +1 -0
  14. package/831.bundle.js +2 -0
  15. package/831.bundle.js.map +1 -0
  16. package/bundle.js +2 -0
  17. package/bundle.js.map +1 -0
  18. package/package.json +75 -0
  19. package/readme.md +129 -0
  20. package/src/components/annotations.js +62 -0
  21. package/src/components/context_menu.js +73 -0
  22. package/src/components/draggable_points.js +114 -0
  23. package/src/components/graph_body.js +292 -0
  24. package/src/components/graph_title.js +16 -0
  25. package/src/components/options.js +111 -0
  26. package/src/components/percentile_button.js +72 -0
  27. package/src/components/range_graph.js +352 -0
  28. package/src/components/range_selection.js +175 -0
  29. package/src/components/range_selection_button.js +26 -0
  30. package/src/components/range_selection_button_base.js +51 -0
  31. package/src/components/series_key.js +235 -0
  32. package/src/components/series_key_axis_container.js +70 -0
  33. package/src/components/series_key_item.js +52 -0
  34. package/src/components/sidebar.js +76 -0
  35. package/src/components/tooltip.js +244 -0
  36. package/src/components/vertical_lines.js +70 -0
  37. package/src/components/x_axis.js +124 -0
  38. package/src/components/y_axis.js +239 -0
  39. package/src/eventable.js +65 -0
  40. package/src/grapher.js +367 -0
  41. package/src/grapher.scss +914 -0
  42. package/src/helpers/axis_sizes.js +2 -0
  43. package/src/helpers/binary_search.js +67 -0
  44. package/src/helpers/color_to_vector.js +35 -0
  45. package/src/helpers/colors.js +27 -0
  46. package/src/helpers/custom_prop_types.js +159 -0
  47. package/src/helpers/flatten_simple_data.js +81 -0
  48. package/src/helpers/format.js +233 -0
  49. package/src/helpers/generator_params_equal.js +10 -0
  50. package/src/helpers/name_for_series.js +16 -0
  51. package/src/helpers/place_grid.js +257 -0
  52. package/src/helpers/pyodide_ready.js +13 -0
  53. package/src/multigrapher.js +105 -0
  54. package/src/renderer/background.frag +7 -0
  55. package/src/renderer/background.vert +7 -0
  56. package/src/renderer/background_program.js +48 -0
  57. package/src/renderer/circle.frag +26 -0
  58. package/src/renderer/circle.vert +12 -0
  59. package/src/renderer/create_gl_program.js +36 -0
  60. package/src/renderer/draw_area.js +159 -0
  61. package/src/renderer/draw_background.js +15 -0
  62. package/src/renderer/draw_bars.js +80 -0
  63. package/src/renderer/draw_line.js +69 -0
  64. package/src/renderer/draw_zero_line.js +24 -0
  65. package/src/renderer/extract_vertices.js +137 -0
  66. package/src/renderer/graph_body_renderer.js +293 -0
  67. package/src/renderer/line.frag +51 -0
  68. package/src/renderer/line.vert +32 -0
  69. package/src/renderer/line_program.js +125 -0
  70. package/src/renderer/paths_from.js +72 -0
  71. package/src/renderer/scale_bounds.js +28 -0
  72. package/src/renderer/size_canvas.js +59 -0
  73. package/src/rust/Cargo.lock +233 -0
  74. package/src/rust/Cargo.toml +35 -0
  75. package/src/rust/pkg/grapher_rs.d.ts +42 -0
  76. package/src/rust/pkg/grapher_rs.js +351 -0
  77. package/src/rust/pkg/grapher_rs_bg.d.ts +11 -0
  78. package/src/rust/pkg/grapher_rs_bg.wasm +0 -0
  79. package/src/rust/pkg/index.js +342 -0
  80. package/src/rust/pkg/index_bg.wasm +0 -0
  81. package/src/rust/pkg/package.json +14 -0
  82. package/src/rust/src/extract_vertices.rs +83 -0
  83. package/src/rust/src/get_point_number.rs +50 -0
  84. package/src/rust/src/lib.rs +15 -0
  85. package/src/rust/src/selected_space_to_render_space.rs +131 -0
  86. package/src/state/average_loop_times.js +15 -0
  87. package/src/state/bound_calculator_from_selection.js +36 -0
  88. package/src/state/bound_calculators.js +41 -0
  89. package/src/state/calculate_annotations_state.js +59 -0
  90. package/src/state/calculate_data_bounds.js +104 -0
  91. package/src/state/calculate_tooltip_state.js +241 -0
  92. package/src/state/data_types.js +13 -0
  93. package/src/state/expand_bounds.js +58 -0
  94. package/src/state/find_matching_axis.js +31 -0
  95. package/src/state/get_default_bounds_calculator.js +15 -0
  96. package/src/state/hooks.js +164 -0
  97. package/src/state/infer_type.js +74 -0
  98. package/src/state/merge_bounds.js +64 -0
  99. package/src/state/multigraph_state_controller.js +334 -0
  100. package/src/state/selection_from_global_bounds.js +25 -0
  101. package/src/state/space_conversions/condense_data_space.js +115 -0
  102. package/src/state/space_conversions/data_space_to_selected_space.js +328 -0
  103. package/src/state/space_conversions/selected_space_to_background_space.js +144 -0
  104. package/src/state/space_conversions/selected_space_to_render_space.js +161 -0
  105. package/src/state/space_conversions/simple_series_to_data_space.js +229 -0
  106. package/src/state/state_controller.js +1770 -0
  107. package/src/state/sync_pool.js +101 -0
  108. package/test/setup.js +15 -0
  109. package/test/space_conversions/data_space_to_selected_space.test.js +434 -0
  110. package/webpack.dev.config.js +109 -0
  111. package/webpack.prod.config.js +60 -0
  112. 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,7 @@
1
+ precision highp float;
2
+
3
+ uniform vec4 color;
4
+
5
+ void main() {
6
+ gl_FragColor = vec4(color);
7
+ }
@@ -0,0 +1,7 @@
1
+ precision highp float;
2
+
3
+ attribute vec2 position;
4
+
5
+ void main() {
6
+ gl_Position = vec4((position - 0.5)*2.0, 0.0, 1.0);
7
+ }
@@ -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
+ }