@windborne/grapher 1.0.21 → 1.0.24
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/dist/26b23c7580ea3345d644.wasm +0 -0
- package/dist/744.bundle.cjs +2 -1
- package/dist/744.bundle.cjs.map +1 -0
- package/dist/744.bundle.esm.js +2 -1
- package/dist/744.bundle.esm.js.map +1 -0
- package/dist/bundle.cjs +1 -1
- package/dist/bundle.cjs.map +1 -1
- package/dist/bundle.esm.js +1 -1
- package/dist/bundle.esm.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +1 -1
- package/src/components/graph_body.jsx +41 -1
- package/src/components/range_graph.jsx +70 -22
- package/src/components/x_axis.jsx +139 -21
- package/src/components/y_axis.jsx +0 -5
- package/src/grapher.jsx +4 -3
- package/src/grapher.scss +84 -43
- package/src/helpers/color_to_vector.js +6 -1
- package/src/helpers/colors.js +82 -16
- package/src/helpers/custom_prop_types.js +2 -1
- package/src/helpers/place_grid.js +219 -0
- package/src/index.d.ts +150 -24
- package/src/renderer/create_gl_program.js +11 -3
- package/src/renderer/draw_area.js +1161 -102
- package/src/renderer/draw_background.js +5 -0
- package/src/renderer/draw_bars.js +100 -11
- package/src/renderer/draw_line.js +338 -38
- package/src/renderer/draw_zero_line.js +5 -0
- package/src/renderer/extract_vertices.js +1 -1
- package/src/renderer/graph_body_renderer.js +278 -17
- package/src/renderer/line.frag +16 -1
- package/src/renderer/line_program.js +200 -10
- package/src/renderer/shadow.frag +98 -0
- package/src/renderer/shadow.vert +20 -0
- package/src/renderer/shadow_program.js +479 -0
- package/src/rust/Cargo.lock +22 -31
- package/src/rust/pkg/grapher_rs.d.ts +6 -0
- package/src/rust/pkg/grapher_rs.js +5 -0
- package/src/rust/pkg/grapher_rs_bg.js +305 -0
- package/src/rust/pkg/grapher_rs_bg.wasm +0 -0
- package/src/rust/pkg/grapher_rs_bg.wasm.d.ts +11 -0
- package/src/rust/pkg/index.js +397 -0
- package/src/state/calculate_tooltip_state.js +6 -6
- package/src/state/state_controller.js +20 -6
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import sizeCanvas from './size_canvas';
|
|
2
|
-
import getColor from '../helpers/colors';
|
|
3
|
-
import LineProgram from './line_program';
|
|
4
|
-
import drawLine from './draw_line';
|
|
5
1
|
import Eventable from '../eventable';
|
|
6
|
-
import
|
|
2
|
+
import getColor from '../helpers/colors';
|
|
3
|
+
import inferType from '../state/infer_type';
|
|
7
4
|
import BackgroundProgram from './background_program.js';
|
|
8
|
-
import drawBars from './draw_bars';
|
|
9
5
|
import drawArea from './draw_area';
|
|
10
|
-
import
|
|
6
|
+
import drawBackground from './draw_background.js';
|
|
7
|
+
import drawBars from './draw_bars';
|
|
8
|
+
import drawLine from './draw_line';
|
|
9
|
+
import LineProgram from './line_program';
|
|
10
|
+
import ShadowProgram from './shadow_program';
|
|
11
|
+
import sizeCanvas from './size_canvas';
|
|
12
|
+
import { applyReducedOpacity } from "../helpers/colors";
|
|
11
13
|
|
|
12
14
|
export default class GraphBodyRenderer extends Eventable {
|
|
13
15
|
|
|
@@ -19,21 +21,36 @@ export default class GraphBodyRenderer extends Eventable {
|
|
|
19
21
|
this._checkIntersection = checkIntersection;
|
|
20
22
|
this._canvas = canvasElement;
|
|
21
23
|
this._webgl = webgl;
|
|
24
|
+
|
|
25
|
+
if (!this._canvas) {
|
|
26
|
+
console.error('Canvas element is null in GraphBodyRenderer constructor');
|
|
27
|
+
this._initialized = false;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
if (webgl) {
|
|
23
32
|
this._context = this._canvas.getContext('webgl');
|
|
24
33
|
if (this._context) {
|
|
25
34
|
this._lineProgram = new LineProgram(this._context);
|
|
35
|
+
this._shadowProgram = new ShadowProgram(this._context);
|
|
26
36
|
} else {
|
|
37
|
+
console.error('❌ WebGL context creation failed');
|
|
27
38
|
alert('WebGL failed! Attempting fallback to CPU rendering');
|
|
28
39
|
this._webgl = false;
|
|
29
40
|
}
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
if (!this._webgl) {
|
|
33
|
-
this._context = this._canvas.getContext(
|
|
44
|
+
this._context = this._canvas.getContext('2d');
|
|
34
45
|
this._context2d = this._context;
|
|
35
46
|
}
|
|
36
47
|
|
|
48
|
+
if (!this._context) {
|
|
49
|
+
console.error('Failed to get canvas context in GraphBodyRenderer');
|
|
50
|
+
this._initialized = false;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
this._initialized = this._initializeCanvas();
|
|
38
55
|
|
|
39
56
|
this._boundResize = this.resize.bind(this);
|
|
@@ -57,6 +74,7 @@ export default class GraphBodyRenderer extends Eventable {
|
|
|
57
74
|
dispose() {
|
|
58
75
|
this.clearListeners();
|
|
59
76
|
this._lineProgram && this._lineProgram.dispose();
|
|
77
|
+
this._shadowProgram && this._shadowProgram.dispose();
|
|
60
78
|
this._cachedAxisCount = null;
|
|
61
79
|
this._stateController.off('axes_changed', this._onAxisChange);
|
|
62
80
|
this._stateController.off('dragging_y_changed', this._boundResize);
|
|
@@ -68,6 +86,12 @@ export default class GraphBodyRenderer extends Eventable {
|
|
|
68
86
|
if (this._intersectionObserver) {
|
|
69
87
|
this._intersectionObserver.disconnect();
|
|
70
88
|
}
|
|
89
|
+
|
|
90
|
+
if (this._zeroLineCanvas && this._zeroLineCanvas.parentNode) {
|
|
91
|
+
this._zeroLineCanvas.parentNode.removeChild(this._zeroLineCanvas);
|
|
92
|
+
this._zeroLineCanvas = null;
|
|
93
|
+
this._zeroLineContext = null;
|
|
94
|
+
}
|
|
71
95
|
}
|
|
72
96
|
|
|
73
97
|
clear() {
|
|
@@ -79,6 +103,57 @@ export default class GraphBodyRenderer extends Eventable {
|
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
render(singleSeries, inRenderSpace, { highlighted, showIndividualPoints, shadowColor, shadowBlur, width, defaultLineWidth, bounds, globalBounds, inRenderSpaceAreaBottom }) {
|
|
106
|
+
if (!this._initialized || !this._context || !this._canvas) {
|
|
107
|
+
console.warn('GraphBodyRenderer: Cannot render - not initialized, missing context, or missing canvas');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let cutoffIndex = -1;
|
|
112
|
+
// cutoff time calculations for visible bounds-based approach
|
|
113
|
+
|
|
114
|
+
if (singleSeries.cutoffTime && singleSeries.data && singleSeries.data.length > 0) {
|
|
115
|
+
let cutoffDate;
|
|
116
|
+
if (singleSeries.cutoffTime === 'now') {
|
|
117
|
+
cutoffDate = new Date();
|
|
118
|
+
} else if (typeof singleSeries.cutoffTime === 'number') {
|
|
119
|
+
cutoffDate = new Date(singleSeries.cutoffTime);
|
|
120
|
+
} else {
|
|
121
|
+
cutoffDate = singleSeries.cutoffTime;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// getting the ghost point
|
|
125
|
+
const cutoffTime = cutoffDate instanceof Date ? cutoffDate.getTime() : cutoffDate;
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < singleSeries.data.length - 1; i++) {
|
|
128
|
+
const currentPoint = singleSeries.data[i];
|
|
129
|
+
const nextPoint = singleSeries.data[i + 1];
|
|
130
|
+
|
|
131
|
+
const currentTime = Array.isArray(currentPoint) ?
|
|
132
|
+
(currentPoint[0] instanceof Date ? currentPoint[0].getTime() : currentPoint[0]) : i;
|
|
133
|
+
const nextTime = Array.isArray(nextPoint) ?
|
|
134
|
+
(nextPoint[0] instanceof Date ? nextPoint[0].getTime() : nextPoint[0]) : (i + 1);
|
|
135
|
+
|
|
136
|
+
if (currentTime <= cutoffTime && cutoffTime <= nextTime) {
|
|
137
|
+
// interpolate exact position between these two points
|
|
138
|
+
const timeRatio = (cutoffTime - currentTime) / (nextTime - currentTime);
|
|
139
|
+
cutoffIndex = i + timeRatio;
|
|
140
|
+
break;
|
|
141
|
+
} else if (currentTime > cutoffTime) {
|
|
142
|
+
// cutoff is before the first data point
|
|
143
|
+
cutoffIndex = i;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// cutoff is after all data points
|
|
149
|
+
if (cutoffIndex === -1) {
|
|
150
|
+
cutoffIndex = singleSeries.data.length - 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
// Note: cutoffIndex is used for cutoff calculations but we no longer split data
|
|
155
|
+
}
|
|
156
|
+
|
|
82
157
|
const getIndividualPoints = (useDataSpace) => {
|
|
83
158
|
if (!bounds) {
|
|
84
159
|
bounds = singleSeries.axis.currentBounds;
|
|
@@ -144,8 +219,20 @@ export default class GraphBodyRenderer extends Eventable {
|
|
|
144
219
|
let commonCPUParams;
|
|
145
220
|
|
|
146
221
|
if (cpuRendering) {
|
|
147
|
-
|
|
148
|
-
|
|
222
|
+
if (this._webgl) {
|
|
223
|
+
console.warn(`CPU rendering (${singleSeries.rendering}) is not supported with webgl={true}. Use webgl={false} or switch to 'line' rendering.`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
//we still need a canvas context for cpu stuff
|
|
228
|
+
if (!this._context2d) {
|
|
229
|
+
this._context2d = this._canvas.getContext('2d', { willReadFrequently: false });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!this._context2d) {
|
|
233
|
+
console.error('Failed to get 2D context for CPU rendering');
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
149
236
|
|
|
150
237
|
if (this._webgl) {
|
|
151
238
|
// make sure we don't have any webgl stuff in the way before we get back to CPU rendering
|
|
@@ -177,18 +264,32 @@ export default class GraphBodyRenderer extends Eventable {
|
|
|
177
264
|
}
|
|
178
265
|
|
|
179
266
|
if (singleSeries.rendering === 'bar') {
|
|
180
|
-
|
|
267
|
+
let barParams = {
|
|
181
268
|
...commonCPUParams,
|
|
182
269
|
indexInAxis: singleSeries.axis.series.indexOf(singleSeries),
|
|
183
270
|
axisSeriesCount: singleSeries.axis.series.length,
|
|
184
271
|
closestSpacing: globalBounds.closestSpacing,
|
|
185
272
|
bounds
|
|
186
|
-
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
if (singleSeries.cutoffTime) {
|
|
276
|
+
barParams.cutoffIndex = cutoffIndex;
|
|
277
|
+
barParams.cutoffOpacity = 0.35;
|
|
278
|
+
barParams.originalData = singleSeries.data;
|
|
279
|
+
barParams.renderCutoffGradient = cutoffIndex >= 0;
|
|
280
|
+
|
|
281
|
+
const selection = this === this._stateController.rangeGraphRenderer
|
|
282
|
+
? this._stateController._bounds
|
|
283
|
+
: (this._stateController._selection || this._stateController._bounds);
|
|
284
|
+
barParams.selectionBounds = selection;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
drawBars(getIndividualPoints(true), barParams);
|
|
187
288
|
return;
|
|
188
289
|
}
|
|
189
290
|
|
|
190
291
|
if (singleSeries.rendering === 'area') {
|
|
191
|
-
|
|
292
|
+
let areaParams = {
|
|
192
293
|
...commonCPUParams,
|
|
193
294
|
showIndividualPoints: typeof singleSeries.showIndividualPoints === 'boolean' ? singleSeries.showIndividualPoints : showIndividualPoints,
|
|
194
295
|
gradient: singleSeries.gradient,
|
|
@@ -198,10 +299,147 @@ export default class GraphBodyRenderer extends Eventable {
|
|
|
198
299
|
shadowColor,
|
|
199
300
|
shadowBlur,
|
|
200
301
|
inRenderSpaceAreaBottom
|
|
201
|
-
}
|
|
202
|
-
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// add cutoff information for gradient area rendering
|
|
305
|
+
if (singleSeries.cutoffTime) {
|
|
306
|
+
areaParams.cutoffIndex = cutoffIndex;
|
|
307
|
+
areaParams.cutoffOpacity = 0.35;
|
|
308
|
+
areaParams.originalData = singleSeries.data;
|
|
309
|
+
areaParams.renderCutoffGradient = cutoffIndex >= 0;
|
|
310
|
+
areaParams.isPreview = this === this._stateController.rangeGraphRenderer;
|
|
311
|
+
|
|
312
|
+
const selection = this === this._stateController.rangeGraphRenderer
|
|
313
|
+
? this._stateController._bounds
|
|
314
|
+
: (this._stateController._selection || this._stateController._bounds);
|
|
315
|
+
areaParams.selectionBounds = selection;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
drawArea(getIndividualPoints(true), inRenderSpace, areaParams);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (singleSeries.rendering === 'shadow') {
|
|
322
|
+
if (!this._webgl || !this._shadowProgram) {
|
|
323
|
+
console.warn('Shadow rendering requires WebGL. Enable webgl={true} on your Grapher component.', {
|
|
324
|
+
webgl: !!this._webgl,
|
|
325
|
+
shadowProgram: !!this._shadowProgram,
|
|
326
|
+
program: !!this._shadowProgram?._program
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!this._shadowProgram._program) {
|
|
332
|
+
console.error('ShadowProgram has no valid WebGL program');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!inRenderSpace) {
|
|
337
|
+
console.error('inRenderSpace is null for shadow rendering');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!bounds) {
|
|
342
|
+
bounds = singleSeries.axis.currentBounds;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let zero = singleSeries.zeroLineY === 'bottom' ?
|
|
346
|
+
this._sizing.renderHeight :
|
|
347
|
+
(1.0 - ((singleSeries.zeroLineY || 0) - bounds.minY) / (bounds.maxY - bounds.minY)) * this._sizing.renderHeight;
|
|
348
|
+
|
|
349
|
+
const boundsChanged = !this._lastBounds ||
|
|
350
|
+
bounds.minY !== this._lastBounds.minY ||
|
|
351
|
+
bounds.maxY !== this._lastBounds.maxY ||
|
|
352
|
+
this._sizing.renderHeight !== this._lastRenderHeight;
|
|
353
|
+
|
|
354
|
+
this._lastBounds = {...bounds};
|
|
355
|
+
this._lastRenderHeight = this._sizing.renderHeight;
|
|
356
|
+
|
|
357
|
+
if (boundsChanged && this._lastShadowCache) {
|
|
358
|
+
this._lastShadowCache = null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (zero > this._sizing.renderHeight * 1.5) {
|
|
362
|
+
zero = this._sizing.renderHeight;
|
|
363
|
+
} else if (zero < -this._sizing.renderHeight * 0.5) {
|
|
364
|
+
zero = 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let shadowParams = {
|
|
368
|
+
color: getColor(singleSeries.color, singleSeries.index, singleSeries.multigrapherSeriesIndex),
|
|
369
|
+
gradient: singleSeries.gradient,
|
|
370
|
+
zero,
|
|
371
|
+
sizing: this._sizing,
|
|
372
|
+
inRenderSpaceAreaBottom
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// add cutoff information for gradient shadow rendering
|
|
376
|
+
if (singleSeries.cutoffTime) {
|
|
377
|
+
shadowParams.cutoffIndex = cutoffIndex;
|
|
378
|
+
shadowParams.cutoffOpacity = 0.35;
|
|
379
|
+
shadowParams.originalData = singleSeries.data;
|
|
380
|
+
shadowParams.renderCutoffGradient = cutoffIndex >= 0;
|
|
381
|
+
shadowParams.isPreview = this === this._stateController.rangeGraphRenderer;
|
|
382
|
+
|
|
383
|
+
const selection = this === this._stateController.rangeGraphRenderer
|
|
384
|
+
? this._stateController._bounds
|
|
385
|
+
: (this._stateController._selection || this._stateController._bounds);
|
|
386
|
+
shadowParams.selectionBounds = selection;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this._shadowProgram.draw(getIndividualPoints(true), shadowParams);
|
|
390
|
+
|
|
391
|
+
if (this._webgl) {
|
|
392
|
+
const gl = this._context;
|
|
393
|
+
gl.disable(gl.BLEND);
|
|
394
|
+
gl.enable(gl.BLEND);
|
|
395
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
if (singleSeries.zeroLineWidth && singleSeries.zeroLineWidth > 0) {
|
|
400
|
+
if (this._context2d) {
|
|
401
|
+
// in non-webgl mode, use the existing 2d context
|
|
402
|
+
this._context2d.save();
|
|
403
|
+
this._context2d.strokeStyle = singleSeries.zeroLineColor || getColor(singleSeries.color, singleSeries.index, singleSeries.multigrapherSeriesIndex);
|
|
404
|
+
this._context2d.lineWidth = singleSeries.zeroLineWidth;
|
|
405
|
+
this._context2d.globalCompositeOperation = 'source-over';
|
|
406
|
+
|
|
407
|
+
this._context2d.beginPath();
|
|
408
|
+
this._context2d.moveTo(0, zero);
|
|
409
|
+
this._context2d.lineTo(this._sizing.renderWidth, zero);
|
|
410
|
+
this._context2d.stroke();
|
|
411
|
+
this._context2d.restore();
|
|
412
|
+
} else {
|
|
413
|
+
// in webgl mode, we instead create an overlay 2d canvas for the zero line
|
|
414
|
+
if (!this._zeroLineCanvas) {
|
|
415
|
+
this._zeroLineCanvas = document.createElement('canvas');
|
|
416
|
+
this._zeroLineCanvas.style.position = 'absolute';
|
|
417
|
+
this._zeroLineCanvas.style.top = '0';
|
|
418
|
+
this._zeroLineCanvas.style.left = '0';
|
|
419
|
+
this._zeroLineCanvas.style.pointerEvents = 'none';
|
|
420
|
+
this._zeroLineContext = this._zeroLineCanvas.getContext('2d');
|
|
421
|
+
this._canvas.parentNode.insertBefore(this._zeroLineCanvas, this._canvas.nextSibling);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
this._zeroLineCanvas.width = this._canvas.width;
|
|
425
|
+
this._zeroLineCanvas.height = this._canvas.height;
|
|
426
|
+
this._zeroLineCanvas.style.width = this._canvas.style.width;
|
|
427
|
+
this._zeroLineCanvas.style.height = this._canvas.style.height;
|
|
428
|
+
|
|
429
|
+
this._zeroLineContext.clearRect(0, 0, this._zeroLineCanvas.width, this._zeroLineCanvas.height);
|
|
430
|
+
this._zeroLineContext.strokeStyle = singleSeries.zeroLineColor || getColor(singleSeries.color, singleSeries.index, singleSeries.multigrapherSeriesIndex);
|
|
431
|
+
this._zeroLineContext.lineWidth = singleSeries.zeroLineWidth;
|
|
432
|
+
|
|
433
|
+
this._zeroLineContext.beginPath();
|
|
434
|
+
this._zeroLineContext.moveTo(0, zero);
|
|
435
|
+
this._zeroLineContext.lineTo(this._sizing.renderWidth, zero);
|
|
436
|
+
this._zeroLineContext.stroke();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
203
439
|
}
|
|
204
440
|
|
|
441
|
+
const shouldShowIndividualPoints = typeof singleSeries.showIndividualPoints === 'boolean' ? singleSeries.showIndividualPoints : showIndividualPoints;
|
|
442
|
+
|
|
205
443
|
const drawParams = {
|
|
206
444
|
color: getColor(singleSeries.color, singleSeries.index, singleSeries.multigrapherSeriesIndex),
|
|
207
445
|
context: this._context,
|
|
@@ -211,16 +449,39 @@ export default class GraphBodyRenderer extends Eventable {
|
|
|
211
449
|
dashed: singleSeries.dashed,
|
|
212
450
|
dashPattern: singleSeries.dashPattern,
|
|
213
451
|
highlighted,
|
|
214
|
-
showIndividualPoints:
|
|
452
|
+
showIndividualPoints: shouldShowIndividualPoints,
|
|
215
453
|
getIndividualPoints,
|
|
216
|
-
getRanges: singleSeries.rangeKey ? getRanges : null
|
|
454
|
+
getRanges: singleSeries.rangeKey ? getRanges : null,
|
|
455
|
+
rendering: singleSeries.rendering // Pass rendering type for all charts
|
|
217
456
|
};
|
|
218
457
|
|
|
458
|
+
if (!inRenderSpace) {
|
|
459
|
+
console.error('inRenderSpace is null for line rendering');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Add cutoff information to drawParams for gradient line rendering
|
|
464
|
+
if (singleSeries.cutoffTime) {
|
|
465
|
+
drawParams.cutoffIndex = cutoffIndex;
|
|
466
|
+
drawParams.cutoffOpacity = 0.35;
|
|
467
|
+
drawParams.originalData = singleSeries.data;
|
|
468
|
+
drawParams.renderCutoffGradient = cutoffIndex >= 0; // Only render cutoff if valid cutoff
|
|
469
|
+
drawParams.currentBounds = bounds;
|
|
470
|
+
drawParams.isPreview = this === this._stateController.rangeGraphRenderer; // Flag for preview rendering
|
|
471
|
+
|
|
472
|
+
// Always set selectionBounds with fallback
|
|
473
|
+
const selection = this === this._stateController.rangeGraphRenderer
|
|
474
|
+
? this._stateController._bounds
|
|
475
|
+
: (this._stateController._selection || this._stateController._bounds);
|
|
476
|
+
drawParams.selectionBounds = selection;
|
|
477
|
+
}
|
|
478
|
+
|
|
219
479
|
if (this._webgl) {
|
|
220
480
|
this._lineProgram.draw(inRenderSpace, drawParams);
|
|
221
481
|
} else {
|
|
222
482
|
drawLine(inRenderSpace, drawParams);
|
|
223
483
|
}
|
|
484
|
+
|
|
224
485
|
}
|
|
225
486
|
|
|
226
487
|
renderBackground(inBackgroundSpace) {
|
package/src/renderer/line.frag
CHANGED
|
@@ -4,6 +4,9 @@ uniform vec4 color;
|
|
|
4
4
|
uniform float thickness;
|
|
5
5
|
uniform float shadowBlur;
|
|
6
6
|
uniform vec4 shadowColor;
|
|
7
|
+
uniform float width; // Canvas width for normalization
|
|
8
|
+
uniform float cutoffX; // Cutoff X position as ratio (0.0-1.0)
|
|
9
|
+
uniform float cutoffOpacity; // Opacity for pre-cutoff area
|
|
7
10
|
|
|
8
11
|
varying vec2 position_vec;
|
|
9
12
|
varying vec2 prev_position_vec;
|
|
@@ -39,13 +42,25 @@ float distance_from_line() {
|
|
|
39
42
|
void main() {
|
|
40
43
|
vec4 transparent = vec4(0.0, 0.0, 0.0, 0.0);
|
|
41
44
|
|
|
45
|
+
// Apply cutoff opacity if active
|
|
46
|
+
vec4 finalColor = vec4(color);
|
|
47
|
+
if (cutoffX >= 0.0) {
|
|
48
|
+
// Convert pixel position to normalized coordinate (0.0 to 1.0)
|
|
49
|
+
float normalizedX = gl_FragCoord.x / width;
|
|
50
|
+
|
|
51
|
+
// Apply reduced opacity to pixels left of cutoff
|
|
52
|
+
if (normalizedX < cutoffX) {
|
|
53
|
+
finalColor.a *= cutoffOpacity;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
float dist = distance_from_line();
|
|
43
58
|
|
|
44
59
|
if (dist + shadowBlur >= thickness) {
|
|
45
60
|
float percent_shadowed = ((thickness - dist) / shadowBlur);
|
|
46
61
|
gl_FragColor = mix(transparent, shadowColor, percent_shadowed*percent_shadowed);
|
|
47
62
|
} else {
|
|
48
|
-
gl_FragColor =
|
|
63
|
+
gl_FragColor = finalColor;
|
|
49
64
|
gl_FragColor.rgb *= gl_FragColor.a;
|
|
50
65
|
}
|
|
51
66
|
}
|
|
@@ -6,6 +6,7 @@ import colorToVector from '../helpers/color_to_vector';
|
|
|
6
6
|
import extractVertices from './extract_vertices';
|
|
7
7
|
import createGLProgram from './create_gl_program';
|
|
8
8
|
import {DPI_INCREASE} from './size_canvas';
|
|
9
|
+
import { applyReducedOpacity } from "../helpers/colors";
|
|
9
10
|
|
|
10
11
|
export default class LineProgram {
|
|
11
12
|
|
|
@@ -36,7 +37,6 @@ export default class LineProgram {
|
|
|
36
37
|
const height = gl.drawingBufferHeight;
|
|
37
38
|
|
|
38
39
|
gl.clearColor(0, 0, 0, 0);
|
|
39
|
-
gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT);
|
|
40
40
|
gl.viewport(0, 0, width, height);
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -63,16 +63,23 @@ export default class LineProgram {
|
|
|
63
63
|
const height = gl.drawingBufferHeight;
|
|
64
64
|
gl.useProgram(this._program);
|
|
65
65
|
|
|
66
|
-
// gl.disable(gl.DEPTH_TEST);
|
|
67
|
-
|
|
68
66
|
const thickness = DPI_INCREASE*((parameters.width || 1) + (parameters.highlighted ? 2 : 0));
|
|
69
67
|
const shadowBlur = parameters.shadowBlur === undefined ? 2 : parameters.shadowBlur;
|
|
70
68
|
const shadowColor = parameters.shadowColor || 'black';
|
|
71
69
|
const dashed = parameters.dashed || false;
|
|
72
70
|
const dashPattern = parameters.dashPattern || [5, 5];
|
|
71
|
+
if (parameters.renderCutoffGradient && parameters.cutoffIndex !== undefined && parameters.originalData) {
|
|
72
|
+
this.drawLineWithCutoff(dataInRenderSpace, parameters);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
73
75
|
|
|
74
76
|
const {positions, prevPositions, vertices, indices} = extractVertices(dataInRenderSpace, { dashed, dashPattern });
|
|
75
77
|
|
|
78
|
+
if (!this._program) {
|
|
79
|
+
console.error('WebGL line program is null - shader compilation failed!');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
const positionIndex = gl.getAttribLocation(this._program, 'position');
|
|
77
84
|
const prevPositionIndex = gl.getAttribLocation(this._program, 'prevPosition');
|
|
78
85
|
const vertexIndex = gl.getAttribLocation(this._program, 'vertex');
|
|
@@ -96,8 +103,15 @@ export default class LineProgram {
|
|
|
96
103
|
gl.uniform1f(gl.getUniformLocation(this._program, 'height'), height);
|
|
97
104
|
gl.uniform1f(gl.getUniformLocation(this._program, 'thickness'), Math.max(thickness, 1)+shadowBlur);
|
|
98
105
|
gl.uniform1f(gl.getUniformLocation(this._program, 'shadowBlur'), shadowBlur);
|
|
99
|
-
|
|
106
|
+
const colorVector = colorToVector(parameters.color);
|
|
107
|
+
gl.uniform4f(gl.getUniformLocation(this._program, 'color'), ...colorVector);
|
|
100
108
|
gl.uniform4f(gl.getUniformLocation(this._program, 'shadowColor'), ...colorToVector(shadowColor));
|
|
109
|
+
|
|
110
|
+
const cutoffX = parameters.cutoffX !== undefined ? parameters.cutoffX : -1.0; // Use parameter or disable
|
|
111
|
+
const cutoffOpacity = parameters.cutoffOpacity !== undefined ? parameters.cutoffOpacity : 1.0;
|
|
112
|
+
|
|
113
|
+
gl.uniform1f(gl.getUniformLocation(this._program, 'cutoffX'), cutoffX);
|
|
114
|
+
gl.uniform1f(gl.getUniformLocation(this._program, 'cutoffOpacity'), cutoffOpacity);
|
|
101
115
|
|
|
102
116
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer);
|
|
103
117
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
|
|
@@ -109,17 +123,193 @@ export default class LineProgram {
|
|
|
109
123
|
gl.uniform1f(gl.getUniformLocation(this._circleProgram, 'width'), width);
|
|
110
124
|
gl.uniform1f(gl.getUniformLocation(this._circleProgram, 'height'), height);
|
|
111
125
|
gl.uniform1f(gl.getUniformLocation(this._circleProgram, 'pointSize'), 2*(thickness+6));
|
|
112
|
-
gl.uniform4f(gl.getUniformLocation(this._circleProgram, 'color'), ...colorToVector(parameters.color));
|
|
113
126
|
|
|
114
127
|
const individualPoints = parameters.getIndividualPoints();
|
|
115
128
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
if (parameters.cutoffIndex !== undefined && parameters.cutoffIndex > 0 && parameters.originalData) {
|
|
130
|
+
const { originalData } = parameters;
|
|
131
|
+
let cutoffTime;
|
|
132
|
+
|
|
133
|
+
if (typeof originalData[0] === 'object' && originalData[0].length === 2) {
|
|
134
|
+
const baseIndex = Math.floor(parameters.cutoffIndex);
|
|
135
|
+
const fraction = parameters.cutoffIndex - baseIndex;
|
|
136
|
+
|
|
137
|
+
if (fraction === 0 || baseIndex >= originalData.length - 1) {
|
|
138
|
+
const cutoffDate = originalData[Math.min(baseIndex, originalData.length - 1)][0];
|
|
139
|
+
cutoffTime = cutoffDate instanceof Date ? cutoffDate.getTime() : cutoffDate;
|
|
140
|
+
} else {
|
|
141
|
+
const currentDate = originalData[baseIndex][0];
|
|
142
|
+
const nextDate = originalData[baseIndex + 1][0];
|
|
143
|
+
const currentTime = currentDate instanceof Date ? currentDate.getTime() : currentDate;
|
|
144
|
+
const nextTime = nextDate instanceof Date ? nextDate.getTime() : nextDate;
|
|
145
|
+
cutoffTime = currentTime + fraction * (nextTime - currentTime);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const preCutoffPoints = [];
|
|
150
|
+
const postCutoffPoints = [];
|
|
151
|
+
|
|
152
|
+
if (parameters.isPreview) {
|
|
153
|
+
const firstTime = originalData[0][0] instanceof Date ? originalData[0][0].getTime() : originalData[0][0];
|
|
154
|
+
const lastTime = originalData[originalData.length - 1][0] instanceof Date ?
|
|
155
|
+
originalData[originalData.length - 1][0].getTime() : originalData[originalData.length - 1][0];
|
|
156
|
+
const timeRatio = (cutoffTime - firstTime) / (lastTime - firstTime);
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < individualPoints.length; i++) {
|
|
159
|
+
const pointRatio = i / (individualPoints.length - 1);
|
|
160
|
+
if (pointRatio < timeRatio) {
|
|
161
|
+
preCutoffPoints.push(individualPoints[i]);
|
|
162
|
+
} else {
|
|
163
|
+
postCutoffPoints.push(individualPoints[i]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else if (!parameters.selectionBounds) {
|
|
167
|
+
postCutoffPoints.push(...individualPoints);
|
|
168
|
+
} else {
|
|
169
|
+
const visibleMinTime = parameters.selectionBounds.minX instanceof Date ?
|
|
170
|
+
parameters.selectionBounds.minX.getTime() : parameters.selectionBounds.minX;
|
|
171
|
+
const visibleMaxTime = parameters.selectionBounds.maxX instanceof Date ?
|
|
172
|
+
parameters.selectionBounds.maxX.getTime() : parameters.selectionBounds.maxX;
|
|
173
|
+
|
|
174
|
+
if (cutoffTime < visibleMinTime) {
|
|
175
|
+
postCutoffPoints.push(...individualPoints);
|
|
176
|
+
} else if (cutoffTime > visibleMaxTime) {
|
|
177
|
+
if (parameters.rendering === 'shadow') {
|
|
178
|
+
postCutoffPoints.push(...individualPoints);
|
|
179
|
+
} else {
|
|
180
|
+
preCutoffPoints.push(...individualPoints);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
const visibleCutoffRatio = (cutoffTime - visibleMinTime) / (visibleMaxTime - visibleMinTime);
|
|
184
|
+
const renderWidth = this._gl.canvas.width;
|
|
185
|
+
const cutoffPixelX = visibleCutoffRatio * renderWidth;
|
|
186
|
+
|
|
187
|
+
individualPoints.forEach((point, index) => {
|
|
188
|
+
const [pixelX, pixelY] = point;
|
|
189
|
+
|
|
190
|
+
if (pixelX < cutoffPixelX) {
|
|
191
|
+
preCutoffPoints.push(point);
|
|
192
|
+
} else {
|
|
193
|
+
postCutoffPoints.push(point);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (preCutoffPoints.length > 0) {
|
|
200
|
+
const reducedOpacityColor = applyReducedOpacity(parameters.color, parameters.cutoffOpacity || 0.35);
|
|
201
|
+
gl.uniform4f(gl.getUniformLocation(this._circleProgram, 'color'), ...colorToVector(reducedOpacityColor));
|
|
202
|
+
|
|
203
|
+
gl.enableVertexAttribArray(0);
|
|
204
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this._individualPointBuffer);
|
|
205
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(preCutoffPoints.flat()), gl.STATIC_DRAW);
|
|
206
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
207
|
+
gl.drawArrays(gl.POINTS, 0, preCutoffPoints.length);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (postCutoffPoints.length > 0) {
|
|
211
|
+
gl.uniform4f(gl.getUniformLocation(this._circleProgram, 'color'), ...colorToVector(parameters.color));
|
|
212
|
+
|
|
213
|
+
gl.enableVertexAttribArray(0);
|
|
214
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this._individualPointBuffer);
|
|
215
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(postCutoffPoints.flat()), gl.STATIC_DRAW);
|
|
216
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
217
|
+
gl.drawArrays(gl.POINTS, 0, postCutoffPoints.length);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
gl.uniform4f(gl.getUniformLocation(this._circleProgram, 'color'), ...colorToVector(parameters.color));
|
|
221
|
+
|
|
222
|
+
gl.enableVertexAttribArray(0);
|
|
223
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this._individualPointBuffer);
|
|
224
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(individualPoints.flat()), gl.STATIC_DRAW);
|
|
225
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
120
226
|
|
|
121
|
-
|
|
227
|
+
gl.drawArrays(gl.POINTS, 0, individualPoints.length);
|
|
228
|
+
}
|
|
122
229
|
}
|
|
123
230
|
}
|
|
124
231
|
|
|
232
|
+
drawLineWithCutoff(dataInRenderSpace, parameters) {
|
|
233
|
+
const { cutoffIndex, cutoffOpacity, originalData, selectionBounds } = parameters;
|
|
234
|
+
|
|
235
|
+
let cutoffTime;
|
|
236
|
+
if (typeof originalData[0] === 'object' && originalData[0].length === 2) {
|
|
237
|
+
const baseIndex = Math.floor(cutoffIndex);
|
|
238
|
+
const fraction = cutoffIndex - baseIndex;
|
|
239
|
+
|
|
240
|
+
if (fraction === 0 || baseIndex >= originalData.length - 1) {
|
|
241
|
+
const cutoffDate = originalData[Math.min(baseIndex, originalData.length - 1)][0];
|
|
242
|
+
cutoffTime = cutoffDate instanceof Date ? cutoffDate.getTime() : cutoffDate;
|
|
243
|
+
} else {
|
|
244
|
+
const currentDate = originalData[baseIndex][0];
|
|
245
|
+
const nextDate = originalData[baseIndex + 1][0];
|
|
246
|
+
const currentTime = currentDate instanceof Date ? currentDate.getTime() : currentDate;
|
|
247
|
+
const nextTime = nextDate instanceof Date ? nextDate.getTime() : nextDate;
|
|
248
|
+
cutoffTime = currentTime + fraction * (nextTime - currentTime);
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
cutoffTime = cutoffIndex;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (parameters.isPreview) {
|
|
255
|
+
const firstTime = originalData[0][0] instanceof Date ? originalData[0][0].getTime() : originalData[0][0];
|
|
256
|
+
const lastTime = originalData[originalData.length - 1][0] instanceof Date ?
|
|
257
|
+
originalData[originalData.length - 1][0].getTime() : originalData[originalData.length - 1][0];
|
|
258
|
+
const timeRatio = (cutoffTime - firstTime) / (lastTime - firstTime);
|
|
259
|
+
|
|
260
|
+
if (timeRatio < 0) {
|
|
261
|
+
this.draw(dataInRenderSpace, { ...parameters, renderCutoffGradient: false });
|
|
262
|
+
} else if (timeRatio > 1) {
|
|
263
|
+
const reducedOpacityColor = this.applyReducedOpacity(parameters.color, cutoffOpacity);
|
|
264
|
+
this.draw(dataInRenderSpace, {
|
|
265
|
+
...parameters,
|
|
266
|
+
color: reducedOpacityColor,
|
|
267
|
+
renderCutoffGradient: false
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
const gl = this._gl;
|
|
271
|
+
gl.enable(gl.BLEND);
|
|
272
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
273
|
+
|
|
274
|
+
this.draw(dataInRenderSpace, {
|
|
275
|
+
...parameters,
|
|
276
|
+
renderCutoffGradient: false,
|
|
277
|
+
cutoffX: timeRatio,
|
|
278
|
+
cutoffOpacity: cutoffOpacity || 0.35
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
if (!selectionBounds) {
|
|
283
|
+
this.draw(dataInRenderSpace, { ...parameters, renderCutoffGradient: false });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const visibleMinTime = selectionBounds.minX instanceof Date ? selectionBounds.minX.getTime() : selectionBounds.minX;
|
|
288
|
+
const visibleMaxTime = selectionBounds.maxX instanceof Date ? selectionBounds.maxX.getTime() : selectionBounds.maxX;
|
|
289
|
+
|
|
290
|
+
if (cutoffTime < visibleMinTime) {
|
|
291
|
+
this.draw(dataInRenderSpace, { ...parameters, renderCutoffGradient: false });
|
|
292
|
+
} else if (cutoffTime > visibleMaxTime) {
|
|
293
|
+
const reducedOpacityColor = this.applyReducedOpacity(parameters.color, cutoffOpacity);
|
|
294
|
+
this.draw(dataInRenderSpace, {
|
|
295
|
+
...parameters,
|
|
296
|
+
color: reducedOpacityColor,
|
|
297
|
+
renderCutoffGradient: false
|
|
298
|
+
});
|
|
299
|
+
} else {
|
|
300
|
+
const visibleCutoffRatio = (cutoffTime - visibleMinTime) / (visibleMaxTime - visibleMinTime);
|
|
301
|
+
|
|
302
|
+
const gl = this._gl;
|
|
303
|
+
gl.enable(gl.BLEND);
|
|
304
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
305
|
+
|
|
306
|
+
this.draw(dataInRenderSpace, {
|
|
307
|
+
...parameters,
|
|
308
|
+
renderCutoffGradient: false,
|
|
309
|
+
cutoffX: visibleCutoffRatio,
|
|
310
|
+
cutoffOpacity: cutoffOpacity || 0.35
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
125
315
|
}
|