@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.
Files changed (44) hide show
  1. package/dist/26b23c7580ea3345d644.wasm +0 -0
  2. package/dist/744.bundle.cjs +2 -1
  3. package/dist/744.bundle.cjs.map +1 -0
  4. package/dist/744.bundle.esm.js +2 -1
  5. package/dist/744.bundle.esm.js.map +1 -0
  6. package/dist/bundle.cjs +1 -1
  7. package/dist/bundle.cjs.map +1 -1
  8. package/dist/bundle.esm.js +1 -1
  9. package/dist/bundle.esm.js.map +1 -1
  10. package/package.json +1 -1
  11. package/readme.md +1 -1
  12. package/src/components/graph_body.jsx +41 -1
  13. package/src/components/range_graph.jsx +70 -22
  14. package/src/components/x_axis.jsx +139 -21
  15. package/src/components/y_axis.jsx +0 -5
  16. package/src/grapher.jsx +4 -3
  17. package/src/grapher.scss +84 -43
  18. package/src/helpers/color_to_vector.js +6 -1
  19. package/src/helpers/colors.js +82 -16
  20. package/src/helpers/custom_prop_types.js +2 -1
  21. package/src/helpers/place_grid.js +219 -0
  22. package/src/index.d.ts +150 -24
  23. package/src/renderer/create_gl_program.js +11 -3
  24. package/src/renderer/draw_area.js +1161 -102
  25. package/src/renderer/draw_background.js +5 -0
  26. package/src/renderer/draw_bars.js +100 -11
  27. package/src/renderer/draw_line.js +338 -38
  28. package/src/renderer/draw_zero_line.js +5 -0
  29. package/src/renderer/extract_vertices.js +1 -1
  30. package/src/renderer/graph_body_renderer.js +278 -17
  31. package/src/renderer/line.frag +16 -1
  32. package/src/renderer/line_program.js +200 -10
  33. package/src/renderer/shadow.frag +98 -0
  34. package/src/renderer/shadow.vert +20 -0
  35. package/src/renderer/shadow_program.js +479 -0
  36. package/src/rust/Cargo.lock +22 -31
  37. package/src/rust/pkg/grapher_rs.d.ts +6 -0
  38. package/src/rust/pkg/grapher_rs.js +5 -0
  39. package/src/rust/pkg/grapher_rs_bg.js +305 -0
  40. package/src/rust/pkg/grapher_rs_bg.wasm +0 -0
  41. package/src/rust/pkg/grapher_rs_bg.wasm.d.ts +11 -0
  42. package/src/rust/pkg/index.js +397 -0
  43. package/src/state/calculate_tooltip_state.js +6 -6
  44. 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 drawBackground from './draw_background.js';
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 inferType from '../state/infer_type';
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( '2d');
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
- // we can currently only render bars with the CPU
148
- this._context2d = this._context2d || this._canvas.getContext('2d');
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
- drawBars(getIndividualPoints(true), {
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
- drawArea(getIndividualPoints(true), inRenderSpace, {
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
- return;
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: typeof singleSeries.showIndividualPoints === 'boolean' ? singleSeries.showIndividualPoints : 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) {
@@ -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 = vec4(color);
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
- gl.uniform4f(gl.getUniformLocation(this._program, 'color'), ...colorToVector(parameters.color));
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
- gl.enableVertexAttribArray(0);
117
- gl.bindBuffer(gl.ARRAY_BUFFER, this._individualPointBuffer);
118
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(individualPoints.flat()), gl.STATIC_DRAW);
119
- gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
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
- gl.drawArrays(gl.POINTS, 0, individualPoints.length);
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
  }