@windborne/grapher 1.0.26 → 1.0.27

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windborne/grapher",
3
- "version": "1.0.26",
3
+ "version": "1.0.27",
4
4
  "description": "Graphing library",
5
5
  "main": "src/index.js",
6
6
  "module": "dist/bundle.esm.js",
@@ -335,6 +335,8 @@ export default class RangeGraph extends React.PureComponent {
335
335
  width={pixelMaxX - pixelMinX}
336
336
  height={totalHeight}
337
337
  className="target-selection-outline"
338
+ onMouseDown={this.startScroll}
339
+ onTouchStart={this.startScroll}
338
340
  />
339
341
  </g>
340
342
 
package/src/grapher.scss CHANGED
@@ -675,7 +675,8 @@
675
675
 
676
676
  .target-selection-outline {
677
677
  stroke: $range-graph-selection-outline-color;
678
- fill: none;
678
+ cursor: ew-resize;
679
+ fill: transparent;
679
680
  }
680
681
 
681
682
  .selection-bar-track {
@@ -109,7 +109,23 @@ export default class GraphBodyRenderer extends Eventable {
109
109
  }
110
110
 
111
111
  let cutoffIndex = -1;
112
- // cutoff time calculations for visible bounds-based approach
112
+ let cutoffTime = null;
113
+ let cutoffData = singleSeries.data;
114
+
115
+ const isObjectFormat = singleSeries.data && singleSeries.data.length > 0 &&
116
+ typeof singleSeries.data[0] === 'object' &&
117
+ !Array.isArray(singleSeries.data[0]);
118
+
119
+ if (isObjectFormat && singleSeries.cutoffTime) {
120
+ cutoffData = singleSeries.data.map(point => {
121
+ const xValue = point[singleSeries.xKey || 'x'];
122
+ const yValue = point[singleSeries.yKey || 'y'];
123
+
124
+ const convertedX = typeof xValue === 'string' ? new Date(xValue) : xValue;
125
+
126
+ return [convertedX, yValue];
127
+ });
128
+ }
113
129
 
114
130
  if (singleSeries.cutoffTime && singleSeries.data && singleSeries.data.length > 0) {
115
131
  let cutoffDate;
@@ -121,40 +137,48 @@ export default class GraphBodyRenderer extends Eventable {
121
137
  cutoffDate = singleSeries.cutoffTime;
122
138
  }
123
139
 
124
- // getting the ghost point
125
- const cutoffTime = cutoffDate instanceof Date ? cutoffDate.getTime() : cutoffDate;
140
+ cutoffTime = cutoffDate instanceof Date ? cutoffDate.getTime() : cutoffDate;
126
141
 
127
- for (let i = 0; i < singleSeries.data.length - 1; i++) {
128
- const currentPoint = singleSeries.data[i];
129
- const nextPoint = singleSeries.data[i + 1];
142
+ for (let i = 0; i < cutoffData.length - 1; i++) {
143
+ const currentPoint = cutoffData[i];
144
+ const nextPoint = cutoffData[i + 1];
130
145
 
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);
146
+ const currentTime = currentPoint[0] instanceof Date ? currentPoint[0].getTime() : currentPoint[0];
147
+ const nextTime = nextPoint[0] instanceof Date ? nextPoint[0].getTime() : nextPoint[0];
135
148
 
136
149
  if (currentTime <= cutoffTime && cutoffTime <= nextTime) {
137
- // interpolate exact position between these two points
138
150
  const timeRatio = (cutoffTime - currentTime) / (nextTime - currentTime);
139
151
  cutoffIndex = i + timeRatio;
140
152
  break;
141
153
  } else if (currentTime > cutoffTime) {
142
- // cutoff is before the first data point
143
154
  cutoffIndex = i;
144
155
  break;
145
156
  }
146
157
  }
147
158
 
148
- // cutoff is after all data points
149
159
  if (cutoffIndex === -1) {
150
- cutoffIndex = singleSeries.data.length - 1;
160
+ cutoffIndex = cutoffData.length - 1;
151
161
  }
152
-
153
-
154
- // Note: cutoffIndex is used for cutoff calculations but we no longer split data
155
162
  }
156
163
 
157
164
  const getIndividualPoints = (useDataSpace) => {
165
+ if (!useDataSpace && inRenderSpace && inRenderSpace.yValues) {
166
+ const individualPoints = [];
167
+ const { yValues, nullMask } = inRenderSpace;
168
+
169
+ for (let pixelX = 0; pixelX < yValues.length; pixelX++) {
170
+ if (nullMask[pixelX] === 0) {
171
+ individualPoints.push([pixelX, yValues[pixelX]]);
172
+ }
173
+ }
174
+
175
+ if (individualPoints.length === 0) {
176
+ return getIndividualPoints(true);
177
+ }
178
+
179
+ return individualPoints;
180
+ }
181
+
158
182
  if (!bounds) {
159
183
  bounds = singleSeries.axis.currentBounds;
160
184
  }
@@ -165,17 +189,33 @@ export default class GraphBodyRenderer extends Eventable {
165
189
  data = singleSeries.inDataSpace;
166
190
  }
167
191
 
168
- for (let [x, y] of data) {
169
- if (y === null) {
192
+ for (let i = 0; i < data.length; i++) {
193
+ let x, y;
194
+
195
+ if (Array.isArray(data[i])) {
196
+ [x, y] = data[i];
197
+ } else if (typeof data[i] === 'object' && data[i] !== null) {
198
+ x = data[i][singleSeries.xKey];
199
+ y = data[i][singleSeries.yKey];
200
+ } else {
201
+ continue;
202
+ }
203
+
204
+ if (y === null || y === undefined) {
170
205
  continue;
171
206
  }
172
207
 
208
+ let xValue = x instanceof Date ? x.getTime() : x;
209
+ let boundsMinX = bounds.minX instanceof Date ? bounds.minX.getTime() : bounds.minX;
210
+ let boundsMaxX = bounds.maxX instanceof Date ? bounds.maxX.getTime() : bounds.maxX;
211
+
173
212
  individualPoints.push([
174
- (x - bounds.minX) / (bounds.maxX - bounds.minX) * this._sizing.renderWidth,
213
+ (xValue - boundsMinX) / (boundsMaxX - boundsMinX) * this._sizing.renderWidth,
175
214
  (1.0 - (y - bounds.minY) / (bounds.maxY - bounds.minY)) * this._sizing.renderHeight
176
215
  ]);
177
216
  }
178
217
 
218
+
179
219
  return individualPoints;
180
220
  };
181
221
 
@@ -224,7 +264,6 @@ export default class GraphBodyRenderer extends Eventable {
224
264
  return;
225
265
  }
226
266
 
227
- //we still need a canvas context for cpu stuff
228
267
  if (!this._context2d) {
229
268
  this._context2d = this._canvas.getContext('2d', { willReadFrequently: false });
230
269
  }
@@ -235,7 +274,6 @@ export default class GraphBodyRenderer extends Eventable {
235
274
  }
236
275
 
237
276
  if (this._webgl) {
238
- // make sure we don't have any webgl stuff in the way before we get back to CPU rendering
239
277
  this._context.flush();
240
278
  }
241
279
 
@@ -275,7 +313,7 @@ export default class GraphBodyRenderer extends Eventable {
275
313
  if (singleSeries.cutoffTime) {
276
314
  barParams.cutoffIndex = cutoffIndex;
277
315
  barParams.cutoffOpacity = 0.35;
278
- barParams.originalData = singleSeries.data;
316
+ barParams.originalData = cutoffData;
279
317
  barParams.renderCutoffGradient = cutoffIndex >= 0;
280
318
 
281
319
  const selection = this === this._stateController.rangeGraphRenderer
@@ -301,11 +339,10 @@ export default class GraphBodyRenderer extends Eventable {
301
339
  inRenderSpaceAreaBottom
302
340
  };
303
341
 
304
- // add cutoff information for gradient area rendering
305
342
  if (singleSeries.cutoffTime) {
306
343
  areaParams.cutoffIndex = cutoffIndex;
307
344
  areaParams.cutoffOpacity = 0.35;
308
- areaParams.originalData = singleSeries.data;
345
+ areaParams.originalData = cutoffData;
309
346
  areaParams.renderCutoffGradient = cutoffIndex >= 0;
310
347
  areaParams.isPreview = this === this._stateController.rangeGraphRenderer;
311
348
 
@@ -344,7 +381,9 @@ export default class GraphBodyRenderer extends Eventable {
344
381
 
345
382
  let zero = singleSeries.zeroLineY === 'bottom' ?
346
383
  this._sizing.renderHeight :
347
- (1.0 - ((singleSeries.zeroLineY || 0) - bounds.minY) / (bounds.maxY - bounds.minY)) * this._sizing.renderHeight;
384
+ singleSeries.zeroLineY !== undefined ?
385
+ (1.0 - ((singleSeries.zeroLineY) - bounds.minY) / (bounds.maxY - bounds.minY)) * this._sizing.renderHeight :
386
+ this._sizing.renderHeight;
348
387
 
349
388
  const boundsChanged = !this._lastBounds ||
350
389
  bounds.minY !== this._lastBounds.minY ||
@@ -372,21 +411,20 @@ export default class GraphBodyRenderer extends Eventable {
372
411
  inRenderSpaceAreaBottom
373
412
  };
374
413
 
375
- // add cutoff information for gradient shadow rendering
376
414
  if (singleSeries.cutoffTime) {
377
415
  shadowParams.cutoffIndex = cutoffIndex;
378
416
  shadowParams.cutoffOpacity = 0.35;
379
- shadowParams.originalData = singleSeries.data;
417
+ shadowParams.originalData = cutoffData;
380
418
  shadowParams.renderCutoffGradient = cutoffIndex >= 0;
381
419
  shadowParams.isPreview = this === this._stateController.rangeGraphRenderer;
382
420
 
383
421
  const selection = this === this._stateController.rangeGraphRenderer
384
422
  ? this._stateController._bounds
385
423
  : (this._stateController._selection || this._stateController._bounds);
386
- shadowParams.selectionBounds = selection;
424
+ shadowParams.selectionBounds = selection || bounds;
387
425
  }
388
426
 
389
- this._shadowProgram.draw(getIndividualPoints(true), shadowParams);
427
+ this._shadowProgram.draw(getIndividualPoints(false), shadowParams);
390
428
 
391
429
  if (this._webgl) {
392
430
  const gl = this._context;
@@ -398,7 +436,6 @@ export default class GraphBodyRenderer extends Eventable {
398
436
 
399
437
  if (singleSeries.zeroLineWidth && singleSeries.zeroLineWidth > 0) {
400
438
  if (this._context2d) {
401
- // in non-webgl mode, use the existing 2d context
402
439
  this._context2d.save();
403
440
  this._context2d.strokeStyle = singleSeries.zeroLineColor || getColor(singleSeries.color, singleSeries.index, singleSeries.multigrapherSeriesIndex);
404
441
  this._context2d.lineWidth = singleSeries.zeroLineWidth;
@@ -410,7 +447,6 @@ export default class GraphBodyRenderer extends Eventable {
410
447
  this._context2d.stroke();
411
448
  this._context2d.restore();
412
449
  } else {
413
- // in webgl mode, we instead create an overlay 2d canvas for the zero line
414
450
  if (!this._zeroLineCanvas) {
415
451
  this._zeroLineCanvas = document.createElement('canvas');
416
452
  this._zeroLineCanvas.style.position = 'absolute';
@@ -460,20 +496,18 @@ export default class GraphBodyRenderer extends Eventable {
460
496
  return;
461
497
  }
462
498
 
463
- // Add cutoff information to drawParams for gradient line rendering
464
499
  if (singleSeries.cutoffTime) {
465
500
  drawParams.cutoffIndex = cutoffIndex;
466
501
  drawParams.cutoffOpacity = 0.35;
467
- drawParams.originalData = singleSeries.data;
468
- drawParams.renderCutoffGradient = cutoffIndex >= 0; // Only render cutoff if valid cutoff
502
+ drawParams.originalData = cutoffData;
503
+ drawParams.renderCutoffGradient = cutoffIndex >= 0;
469
504
  drawParams.currentBounds = bounds;
470
- drawParams.isPreview = this === this._stateController.rangeGraphRenderer; // Flag for preview rendering
505
+ drawParams.isPreview = this === this._stateController.rangeGraphRenderer;
471
506
 
472
- // Always set selectionBounds with fallback
473
507
  const selection = this === this._stateController.rangeGraphRenderer
474
508
  ? this._stateController._bounds
475
509
  : (this._stateController._selection || this._stateController._bounds);
476
- drawParams.selectionBounds = selection;
510
+ drawParams.selectionBounds = selection || bounds;
477
511
  }
478
512
 
479
513
  if (this._webgl) {
@@ -108,7 +108,7 @@ export default class LineProgram {
108
108
  gl.uniform4f(gl.getUniformLocation(this._program, 'shadowColor'), ...colorToVector(shadowColor));
109
109
 
110
110
  const cutoffX = parameters.cutoffX !== undefined ? parameters.cutoffX : -1.0; // Use parameter or disable
111
- const cutoffOpacity = parameters.cutoffOpacity !== undefined ? parameters.cutoffOpacity : 1.0;
111
+ const cutoffOpacity = parameters.cutoffOpacity !== undefined ? parameters.cutoffOpacity : 0.35;
112
112
 
113
113
  gl.uniform1f(gl.getUniformLocation(this._program, 'cutoffX'), cutoffX);
114
114
  gl.uniform1f(gl.getUniformLocation(this._program, 'cutoffOpacity'), cutoffOpacity);
@@ -260,7 +260,7 @@ export default class LineProgram {
260
260
  if (timeRatio < 0) {
261
261
  this.draw(dataInRenderSpace, { ...parameters, renderCutoffGradient: false });
262
262
  } else if (timeRatio > 1) {
263
- const reducedOpacityColor = this.applyReducedOpacity(parameters.color, cutoffOpacity);
263
+ const reducedOpacityColor = applyReducedOpacity(parameters.color, cutoffOpacity);
264
264
  this.draw(dataInRenderSpace, {
265
265
  ...parameters,
266
266
  color: reducedOpacityColor,
@@ -290,7 +290,7 @@ export default class LineProgram {
290
290
  if (cutoffTime < visibleMinTime) {
291
291
  this.draw(dataInRenderSpace, { ...parameters, renderCutoffGradient: false });
292
292
  } else if (cutoffTime > visibleMaxTime) {
293
- const reducedOpacityColor = this.applyReducedOpacity(parameters.color, cutoffOpacity);
293
+ const reducedOpacityColor = applyReducedOpacity(parameters.color, cutoffOpacity);
294
294
  this.draw(dataInRenderSpace, {
295
295
  ...parameters,
296
296
  color: reducedOpacityColor,
@@ -189,6 +189,7 @@ export default class ShadowProgram {
189
189
  * @param {Object} params - Rendering parameters
190
190
  */
191
191
  draw(individualPoints, params) {
192
+
192
193
  if (!individualPoints || individualPoints.length < 2) {
193
194
  return;
194
195
  }
@@ -211,6 +212,7 @@ export default class ShadowProgram {
211
212
  const trapezoids = [];
212
213
  const { zero, inRenderSpaceAreaBottom } = params;
213
214
 
215
+
214
216
  for (let i = 0; i < individualPoints.length - 1; i++) {
215
217
  const [x1, y1] = individualPoints[i];
216
218
  const [x2, y2] = individualPoints[i + 1];
@@ -256,6 +258,7 @@ export default class ShadowProgram {
256
258
  }
257
259
  }
258
260
 
261
+
259
262
  if (trapezoids.length === 0) {
260
263
  return;
261
264
  }
@@ -329,6 +332,13 @@ export default class ShadowProgram {
329
332
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, geometry.indices, gl.STATIC_DRAW);
330
333
 
331
334
  gl.drawElements(gl.TRIANGLES, geometry.indices.length, gl.UNSIGNED_INT, 0);
335
+
336
+ const error = gl.getError();
337
+ if (error !== gl.NO_ERROR) {
338
+ console.error('WebGL error in shadow rendering:', error);
339
+ } else {
340
+ //he he he haw
341
+ }
332
342
  }
333
343
 
334
344
  /**
@@ -337,45 +347,56 @@ export default class ShadowProgram {
337
347
  * @param {Object} params - Rendering parameters with cutoff info
338
348
  */
339
349
  drawShadowWithCutoff(individualPoints, params) {
350
+
340
351
  const { cutoffIndex, cutoffOpacity, originalData, selectionBounds, zero } =
341
352
  params;
342
353
 
343
354
  this._lastIndividualPoints = null;
344
355
  this._lastParams = null;
345
356
 
357
+ // All cutoff data is now in tuple format [x, y] from graph_body_renderer
346
358
  let cutoffTime;
347
- if (typeof originalData[0] === "object" && originalData[0].length === 2) {
359
+
360
+ if (Array.isArray(originalData[0]) && originalData[0].length === 2) {
348
361
  const baseIndex = Math.floor(cutoffIndex);
349
362
  const fraction = cutoffIndex - baseIndex;
350
363
 
351
364
  if (fraction === 0 || baseIndex >= originalData.length - 1) {
352
- const cutoffDate =
353
- originalData[Math.min(baseIndex, originalData.length - 1)][0];
354
- cutoffTime =
355
- cutoffDate instanceof Date ? cutoffDate.getTime() : cutoffDate;
365
+ const cutoffItem = originalData[Math.min(baseIndex, originalData.length - 1)];
366
+ const cutoffDate = cutoffItem[0];
367
+ cutoffTime = cutoffDate instanceof Date ? cutoffDate.getTime() : cutoffDate;
356
368
  } else {
357
- const currentDate = originalData[baseIndex][0];
358
- const nextDate = originalData[baseIndex + 1][0];
359
- const currentTime =
360
- currentDate instanceof Date ? currentDate.getTime() : currentDate;
361
- const nextTime =
362
- nextDate instanceof Date ? nextDate.getTime() : nextDate;
369
+ const currentItem = originalData[baseIndex];
370
+ const nextItem = originalData[baseIndex + 1];
371
+ const currentDate = currentItem[0];
372
+ const nextDate = nextItem[0];
373
+ const currentTime = currentDate instanceof Date ? currentDate.getTime() : currentDate;
374
+ const nextTime = nextDate instanceof Date ? nextDate.getTime() : nextDate;
363
375
  cutoffTime = currentTime + fraction * (nextTime - currentTime);
364
376
  }
365
377
  } else {
366
378
  cutoffTime = cutoffIndex;
367
379
  }
368
380
 
369
- const firstTime =
370
- originalData[0][0] instanceof Date
371
- ? originalData[0][0].getTime()
372
- : originalData[0][0];
373
- const lastTime =
374
- originalData[originalData.length - 1][0] instanceof Date
375
- ? originalData[originalData.length - 1][0].getTime()
376
- : originalData[originalData.length - 1][0];
377
-
378
- const timeRatio = (cutoffTime - firstTime) / (lastTime - firstTime);
381
+ const visibleBounds = params.selectionBounds;
382
+ let firstTime, lastTime;
383
+
384
+ if (visibleBounds && visibleBounds.minX !== undefined && visibleBounds.maxX !== undefined) {
385
+ firstTime = visibleBounds.minX instanceof Date ? visibleBounds.minX.getTime() : visibleBounds.minX;
386
+ lastTime = visibleBounds.maxX instanceof Date ? visibleBounds.maxX.getTime() : visibleBounds.maxX;
387
+ } else {
388
+ const firstItem = originalData[0];
389
+ const lastItem = originalData[originalData.length - 1];
390
+ const firstX = firstItem[0];
391
+ const lastX = lastItem[0];
392
+
393
+ firstTime = firstX instanceof Date ? firstX.getTime() : firstX;
394
+ lastTime = lastX instanceof Date ? lastX.getTime() : lastX;
395
+ }
396
+
397
+ const timeDiff = cutoffTime - firstTime;
398
+ const totalTime = lastTime - firstTime;
399
+ const timeRatio = timeDiff / totalTime;
379
400
 
380
401
  if (timeRatio < 0) {
381
402
  this.draw(individualPoints, { ...params, renderCutoffGradient: false });
@@ -392,7 +413,7 @@ export default class ShadowProgram {
392
413
  } else {
393
414
  this.drawSplitShadowTrapezoids(
394
415
  individualPoints,
395
- params,
416
+ { ...params, selectionBounds: params.selectionBounds },
396
417
  timeRatio,
397
418
  cutoffTime
398
419
  );
@@ -407,41 +428,41 @@ export default class ShadowProgram {
407
428
  * @param {number} cutoffTime - Cutoff timestamp
408
429
  */
409
430
  drawSplitShadowTrapezoids(individualPoints, params, timeRatio, cutoffTime) {
410
- const { zero, cutoffOpacity } = params;
431
+ const { zero, cutoffOpacity, selectionBounds } = params;
411
432
  const gl = this._gl;
412
433
 
413
- const renderCutoffIndex = timeRatio * (individualPoints.length - 1);
414
- const splitIndex = Math.floor(renderCutoffIndex);
415
- const fraction = renderCutoffIndex - splitIndex;
416
-
417
- let ghostPoint = null;
418
- if (
419
- splitIndex >= 0 &&
420
- splitIndex < individualPoints.length - 1 &&
421
- fraction > 0
422
- ) {
423
- const beforePoint = individualPoints[splitIndex];
424
- const afterPoint = individualPoints[splitIndex + 1];
425
- ghostPoint = [
426
- beforePoint[0] + fraction * (afterPoint[0] - beforePoint[0]),
427
- beforePoint[1] + fraction * (afterPoint[1] - beforePoint[1]),
428
- ];
429
- }
434
+ const renderWidth = gl.canvas.width;
435
+ const cutoffPixelX = timeRatio * renderWidth;
430
436
 
431
437
  const preCutoffPoints = [];
432
- for (let i = 0; i <= splitIndex && i < individualPoints.length; i++) {
433
- preCutoffPoints.push(individualPoints[i]);
434
- }
435
- if (ghostPoint && splitIndex < individualPoints.length - 1) {
436
- preCutoffPoints.push(ghostPoint);
437
- }
438
-
439
438
  const postCutoffPoints = [];
440
- if (ghostPoint && splitIndex < individualPoints.length - 1) {
441
- postCutoffPoints.push(ghostPoint);
439
+
440
+ for (let i = 0; i < individualPoints.length; i++) {
441
+ const [pixelX, pixelY] = individualPoints[i];
442
+
443
+ if (pixelX < cutoffPixelX) {
444
+ preCutoffPoints.push(individualPoints[i]);
445
+ } else {
446
+ postCutoffPoints.push(individualPoints[i]);
447
+ }
442
448
  }
443
- for (let i = splitIndex + 1; i < individualPoints.length; i++) {
444
- postCutoffPoints.push(individualPoints[i]);
449
+
450
+ let ghostPoint = null;
451
+ if (preCutoffPoints.length > 0 && postCutoffPoints.length > 0) {
452
+ const lastPrePoint = preCutoffPoints[preCutoffPoints.length - 1];
453
+ const firstPostPoint = postCutoffPoints[0];
454
+
455
+ const [x1, y1] = lastPrePoint;
456
+ const [x2, y2] = firstPostPoint;
457
+
458
+ if (x2 !== x1) {
459
+ const interpolationRatio = (cutoffPixelX - x1) / (x2 - x1);
460
+ const ghostY = y1 + interpolationRatio * (y2 - y1);
461
+ ghostPoint = [cutoffPixelX, ghostY];
462
+
463
+ preCutoffPoints.push(ghostPoint);
464
+ postCutoffPoints.unshift(ghostPoint);
465
+ }
445
466
  }
446
467
 
447
468
  if (preCutoffPoints.length >= 2) {
@@ -467,6 +488,7 @@ export default class ShadowProgram {
467
488
  gradient: translucentGradient,
468
489
  renderCutoffGradient: false,
469
490
  });
491
+ } else {
470
492
  }
471
493
 
472
494
  if (postCutoffPoints.length >= 2) {
@@ -474,6 +496,7 @@ export default class ShadowProgram {
474
496
  ...params,
475
497
  renderCutoffGradient: false,
476
498
  });
499
+ } else {
477
500
  }
478
501
  }
479
502
  }