@windborne/grapher 1.0.32 → 1.0.33
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 +1 -1
- package/readme.md +4 -2
- package/src/helpers/custom_prop_types.js +3 -2
- package/src/index.d.ts +1 -0
- package/src/renderer/draw_area.js +64 -9
- package/src/renderer/draw_line.js +69 -8
- package/src/renderer/graph_body_renderer.js +59 -3
- package/src/renderer/shadow_program.js +106 -57
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -149,8 +149,10 @@ Grapher supports multiple data formats within a series:
|
|
|
149
149
|
| background | `object` | ✗ | Background configuration. |
|
|
150
150
|
| hideFromKey | `boolean` | ✗ | Whether to hide this series from the legend. |
|
|
151
151
|
| showIndividualPoints | `boolean` | ✗ | Whether to show individual data points. |
|
|
152
|
-
|
|
|
153
|
-
|
|
|
152
|
+
| minPointSpacing | `number` | ✗ | Minimum pixel spacing between individual points to prevent overlap. |
|
|
153
|
+
| negativeColor | `string` | ✗ | Color for lines and points with negative values (below zero). |
|
|
154
|
+
| gradient | `string[] \| [number, string][]` | ✗ | Gradient configuration for area and shadow fills. Array of colors or [position, color] pairs. |
|
|
155
|
+
| negativeGradient | `string[] \| [number, string][]` | ✗ | Separate gradient for negative values (below zero) in area and shadow charts. Same format as gradient. |
|
|
154
156
|
| zeroLineWidth | `number` | ✗ | Width of the zero line, only applies to bar and area rendering. |
|
|
155
157
|
| zeroLineColor | `string` | ✗ | Color of the zero line, only applies to bar and area rendering. |
|
|
156
158
|
| zeroLineY | `number \| 'bottom'` | ✗ | Y-coordinate of the zero line, only applies to bar and area rendering. Defaults to zero; may also be the string "bottom". |
|
|
@@ -67,8 +67,9 @@ const SingleSeries = PropTypes.shape({
|
|
|
67
67
|
showIndividualPoints: PropTypes.bool,
|
|
68
68
|
minPointSpacing: PropTypes.number,
|
|
69
69
|
rendering: PropTypes.oneOf(['line', 'bar', 'area', 'shadow']), // defaults to line
|
|
70
|
-
negativeColor: PropTypes.string, //
|
|
71
|
-
gradient: PropTypes.array, // only applies to area
|
|
70
|
+
negativeColor: PropTypes.string, // colors lines and points below zero for all renderings
|
|
71
|
+
gradient: PropTypes.array, // only applies to area and shadow
|
|
72
|
+
negativeGradient: PropTypes.array, // gradient for values below zero (area and shadow)
|
|
72
73
|
zeroLineWidth: PropTypes.number, // only applies to bar and area
|
|
73
74
|
zeroLineColor: PropTypes.string, // only applies to bar and area
|
|
74
75
|
zeroLineY: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), // only applies to bar and area
|
package/src/index.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface SeriesData {
|
|
|
27
27
|
minPointSpacing?: number;
|
|
28
28
|
rendering?: 'line' | 'bar' | 'area' | 'shadow';
|
|
29
29
|
negativeColor?: string;
|
|
30
|
+
negativeGradient?: string[] | [number, string][];
|
|
30
31
|
zeroLineWidth?: number;
|
|
31
32
|
zeroLineColor?: string;
|
|
32
33
|
zeroLineY?: number | string;
|
|
@@ -34,6 +34,7 @@ export default function drawArea(
|
|
|
34
34
|
zero,
|
|
35
35
|
hasNegatives,
|
|
36
36
|
gradient,
|
|
37
|
+
negativeGradient,
|
|
37
38
|
zeroColor,
|
|
38
39
|
zeroWidth,
|
|
39
40
|
showIndividualPoints,
|
|
@@ -95,9 +96,10 @@ export default function drawArea(
|
|
|
95
96
|
// we want to draw a polygon with a flat line at areaBottom, and then follows the shape of the data
|
|
96
97
|
const areaBottom = hasNegatives ? zero : sizing.renderHeight;
|
|
97
98
|
|
|
98
|
-
const
|
|
99
|
+
const shouldSplitAreaPaths = hasNegatives && negativeGradient;
|
|
100
|
+
const areaPaths = pathsFrom(dataInRenderSpace, shouldSplitAreaPaths ? { splitAtY: zero } : undefined);
|
|
99
101
|
const areaBottomPaths =
|
|
100
|
-
inRenderSpaceAreaBottom && pathsFrom(inRenderSpaceAreaBottom);
|
|
102
|
+
inRenderSpaceAreaBottom && pathsFrom(inRenderSpaceAreaBottom, shouldSplitAreaPaths ? { splitAtY: zero } : undefined);
|
|
101
103
|
|
|
102
104
|
const linePaths = pathsFrom(dataInRenderSpace, {
|
|
103
105
|
splitAtY: zero,
|
|
@@ -139,6 +141,56 @@ export default function drawArea(
|
|
|
139
141
|
for (let pathI = 0; pathI < areaPaths.length; pathI++) {
|
|
140
142
|
const path = areaPaths[pathI];
|
|
141
143
|
const areaBottomPath = areaBottomPaths && areaBottomPaths[pathI];
|
|
144
|
+
|
|
145
|
+
// Determine if this path segment is positive or negative for gradient application
|
|
146
|
+
let isPositive = true;
|
|
147
|
+
if (hasNegatives && negativeGradient && path.length > 0) {
|
|
148
|
+
// Check the average Y position of the path to determine if it's above or below zero
|
|
149
|
+
let sumY = 0;
|
|
150
|
+
for (let i = 0; i < path.length; i++) {
|
|
151
|
+
sumY += path[i][1];
|
|
152
|
+
}
|
|
153
|
+
const avgY = sumY / path.length;
|
|
154
|
+
isPositive = avgY <= zero; // In screen coordinates, smaller Y is higher up (positive)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Set the fill style based on whether this is a positive or negative segment
|
|
158
|
+
if (hasNegatives && negativeGradient) {
|
|
159
|
+
if (isPositive) {
|
|
160
|
+
// Use the original gradient or color for positive areas
|
|
161
|
+
if (gradient && gradient.length >= 2) {
|
|
162
|
+
const globalGradient = context.createLinearGradient(0, 0, 0, sizing.renderHeight);
|
|
163
|
+
for (let i = 0; i < gradient.length; i++) {
|
|
164
|
+
const value = gradient[i];
|
|
165
|
+
if (Array.isArray(value)) {
|
|
166
|
+
globalGradient.addColorStop(value[0], value[1]);
|
|
167
|
+
} else {
|
|
168
|
+
globalGradient.addColorStop(i / (gradient.length - 1), value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
context.fillStyle = globalGradient;
|
|
172
|
+
} else {
|
|
173
|
+
context.fillStyle = color;
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
// Use negativeGradient for negative areas
|
|
177
|
+
if (negativeGradient.length >= 2) {
|
|
178
|
+
const negGradient = context.createLinearGradient(0, 0, 0, sizing.renderHeight);
|
|
179
|
+
for (let i = 0; i < negativeGradient.length; i++) {
|
|
180
|
+
const value = negativeGradient[i];
|
|
181
|
+
if (Array.isArray(value)) {
|
|
182
|
+
negGradient.addColorStop(value[0], value[1]);
|
|
183
|
+
} else {
|
|
184
|
+
negGradient.addColorStop(i / (negativeGradient.length - 1), value);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
context.fillStyle = negGradient;
|
|
188
|
+
} else {
|
|
189
|
+
context.fillStyle = negativeColor || color;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
142
194
|
context.beginPath();
|
|
143
195
|
|
|
144
196
|
const [firstX, _startY] = path[0];
|
|
@@ -180,7 +232,7 @@ export default function drawArea(
|
|
|
180
232
|
continue;
|
|
181
233
|
}
|
|
182
234
|
|
|
183
|
-
if (hasNegatives) {
|
|
235
|
+
if (hasNegatives && negativeColor) {
|
|
184
236
|
let positive = true;
|
|
185
237
|
if (path.length >= 2) {
|
|
186
238
|
positive = path[1][1] <= zero;
|
|
@@ -193,6 +245,8 @@ export default function drawArea(
|
|
|
193
245
|
} else {
|
|
194
246
|
context.strokeStyle = negativeColor;
|
|
195
247
|
}
|
|
248
|
+
} else {
|
|
249
|
+
context.strokeStyle = color;
|
|
196
250
|
}
|
|
197
251
|
|
|
198
252
|
context.beginPath();
|
|
@@ -222,8 +276,6 @@ export default function drawArea(
|
|
|
222
276
|
}
|
|
223
277
|
|
|
224
278
|
if (showIndividualPoints && !renderCutoffGradient) {
|
|
225
|
-
context.fillStyle = color;
|
|
226
|
-
|
|
227
279
|
// Apply point spacing for individual point circles only
|
|
228
280
|
function applyPointSpacing(points, minSpacing) {
|
|
229
281
|
if (!minSpacing || points.length <= 1) {
|
|
@@ -246,16 +298,19 @@ export default function drawArea(
|
|
|
246
298
|
|
|
247
299
|
const pointsToRender = applyPointSpacing(individualPoints, minPointSpacing);
|
|
248
300
|
for (let [x, y] of pointsToRender) {
|
|
301
|
+
// Determine the color for this point
|
|
302
|
+
let pointColor = color;
|
|
249
303
|
if (negativeColor && hasNegatives) {
|
|
250
304
|
if (y === zero && zeroColor) {
|
|
251
|
-
|
|
305
|
+
pointColor = zeroColor;
|
|
252
306
|
} else if (y < zero) {
|
|
253
|
-
|
|
307
|
+
pointColor = color;
|
|
254
308
|
} else {
|
|
255
|
-
|
|
309
|
+
pointColor = negativeColor;
|
|
256
310
|
}
|
|
257
311
|
}
|
|
258
|
-
|
|
312
|
+
|
|
313
|
+
context.fillStyle = pointColor;
|
|
259
314
|
context.beginPath();
|
|
260
315
|
context.arc(x, y, pointRadius || 8, 0, 2 * Math.PI, false);
|
|
261
316
|
context.fill();
|
|
@@ -22,7 +22,7 @@ function applyPointSpacing(points, minSpacing) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export default function drawLine(dataInRenderSpace, {
|
|
25
|
-
color, width=1, context, shadowColor='black', shadowBlur=5, dashed=false, dashPattern=null, highlighted=false, showIndividualPoints=false, pointRadius, minPointSpacing, getIndividualPoints, getRanges, cutoffIndex, cutoffOpacity, originalData, renderCutoffGradient, currentBounds, selectionBounds, rendering, isPreview
|
|
25
|
+
color, width=1, context, shadowColor='black', shadowBlur=5, dashed=false, dashPattern=null, highlighted=false, showIndividualPoints=false, pointRadius, minPointSpacing, getIndividualPoints, getRanges, cutoffIndex, cutoffOpacity, originalData, renderCutoffGradient, currentBounds, selectionBounds, rendering, isPreview, negativeColor, hasNegatives, zero, zeroColor
|
|
26
26
|
}) {
|
|
27
27
|
if (!context) {
|
|
28
28
|
console.error("Canvas context is null in drawLine");
|
|
@@ -45,7 +45,8 @@ export default function drawLine(dataInRenderSpace, {
|
|
|
45
45
|
context.setLineDash([]);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
// Split paths at zero line if negativeColor is specified
|
|
49
|
+
const paths = pathsFrom(dataInRenderSpace, hasNegatives && negativeColor ? { splitAtY: zero } : undefined);
|
|
49
50
|
|
|
50
51
|
for (let path of paths) {
|
|
51
52
|
if (renderCutoffGradient && cutoffIndex !== undefined && originalData) {
|
|
@@ -279,6 +280,18 @@ export default function drawLine(dataInRenderSpace, {
|
|
|
279
280
|
}
|
|
280
281
|
}
|
|
281
282
|
} else {
|
|
283
|
+
if (hasNegatives && negativeColor) {
|
|
284
|
+
let positive = true;
|
|
285
|
+
if (path.length >= 2) {
|
|
286
|
+
positive = path[1][1] <= zero;
|
|
287
|
+
} else if (path.length > 0) {
|
|
288
|
+
positive = path[0][1] <= zero;
|
|
289
|
+
}
|
|
290
|
+
context.strokeStyle = positive ? color : negativeColor;
|
|
291
|
+
} else {
|
|
292
|
+
context.strokeStyle = color;
|
|
293
|
+
}
|
|
294
|
+
|
|
282
295
|
context.beginPath();
|
|
283
296
|
|
|
284
297
|
for (let i = 0; i < path.length; i++) {
|
|
@@ -377,11 +390,22 @@ export default function drawLine(dataInRenderSpace, {
|
|
|
377
390
|
isBeforeCutoff = x < cutoffPixelX;
|
|
378
391
|
}
|
|
379
392
|
|
|
393
|
+
let pointColor = color;
|
|
394
|
+
if (negativeColor && hasNegatives) {
|
|
395
|
+
if (y === zero && zeroColor) {
|
|
396
|
+
pointColor = zeroColor;
|
|
397
|
+
} else if (y < zero) {
|
|
398
|
+
pointColor = color;
|
|
399
|
+
} else {
|
|
400
|
+
pointColor = negativeColor;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
380
404
|
if (isBeforeCutoff) {
|
|
381
|
-
const reducedOpacityColor = applyReducedOpacity(
|
|
405
|
+
const reducedOpacityColor = applyReducedOpacity(pointColor, cutoffOpacity);
|
|
382
406
|
context.fillStyle = reducedOpacityColor;
|
|
383
407
|
} else {
|
|
384
|
-
context.fillStyle =
|
|
408
|
+
context.fillStyle = pointColor;
|
|
385
409
|
}
|
|
386
410
|
|
|
387
411
|
context.beginPath();
|
|
@@ -392,7 +416,20 @@ export default function drawLine(dataInRenderSpace, {
|
|
|
392
416
|
const spacedPoints = applyPointSpacing(individualPoints, minPointSpacing);
|
|
393
417
|
for (let i = 0; i < spacedPoints.length; i++) {
|
|
394
418
|
const [x, y] = spacedPoints[i];
|
|
395
|
-
|
|
419
|
+
|
|
420
|
+
// Determine point color based on position relative to zero
|
|
421
|
+
let pointColor = color;
|
|
422
|
+
if (negativeColor && hasNegatives) {
|
|
423
|
+
if (y === zero && zeroColor) {
|
|
424
|
+
pointColor = zeroColor;
|
|
425
|
+
} else if (y < zero) {
|
|
426
|
+
pointColor = color;
|
|
427
|
+
} else {
|
|
428
|
+
pointColor = negativeColor;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
context.fillStyle = pointColor;
|
|
396
433
|
context.beginPath();
|
|
397
434
|
context.arc(x, y, pointRadius || 8, 0, 2 * Math.PI, false);
|
|
398
435
|
context.fill();
|
|
@@ -416,11 +453,22 @@ export default function drawLine(dataInRenderSpace, {
|
|
|
416
453
|
isBeforeCutoff = x < cutoffPixelX;
|
|
417
454
|
}
|
|
418
455
|
|
|
456
|
+
let pointColor = color;
|
|
457
|
+
if (negativeColor && hasNegatives) {
|
|
458
|
+
if (y === zero && zeroColor) {
|
|
459
|
+
pointColor = zeroColor;
|
|
460
|
+
} else if (y < zero) {
|
|
461
|
+
pointColor = color;
|
|
462
|
+
} else {
|
|
463
|
+
pointColor = negativeColor;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
419
467
|
if (isBeforeCutoff) {
|
|
420
|
-
const reducedOpacityColor = applyReducedOpacity(
|
|
468
|
+
const reducedOpacityColor = applyReducedOpacity(pointColor, cutoffOpacity);
|
|
421
469
|
context.fillStyle = reducedOpacityColor;
|
|
422
470
|
} else {
|
|
423
|
-
context.fillStyle =
|
|
471
|
+
context.fillStyle = pointColor;
|
|
424
472
|
}
|
|
425
473
|
|
|
426
474
|
context.beginPath();
|
|
@@ -432,7 +480,20 @@ export default function drawLine(dataInRenderSpace, {
|
|
|
432
480
|
const spacedPoints = applyPointSpacing(individualPoints, minPointSpacing);
|
|
433
481
|
for (let i = 0; i < spacedPoints.length; i++) {
|
|
434
482
|
const [x, y] = spacedPoints[i];
|
|
435
|
-
|
|
483
|
+
|
|
484
|
+
// Determine point color based on position relative to zero
|
|
485
|
+
let pointColor = color;
|
|
486
|
+
if (negativeColor && hasNegatives) {
|
|
487
|
+
if (y === zero && zeroColor) {
|
|
488
|
+
pointColor = zeroColor;
|
|
489
|
+
} else if (y < zero) {
|
|
490
|
+
pointColor = color;
|
|
491
|
+
} else {
|
|
492
|
+
pointColor = negativeColor;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
context.fillStyle = pointColor;
|
|
436
497
|
context.beginPath();
|
|
437
498
|
context.arc(x, y, pointRadius || 8, 0, 2 * Math.PI, false);
|
|
438
499
|
context.fill();
|
|
@@ -370,6 +370,7 @@ export default class GraphBodyRenderer extends Eventable {
|
|
|
370
370
|
...commonCPUParams,
|
|
371
371
|
showIndividualPoints: typeof singleSeries.showIndividualPoints === 'boolean' ? singleSeries.showIndividualPoints : showIndividualPoints,
|
|
372
372
|
gradient: singleSeries.gradient,
|
|
373
|
+
negativeGradient: singleSeries.negativeGradient,
|
|
373
374
|
pointRadius: singleSeries.pointRadius,
|
|
374
375
|
minPointSpacing: singleSeries.minPointSpacing,
|
|
375
376
|
highlighted,
|
|
@@ -451,9 +452,12 @@ export default class GraphBodyRenderer extends Eventable {
|
|
|
451
452
|
}
|
|
452
453
|
|
|
453
454
|
const shadowColor = getColor(singleSeries.color, singleSeries.index, singleSeries.multigrapherSeriesIndex);
|
|
455
|
+
const hasNegatives = !!singleSeries.inDataSpace.find((tuple) => tuple[1] < 0);
|
|
454
456
|
let shadowParams = {
|
|
455
457
|
color: shadowColor,
|
|
456
458
|
gradient: singleSeries.gradient || createDefaultGradient(shadowColor),
|
|
459
|
+
negativeGradient: singleSeries.negativeGradient,
|
|
460
|
+
hasNegatives,
|
|
457
461
|
zero,
|
|
458
462
|
sizing: this._sizing,
|
|
459
463
|
inRenderSpaceAreaBottom
|
|
@@ -523,9 +527,54 @@ export default class GraphBodyRenderer extends Eventable {
|
|
|
523
527
|
|
|
524
528
|
const shouldShowIndividualPoints = typeof singleSeries.showIndividualPoints === 'boolean' ? singleSeries.showIndividualPoints : showIndividualPoints;
|
|
525
529
|
|
|
530
|
+
let zero;
|
|
531
|
+
if (singleSeries.zeroLineY === 'bottom') {
|
|
532
|
+
zero = this._sizing.renderHeight;
|
|
533
|
+
} else if (singleSeries.zeroLineY !== undefined) {
|
|
534
|
+
zero = (1.0 - ((singleSeries.zeroLineY) - bounds.minY) / (bounds.maxY - bounds.minY)) * this._sizing.renderHeight;
|
|
535
|
+
} else {
|
|
536
|
+
if (bounds.minY <= 0 && bounds.maxY >= 0) {
|
|
537
|
+
zero = (1.0 - (0 - bounds.minY) / (bounds.maxY - bounds.minY)) * this._sizing.renderHeight;
|
|
538
|
+
} else {
|
|
539
|
+
zero = this._sizing.renderHeight;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const hasNegatives = !!singleSeries.inDataSpace.find((tuple) => tuple[1] < 0);
|
|
543
|
+
|
|
544
|
+
// For WebGL shadow rendering, we need a separate 2D canvas overlay for lines/points
|
|
545
|
+
// since WebGL and 2D contexts can't coexist on the same canvas
|
|
546
|
+
let drawContext = this._context;
|
|
547
|
+
|
|
548
|
+
if (this._webgl && singleSeries.rendering === 'shadow' && (width > 0 || shouldShowIndividualPoints)) {
|
|
549
|
+
// Only create overlay if we're actually drawing lines or points
|
|
550
|
+
if (!this._overlayCanvas) {
|
|
551
|
+
this._overlayCanvas = document.createElement('canvas');
|
|
552
|
+
this._overlayCanvas.style.position = 'absolute';
|
|
553
|
+
this._overlayCanvas.style.top = '0';
|
|
554
|
+
this._overlayCanvas.style.left = '0';
|
|
555
|
+
this._overlayCanvas.style.pointerEvents = 'none';
|
|
556
|
+
this._overlayContext = this._overlayCanvas.getContext('2d');
|
|
557
|
+
this._canvas.parentNode.insertBefore(this._overlayCanvas, this._canvas.nextSibling);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Size the overlay canvas to match the main canvas
|
|
561
|
+
this._overlayCanvas.width = this._canvas.width;
|
|
562
|
+
this._overlayCanvas.height = this._canvas.height;
|
|
563
|
+
this._overlayCanvas.style.width = this._canvas.style.width;
|
|
564
|
+
this._overlayCanvas.style.height = this._canvas.style.height;
|
|
565
|
+
|
|
566
|
+
// Clear the overlay before drawing
|
|
567
|
+
this._overlayContext.clearRect(0, 0, this._overlayCanvas.width, this._overlayCanvas.height);
|
|
568
|
+
|
|
569
|
+
drawContext = this._overlayContext;
|
|
570
|
+
} else if (this._context2d) {
|
|
571
|
+
// For non-WebGL or non-shadow charts with 2D context
|
|
572
|
+
drawContext = this._context2d;
|
|
573
|
+
}
|
|
574
|
+
|
|
526
575
|
const drawParams = {
|
|
527
576
|
color: getColor(singleSeries.color, singleSeries.index, singleSeries.multigrapherSeriesIndex),
|
|
528
|
-
context:
|
|
577
|
+
context: drawContext,
|
|
529
578
|
width: width || singleSeries.width || defaultLineWidth,
|
|
530
579
|
shadowColor,
|
|
531
580
|
shadowBlur,
|
|
@@ -537,7 +586,11 @@ export default class GraphBodyRenderer extends Eventable {
|
|
|
537
586
|
minPointSpacing: singleSeries.minPointSpacing,
|
|
538
587
|
getIndividualPoints,
|
|
539
588
|
getRanges: singleSeries.rangeKey ? getRanges : null,
|
|
540
|
-
rendering: singleSeries.rendering // Pass rendering type for all charts
|
|
589
|
+
rendering: singleSeries.rendering, // Pass rendering type for all charts
|
|
590
|
+
negativeColor: singleSeries.negativeColor,
|
|
591
|
+
hasNegatives,
|
|
592
|
+
zero,
|
|
593
|
+
zeroColor: singleSeries.zeroLineColor
|
|
541
594
|
};
|
|
542
595
|
|
|
543
596
|
|
|
@@ -560,7 +613,10 @@ export default class GraphBodyRenderer extends Eventable {
|
|
|
560
613
|
drawParams.selectionBounds = selection || bounds;
|
|
561
614
|
}
|
|
562
615
|
|
|
563
|
-
|
|
616
|
+
// For shadow rendering, always use 2D canvas for lines/points even with WebGL
|
|
617
|
+
// This is because shadow uses WebGL for the fill but needs 2D canvas for lines/points
|
|
618
|
+
// to support negativeColor and other features
|
|
619
|
+
if (this._webgl && singleSeries.rendering !== 'shadow') {
|
|
564
620
|
this._lineProgram.draw(inRenderSpace, drawParams);
|
|
565
621
|
} else {
|
|
566
622
|
drawLine(inRenderSpace, drawParams);
|
|
@@ -210,7 +210,9 @@ export default class ShadowProgram {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
const trapezoids = [];
|
|
213
|
-
const
|
|
213
|
+
const positiveTrapezoids = [];
|
|
214
|
+
const negativeTrapezoids = [];
|
|
215
|
+
const { zero, inRenderSpaceAreaBottom, negativeGradient, hasNegatives } = params;
|
|
214
216
|
|
|
215
217
|
|
|
216
218
|
for (let i = 0; i < individualPoints.length - 1; i++) {
|
|
@@ -232,25 +234,45 @@ export default class ShadowProgram {
|
|
|
232
234
|
const yCross = zero;
|
|
233
235
|
|
|
234
236
|
if (Math.abs(y1 - yCross) > 0.1) {
|
|
235
|
-
|
|
237
|
+
const trap = {
|
|
236
238
|
x1,
|
|
237
239
|
y1,
|
|
238
240
|
x2: xCross,
|
|
239
241
|
y2: yCross,
|
|
240
242
|
bottomY1,
|
|
241
243
|
bottomY2: zero,
|
|
242
|
-
}
|
|
244
|
+
};
|
|
245
|
+
trapezoids.push(trap);
|
|
246
|
+
|
|
247
|
+
// Determine if positive or negative (in screen coords, smaller Y is higher/positive)
|
|
248
|
+
if (hasNegatives && negativeGradient) {
|
|
249
|
+
if (y1 <= zero) {
|
|
250
|
+
positiveTrapezoids.push(trap);
|
|
251
|
+
} else {
|
|
252
|
+
negativeTrapezoids.push(trap);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
243
255
|
}
|
|
244
256
|
|
|
245
257
|
if (Math.abs(y2 - yCross) > 0.1) {
|
|
246
|
-
|
|
258
|
+
const trap = {
|
|
247
259
|
x1: xCross,
|
|
248
260
|
y1: yCross,
|
|
249
261
|
x2,
|
|
250
262
|
y2,
|
|
251
263
|
bottomY1: zero,
|
|
252
264
|
bottomY2,
|
|
253
|
-
}
|
|
265
|
+
};
|
|
266
|
+
trapezoids.push(trap);
|
|
267
|
+
|
|
268
|
+
// Determine if positive or negative
|
|
269
|
+
if (hasNegatives && negativeGradient) {
|
|
270
|
+
if (y2 <= zero) {
|
|
271
|
+
positiveTrapezoids.push(trap);
|
|
272
|
+
} else {
|
|
273
|
+
negativeTrapezoids.push(trap);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
254
276
|
}
|
|
255
277
|
} else {
|
|
256
278
|
// Skip trapezoids completely outside canvas
|
|
@@ -272,16 +294,24 @@ export default class ShadowProgram {
|
|
|
272
294
|
|
|
273
295
|
const trapezoid = { x1, y1, x2: finalX2, y2: finalY2, bottomY1, bottomY2: finalBottomY2 };
|
|
274
296
|
trapezoids.push(trapezoid);
|
|
297
|
+
|
|
298
|
+
// Determine if positive or negative
|
|
299
|
+
if (hasNegatives && negativeGradient) {
|
|
300
|
+
// Check average Y position
|
|
301
|
+
const avgY = (y1 + finalY2) / 2;
|
|
302
|
+
if (avgY <= zero) {
|
|
303
|
+
positiveTrapezoids.push(trapezoid);
|
|
304
|
+
} else {
|
|
305
|
+
negativeTrapezoids.push(trapezoid);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
275
308
|
}
|
|
276
309
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
310
|
|
|
280
311
|
if (trapezoids.length === 0) {
|
|
281
312
|
return;
|
|
282
313
|
}
|
|
283
314
|
|
|
284
|
-
const geometry = this.generateTrapezoidGeometry(trapezoids);
|
|
285
315
|
const positionLoc = gl.getAttribLocation(this._program, "position");
|
|
286
316
|
const trapezoidBoundsLoc = gl.getAttribLocation(
|
|
287
317
|
this._program,
|
|
@@ -292,63 +322,82 @@ export default class ShadowProgram {
|
|
|
292
322
|
"trapezoidBottom"
|
|
293
323
|
);
|
|
294
324
|
|
|
295
|
-
gl.enableVertexAttribArray(positionLoc);
|
|
296
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this._positionBuffer);
|
|
297
|
-
gl.bufferData(gl.ARRAY_BUFFER, geometry.positions, gl.STATIC_DRAW);
|
|
298
|
-
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
|
|
299
|
-
|
|
300
|
-
gl.enableVertexAttribArray(trapezoidBoundsLoc);
|
|
301
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this._trapezoidBoundsBuffer);
|
|
302
|
-
gl.bufferData(gl.ARRAY_BUFFER, geometry.trapezoidBounds, gl.STATIC_DRAW);
|
|
303
|
-
gl.vertexAttribPointer(trapezoidBoundsLoc, 4, gl.FLOAT, false, 0, 0);
|
|
304
|
-
|
|
305
|
-
gl.enableVertexAttribArray(trapezoidBottomLoc);
|
|
306
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this._trapezoidBottomBuffer);
|
|
307
|
-
gl.bufferData(gl.ARRAY_BUFFER, geometry.trapezoidBottom, gl.STATIC_DRAW);
|
|
308
|
-
gl.vertexAttribPointer(trapezoidBottomLoc, 4, gl.FLOAT, false, 0, 0);
|
|
309
|
-
|
|
310
325
|
gl.uniform1f(gl.getUniformLocation(this._program, "width"), width);
|
|
311
326
|
gl.uniform1f(gl.getUniformLocation(this._program, "height"), height);
|
|
312
327
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
gl.activeTexture(gl.TEXTURE0);
|
|
316
|
-
gl.bindTexture(gl.TEXTURE_2D, this._gradientTexture);
|
|
317
|
-
|
|
318
|
-
gl.texImage2D(
|
|
319
|
-
gl.TEXTURE_2D,
|
|
320
|
-
0,
|
|
321
|
-
gl.RGBA,
|
|
322
|
-
gradientData.textureWidth,
|
|
323
|
-
1,
|
|
324
|
-
0,
|
|
325
|
-
gl.RGBA,
|
|
326
|
-
gl.UNSIGNED_BYTE,
|
|
327
|
-
gradientData.textureData
|
|
328
|
-
);
|
|
328
|
+
gl.enable(gl.BLEND);
|
|
329
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
329
330
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
331
|
+
// Helper function to render a set of trapezoids with a given gradient
|
|
332
|
+
const renderTrapezoidSet = (trapSet, gradient, color) => {
|
|
333
|
+
if (trapSet.length === 0) return;
|
|
334
|
+
|
|
335
|
+
const geometry = this.generateTrapezoidGeometry(trapSet);
|
|
336
|
+
|
|
337
|
+
gl.enableVertexAttribArray(positionLoc);
|
|
338
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this._positionBuffer);
|
|
339
|
+
gl.bufferData(gl.ARRAY_BUFFER, geometry.positions, gl.STATIC_DRAW);
|
|
340
|
+
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
|
|
341
|
+
|
|
342
|
+
gl.enableVertexAttribArray(trapezoidBoundsLoc);
|
|
343
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this._trapezoidBoundsBuffer);
|
|
344
|
+
gl.bufferData(gl.ARRAY_BUFFER, geometry.trapezoidBounds, gl.STATIC_DRAW);
|
|
345
|
+
gl.vertexAttribPointer(trapezoidBoundsLoc, 4, gl.FLOAT, false, 0, 0);
|
|
346
|
+
|
|
347
|
+
gl.enableVertexAttribArray(trapezoidBottomLoc);
|
|
348
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this._trapezoidBottomBuffer);
|
|
349
|
+
gl.bufferData(gl.ARRAY_BUFFER, geometry.trapezoidBottom, gl.STATIC_DRAW);
|
|
350
|
+
gl.vertexAttribPointer(trapezoidBottomLoc, 4, gl.FLOAT, false, 0, 0);
|
|
351
|
+
|
|
352
|
+
const gradientData = this.parseGradient(gradient, color);
|
|
353
|
+
|
|
354
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
355
|
+
gl.bindTexture(gl.TEXTURE_2D, this._gradientTexture);
|
|
356
|
+
|
|
357
|
+
gl.texImage2D(
|
|
358
|
+
gl.TEXTURE_2D,
|
|
359
|
+
0,
|
|
360
|
+
gl.RGBA,
|
|
361
|
+
gradientData.textureWidth,
|
|
362
|
+
1,
|
|
363
|
+
0,
|
|
364
|
+
gl.RGBA,
|
|
365
|
+
gl.UNSIGNED_BYTE,
|
|
366
|
+
gradientData.textureData
|
|
367
|
+
);
|
|
334
368
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
gl.
|
|
338
|
-
|
|
339
|
-
);
|
|
340
|
-
gl.uniform4fv(
|
|
341
|
-
gl.getUniformLocation(this._program, "fallbackColor"),
|
|
342
|
-
gradientData.fallbackColor
|
|
343
|
-
);
|
|
369
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
370
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
371
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
372
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
344
373
|
|
|
345
|
-
|
|
346
|
-
|
|
374
|
+
gl.uniform1i(gl.getUniformLocation(this._program, "gradientTexture"), 0);
|
|
375
|
+
gl.uniform1i(
|
|
376
|
+
gl.getUniformLocation(this._program, "gradientCount"),
|
|
377
|
+
gradientData.gradientCount
|
|
378
|
+
);
|
|
379
|
+
gl.uniform4fv(
|
|
380
|
+
gl.getUniformLocation(this._program, "fallbackColor"),
|
|
381
|
+
gradientData.fallbackColor
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer);
|
|
385
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, geometry.indices, gl.STATIC_DRAW);
|
|
347
386
|
|
|
348
|
-
|
|
349
|
-
|
|
387
|
+
gl.drawElements(gl.TRIANGLES, geometry.indices.length, gl.UNSIGNED_INT, 0);
|
|
388
|
+
};
|
|
350
389
|
|
|
351
|
-
|
|
390
|
+
// If we have negativeGradient and separate trapezoid sets, render them separately
|
|
391
|
+
if (hasNegatives && negativeGradient && (positiveTrapezoids.length > 0 || negativeTrapezoids.length > 0)) {
|
|
392
|
+
// Render positive trapezoids with the normal gradient
|
|
393
|
+
renderTrapezoidSet(positiveTrapezoids, params.gradient, params.color);
|
|
394
|
+
|
|
395
|
+
// Render negative trapezoids with negativeGradient
|
|
396
|
+
renderTrapezoidSet(negativeTrapezoids, negativeGradient, params.color);
|
|
397
|
+
} else {
|
|
398
|
+
// Fallback to rendering all trapezoids with the same gradient (original behavior)
|
|
399
|
+
renderTrapezoidSet(trapezoids, params.gradient, params.color);
|
|
400
|
+
}
|
|
352
401
|
|
|
353
402
|
const error = gl.getError();
|
|
354
403
|
if (error !== gl.NO_ERROR) {
|