@coinbase/cds-mobile-visualization 3.6.2 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dts/chart/CartesianChart.d.ts +4 -8
- package/dts/chart/CartesianChart.d.ts.map +1 -1
- package/dts/chart/area/Area.d.ts +3 -0
- package/dts/chart/area/Area.d.ts.map +1 -1
- package/dts/chart/area/AreaChart.d.ts.map +1 -1
- package/dts/chart/area/DottedArea.d.ts.map +1 -1
- package/dts/chart/area/GradientArea.d.ts.map +1 -1
- package/dts/chart/area/SolidArea.d.ts.map +1 -1
- package/dts/chart/bar/BarChart.d.ts.map +1 -1
- package/dts/chart/bar/BarStack.d.ts.map +1 -1
- package/dts/chart/gradient/Gradient.d.ts +14 -3
- package/dts/chart/gradient/Gradient.d.ts.map +1 -1
- package/dts/chart/line/DottedLine.d.ts.map +1 -1
- package/dts/chart/line/Line.d.ts +3 -0
- package/dts/chart/line/Line.d.ts.map +1 -1
- package/dts/chart/line/LineChart.d.ts.map +1 -1
- package/dts/chart/line/SolidLine.d.ts.map +1 -1
- package/dts/chart/utils/axis.d.ts +18 -8
- package/dts/chart/utils/axis.d.ts.map +1 -1
- package/dts/chart/utils/bar.d.ts +17 -7
- package/dts/chart/utils/bar.d.ts.map +1 -1
- package/dts/chart/utils/chart.d.ts +9 -0
- package/dts/chart/utils/chart.d.ts.map +1 -1
- package/dts/chart/utils/context.d.ts +3 -3
- package/dts/chart/utils/context.d.ts.map +1 -1
- package/dts/chart/utils/gradient.d.ts +14 -4
- package/dts/chart/utils/gradient.d.ts.map +1 -1
- package/esm/chart/CartesianChart.js +6 -4
- package/esm/chart/__stories__/ChartTransitions.stories.js +68 -0
- package/esm/chart/area/Area.js +0 -2
- package/esm/chart/area/AreaChart.js +15 -19
- package/esm/chart/area/DottedArea.js +6 -4
- package/esm/chart/area/GradientArea.js +6 -4
- package/esm/chart/area/SolidArea.js +3 -0
- package/esm/chart/area/__stories__/AreaChart.stories.js +189 -3
- package/esm/chart/bar/BarChart.js +14 -22
- package/esm/chart/bar/BarStack.js +15 -10
- package/esm/chart/bar/__stories__/BarChart.stories.js +84 -2
- package/esm/chart/gradient/Gradient.js +119 -26
- package/esm/chart/line/DottedLine.js +3 -0
- package/esm/chart/line/Line.js +1 -3
- package/esm/chart/line/LineChart.js +8 -4
- package/esm/chart/line/SolidLine.js +3 -0
- package/esm/chart/utils/axis.js +32 -4
- package/esm/chart/utils/bar.js +129 -76
- package/esm/chart/utils/chart.js +53 -21
- package/esm/chart/utils/gradient.js +15 -5
- package/package.json +1 -1
package/esm/chart/utils/bar.js
CHANGED
|
@@ -133,19 +133,20 @@ export function getStackGroups(series, defaultAxisId) {
|
|
|
133
133
|
* @param bars - Array of bar items with current valuePos and length
|
|
134
134
|
* @param stackGap - Gap size in pixels between adjacent bars
|
|
135
135
|
* @param layout - The layout of the chart
|
|
136
|
-
* @param baseline -
|
|
136
|
+
* @param baseline - Value-axis baseline in data space
|
|
137
|
+
* @param baselinePx - Pixel position of the value-axis baseline on the value axis
|
|
137
138
|
* @returns New array of bars with adjusted valuePos and length
|
|
138
139
|
*/
|
|
139
|
-
function applyStackGap(bars, stackGap, layout, baseline) {
|
|
140
|
+
function applyStackGap(bars, stackGap, layout, baseline, baselinePx) {
|
|
140
141
|
if (!stackGap || bars.length <= 1) return bars;
|
|
141
142
|
const result = [...bars];
|
|
142
143
|
const barsAboveBaseline = bars.filter(bar => {
|
|
143
144
|
const [bottom, top] = [...bar.dataValue].sort((a, b) => a - b);
|
|
144
|
-
return bottom >=
|
|
145
|
+
return bottom >= baseline && top !== bottom && bar.shouldApplyGap;
|
|
145
146
|
});
|
|
146
147
|
const barsBelowBaseline = bars.filter(bar => {
|
|
147
148
|
const [bottom, top] = [...bar.dataValue].sort((a, b) => a - b);
|
|
148
|
-
return top <=
|
|
149
|
+
return top <= baseline && bottom !== top && bar.shouldApplyGap;
|
|
149
150
|
});
|
|
150
151
|
const applyGapGroup = (group, growing) => {
|
|
151
152
|
if (group.length <= 1) return;
|
|
@@ -153,7 +154,7 @@ function applyStackGap(bars, stackGap, layout, baseline) {
|
|
|
153
154
|
const totalDataLength = group.reduce((sum, bar) => sum + bar.length, 0);
|
|
154
155
|
const lengthReduction = totalGapSpace / totalDataLength;
|
|
155
156
|
const sortedBars = growing ? [...group].sort((a, b) => b.valuePos - a.valuePos) : [...group].sort((a, b) => a.valuePos - b.valuePos);
|
|
156
|
-
let currentEdge =
|
|
157
|
+
let currentEdge = baselinePx;
|
|
157
158
|
sortedBars.forEach((bar, index) => {
|
|
158
159
|
const newLength = bar.length * (1 - lengthReduction);
|
|
159
160
|
let newValuePos;
|
|
@@ -194,20 +195,21 @@ function applyStackGap(bars, stackGap, layout, baseline) {
|
|
|
194
195
|
* @param bars - Array of bar items with final valuePos, length, and dataValue
|
|
195
196
|
* @param initialBarMinSizes - Per-bar initial sizes in pixels for entrance animation
|
|
196
197
|
* @param stackGap - Gap between adjacent bars in pixels
|
|
197
|
-
* @param baseline -
|
|
198
|
+
* @param baseline - Value-axis baseline in data space
|
|
199
|
+
* @param baselinePx - Pixel position of the value-axis baseline on the value axis
|
|
198
200
|
* @param layout - The layout of the chart
|
|
199
|
-
* @returns Array of origin positions (one per bar, parallel to input), all defaulting to
|
|
201
|
+
* @returns Array of origin positions (one per bar, parallel to input), all defaulting to baselinePx
|
|
200
202
|
*/
|
|
201
|
-
function getBarOrigins(bars, initialBarMinSizes, stackGap, baseline, layout) {
|
|
202
|
-
const result = bars.map(() =>
|
|
203
|
+
function getBarOrigins(bars, initialBarMinSizes, stackGap, baseline, baselinePx, layout) {
|
|
204
|
+
const result = bars.map(() => baselinePx);
|
|
203
205
|
if (bars.length === 0 || initialBarMinSizes.every(size => !size)) return result;
|
|
204
206
|
const isPositive = bar => {
|
|
205
207
|
const [lo, hi] = [...bar.dataValue].sort((a, b) => a - b);
|
|
206
|
-
return lo >=
|
|
208
|
+
return lo >= baseline && hi !== lo;
|
|
207
209
|
};
|
|
208
210
|
const isNegative = bar => {
|
|
209
211
|
const [lo, hi] = [...bar.dataValue].sort((a, b) => a - b);
|
|
210
|
-
return hi <=
|
|
212
|
+
return hi <= baseline && hi !== lo;
|
|
211
213
|
};
|
|
212
214
|
const positiveBars = bars.map((bar, i) => ({
|
|
213
215
|
bar,
|
|
@@ -219,7 +221,7 @@ function getBarOrigins(bars, initialBarMinSizes, stackGap, baseline, layout) {
|
|
|
219
221
|
return isPositive(bar);
|
|
220
222
|
}).sort((a, b) => layout === 'vertical' ? b.bar.valuePos - a.bar.valuePos : a.bar.valuePos - b.bar.valuePos);
|
|
221
223
|
if (layout === 'vertical') {
|
|
222
|
-
let currentPositive =
|
|
224
|
+
let currentPositive = baselinePx;
|
|
223
225
|
positiveBars.forEach((_ref2, idx) => {
|
|
224
226
|
var _initialBarMinSizes$i;
|
|
225
227
|
let {
|
|
@@ -233,7 +235,7 @@ function getBarOrigins(bars, initialBarMinSizes, stackGap, baseline, layout) {
|
|
|
233
235
|
}
|
|
234
236
|
});
|
|
235
237
|
} else {
|
|
236
|
-
let currentPositive =
|
|
238
|
+
let currentPositive = baselinePx;
|
|
237
239
|
positiveBars.forEach((_ref3, idx) => {
|
|
238
240
|
var _initialBarMinSizes$i2;
|
|
239
241
|
let {
|
|
@@ -257,7 +259,7 @@ function getBarOrigins(bars, initialBarMinSizes, stackGap, baseline, layout) {
|
|
|
257
259
|
return isNegative(bar);
|
|
258
260
|
}).sort((a, b) => layout === 'vertical' ? a.bar.valuePos - b.bar.valuePos : b.bar.valuePos + b.bar.length - (a.bar.valuePos + a.bar.length));
|
|
259
261
|
if (layout === 'vertical') {
|
|
260
|
-
let currentNegative =
|
|
262
|
+
let currentNegative = baselinePx;
|
|
261
263
|
negativeBars.forEach((_ref5, idx) => {
|
|
262
264
|
var _initialBarMinSizes$i3;
|
|
263
265
|
let {
|
|
@@ -271,7 +273,7 @@ function getBarOrigins(bars, initialBarMinSizes, stackGap, baseline, layout) {
|
|
|
271
273
|
}
|
|
272
274
|
});
|
|
273
275
|
} else {
|
|
274
|
-
let currentNegative =
|
|
276
|
+
let currentNegative = baselinePx;
|
|
275
277
|
negativeBars.forEach((_ref6, idx) => {
|
|
276
278
|
var _initialBarMinSizes$i4;
|
|
277
279
|
let {
|
|
@@ -382,11 +384,12 @@ export function getStackInitialClipRect(stackRect, layout, origin) {
|
|
|
382
384
|
*
|
|
383
385
|
* @param bars - Array of bar items with current valuePos and length
|
|
384
386
|
* @param barMinSize - Minimum bar size in pixels
|
|
385
|
-
* @param
|
|
386
|
-
* @param
|
|
387
|
+
* @param baseline - Value-axis baseline in data space
|
|
388
|
+
* @param baselinePx - Pixel position of the value-axis baseline on the value axis
|
|
389
|
+
* @param layout - Chart layout
|
|
387
390
|
* @returns New array of bars with adjusted valuePos and length
|
|
388
391
|
*/
|
|
389
|
-
function applyBarMinSize(bars, barMinSize,
|
|
392
|
+
function applyBarMinSize(bars, barMinSize, baseline, baselinePx, layout) {
|
|
390
393
|
if (!barMinSize || bars.length === 0) return bars;
|
|
391
394
|
const originalTotalLength = bars.reduce((sum, bar) => sum + bar.length, 0);
|
|
392
395
|
const needsExpansion = bars.map(bar => bar.length < barMinSize);
|
|
@@ -429,45 +432,85 @@ function applyBarMinSize(bars, barMinSize, layout, baseline) {
|
|
|
429
432
|
// independent of the current valuePos (which hasn't been repositioned yet).
|
|
430
433
|
const barsAboveBaseline = stackedSortedBars.filter(bar => {
|
|
431
434
|
const [bottom, top] = [...bar.dataValue].sort((a, b) => a - b);
|
|
432
|
-
return
|
|
435
|
+
return bottom >= baseline && top !== bottom;
|
|
433
436
|
});
|
|
434
437
|
const barsBelowBaseline = stackedSortedBars.filter(bar => {
|
|
435
438
|
const [bottom, top] = [...bar.dataValue].sort((a, b) => a - b);
|
|
436
|
-
return
|
|
439
|
+
return top <= baseline && bottom !== top;
|
|
437
440
|
});
|
|
438
441
|
|
|
439
|
-
// Restack bars above baseline (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
442
|
+
// Restack bars above baseline (positive data side).
|
|
443
|
+
// vertical → grow up (−Y from baseline); horizontal → grow right (+X from baseline).
|
|
444
|
+
if (layout === 'vertical') {
|
|
445
|
+
let currentAbove = baselinePx;
|
|
446
|
+
for (let i = barsAboveBaseline.length - 1; i >= 0; i--) {
|
|
447
|
+
const bar = barsAboveBaseline[i];
|
|
448
|
+
const newValuePos = currentAbove - bar.length;
|
|
449
|
+
newPositions.set(bar.seriesId, {
|
|
450
|
+
valuePos: newValuePos,
|
|
451
|
+
length: bar.length
|
|
452
|
+
});
|
|
453
|
+
if (i > 0) {
|
|
454
|
+
const nextBar = barsAboveBaseline[i - 1];
|
|
455
|
+
const originalCurrent = bars.find(b => b.seriesId === bar.seriesId);
|
|
456
|
+
const originalNext = bars.find(b => b.seriesId === nextBar.seriesId);
|
|
457
|
+
const originalGap = originalCurrent.valuePos - (originalNext.valuePos + originalNext.length);
|
|
458
|
+
currentAbove = newValuePos - originalGap;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
} else {
|
|
462
|
+
let currentEdge = baselinePx;
|
|
463
|
+
for (let i = 0; i < barsAboveBaseline.length; i++) {
|
|
464
|
+
const bar = barsAboveBaseline[i];
|
|
465
|
+
newPositions.set(bar.seriesId, {
|
|
466
|
+
valuePos: currentEdge,
|
|
467
|
+
length: bar.length
|
|
468
|
+
});
|
|
469
|
+
if (i < barsAboveBaseline.length - 1) {
|
|
470
|
+
const nextBar = barsAboveBaseline[i + 1];
|
|
471
|
+
const originalCurrent = bars.find(b => b.seriesId === bar.seriesId);
|
|
472
|
+
const originalNext = bars.find(b => b.seriesId === nextBar.seriesId);
|
|
473
|
+
const originalGap = originalNext.valuePos - (originalCurrent.valuePos + originalCurrent.length);
|
|
474
|
+
currentEdge = currentEdge + bar.length + originalGap;
|
|
475
|
+
}
|
|
454
476
|
}
|
|
455
477
|
}
|
|
456
478
|
|
|
457
|
-
// Restack bars below baseline (
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
479
|
+
// Restack bars below baseline (negative data side).
|
|
480
|
+
// vertical → grow down (+Y); horizontal → grow left (−X).
|
|
481
|
+
if (layout === 'vertical') {
|
|
482
|
+
let currentBelow = baselinePx;
|
|
483
|
+
for (let i = 0; i < barsBelowBaseline.length; i++) {
|
|
484
|
+
const bar = barsBelowBaseline[i];
|
|
485
|
+
newPositions.set(bar.seriesId, {
|
|
486
|
+
valuePos: currentBelow,
|
|
487
|
+
length: bar.length
|
|
488
|
+
});
|
|
489
|
+
if (i < barsBelowBaseline.length - 1) {
|
|
490
|
+
const nextBar = barsBelowBaseline[i + 1];
|
|
491
|
+
const originalCurrent = bars.find(b => b.seriesId === bar.seriesId);
|
|
492
|
+
const originalNext = bars.find(b => b.seriesId === nextBar.seriesId);
|
|
493
|
+
const originalGap = originalNext.valuePos - (originalCurrent.valuePos + originalCurrent.length);
|
|
494
|
+
currentBelow = currentBelow + bar.length + originalGap;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
const sortedBelow = [...barsBelowBaseline].sort((a, b) => b.valuePos - a.valuePos);
|
|
499
|
+
let currentEdge = baselinePx;
|
|
500
|
+
for (let i = sortedBelow.length - 1; i >= 0; i--) {
|
|
501
|
+
const bar = sortedBelow[i];
|
|
502
|
+
const newValuePos = currentEdge - bar.length;
|
|
503
|
+
newPositions.set(bar.seriesId, {
|
|
504
|
+
valuePos: newValuePos,
|
|
505
|
+
length: bar.length
|
|
506
|
+
});
|
|
507
|
+
if (i > 0) {
|
|
508
|
+
const nextBar = sortedBelow[i - 1];
|
|
509
|
+
const originalCurrent = bars.find(b => b.seriesId === bar.seriesId);
|
|
510
|
+
const originalNext = bars.find(b => b.seriesId === nextBar.seriesId);
|
|
511
|
+
const originalGap = originalCurrent.valuePos - (originalNext.valuePos + originalNext.length);
|
|
512
|
+
currentEdge = newValuePos - originalGap;
|
|
513
|
+
}
|
|
471
514
|
}
|
|
472
515
|
}
|
|
473
516
|
}
|
|
@@ -493,10 +536,11 @@ function applyBarMinSize(bars, barMinSize, layout, baseline) {
|
|
|
493
536
|
* @param layout - The layout of the chart
|
|
494
537
|
* @param indexPos - Pixel position along the categorical (index) axis
|
|
495
538
|
* @param thickness - Bar thickness in pixels
|
|
496
|
-
* @param baseline -
|
|
539
|
+
* @param baseline - Value-axis baseline in data space
|
|
540
|
+
* @param baselinePx - Pixel position of the value-axis baseline on the value axis
|
|
497
541
|
* @returns Updated bars and stackBounds; unchanged if stackSize >= stackMinSize
|
|
498
542
|
*/
|
|
499
|
-
function applyStackMinSize(bars, stackMinSize, stackSize, stackBounds, layout, indexPos, thickness, baseline) {
|
|
543
|
+
function applyStackMinSize(bars, stackMinSize, stackSize, stackBounds, layout, indexPos, thickness, baseline, baselinePx) {
|
|
500
544
|
if (!stackMinSize || stackSize >= stackMinSize) return {
|
|
501
545
|
bars,
|
|
502
546
|
stackBounds
|
|
@@ -513,10 +557,10 @@ function applyStackMinSize(bars, stackMinSize, stackSize, stackBounds, layout, i
|
|
|
513
557
|
const [bottom, top] = [...bar.dataValue].sort((a, b) => a - b);
|
|
514
558
|
let newValuePos;
|
|
515
559
|
const newLength = stackMinSize;
|
|
516
|
-
if (bottom >=
|
|
560
|
+
if (bottom >= baseline && top !== bottom) {
|
|
517
561
|
// Bar is on the positive side: vertical→expands upward (↑), horizontal→expands rightward (→)
|
|
518
562
|
newValuePos = layout === 'vertical' ? bar.valuePos - sizeIncrease : bar.valuePos;
|
|
519
|
-
} else if (top <=
|
|
563
|
+
} else if (top <= baseline && top !== bottom) {
|
|
520
564
|
// Bar is on the negative side: vertical→expands downward (↓), horizontal→expands leftward (←)
|
|
521
565
|
newValuePos = layout === 'vertical' ? bar.valuePos : bar.valuePos - sizeIncrease;
|
|
522
566
|
} else {
|
|
@@ -542,12 +586,12 @@ function applyStackMinSize(bars, stackMinSize, stackSize, stackBounds, layout, i
|
|
|
542
586
|
|
|
543
587
|
// For vertical: positive bars are above baseline (smaller Y), negative bars are below (larger Y)
|
|
544
588
|
// For horizontal: positive bars are right of baseline (larger X), negative bars are left (smaller X)
|
|
545
|
-
const barsOnPositiveSide = layout === 'vertical' ? sortedBars.filter(bar => bar.valuePos + bar.length <=
|
|
546
|
-
const barsOnNegativeSide = layout === 'vertical' ? sortedBars.filter(bar => bar.valuePos >=
|
|
589
|
+
const barsOnPositiveSide = layout === 'vertical' ? sortedBars.filter(bar => bar.valuePos + bar.length <= baselinePx) : sortedBars.filter(bar => bar.valuePos >= baselinePx);
|
|
590
|
+
const barsOnNegativeSide = layout === 'vertical' ? sortedBars.filter(bar => bar.valuePos >= baselinePx) : sortedBars.filter(bar => bar.valuePos + bar.length <= baselinePx);
|
|
547
591
|
const newPositions = new Map();
|
|
548
592
|
if (layout === 'vertical') {
|
|
549
593
|
// Stack from baseline upward (decreasing valuePos) for positive bars
|
|
550
|
-
let currentPos =
|
|
594
|
+
let currentPos = baselinePx;
|
|
551
595
|
for (let i = barsOnPositiveSide.length - 1; i >= 0; i--) {
|
|
552
596
|
const bar = barsOnPositiveSide[i];
|
|
553
597
|
const newLength = bar.length * barScaleFactor;
|
|
@@ -563,7 +607,7 @@ function applyStackMinSize(bars, stackMinSize, stackSize, stackBounds, layout, i
|
|
|
563
607
|
}
|
|
564
608
|
}
|
|
565
609
|
// Stack from baseline downward (increasing valuePos) for negative bars
|
|
566
|
-
let currentPosBelow =
|
|
610
|
+
let currentPosBelow = baselinePx;
|
|
567
611
|
for (let i = 0; i < barsOnNegativeSide.length; i++) {
|
|
568
612
|
const bar = barsOnNegativeSide[i];
|
|
569
613
|
const newLength = bar.length * barScaleFactor;
|
|
@@ -579,7 +623,7 @@ function applyStackMinSize(bars, stackMinSize, stackSize, stackBounds, layout, i
|
|
|
579
623
|
}
|
|
580
624
|
} else {
|
|
581
625
|
// Stack from baseline rightward (increasing valuePos) for positive bars
|
|
582
|
-
let currentPos =
|
|
626
|
+
let currentPos = baselinePx;
|
|
583
627
|
for (let i = 0; i < barsOnPositiveSide.length; i++) {
|
|
584
628
|
const bar = barsOnPositiveSide[i];
|
|
585
629
|
const newLength = bar.length * barScaleFactor;
|
|
@@ -594,7 +638,7 @@ function applyStackMinSize(bars, stackMinSize, stackSize, stackBounds, layout, i
|
|
|
594
638
|
}
|
|
595
639
|
}
|
|
596
640
|
// Stack from baseline leftward (decreasing valuePos) for negative bars
|
|
597
|
-
let currentPosLeft =
|
|
641
|
+
let currentPosLeft = baselinePx;
|
|
598
642
|
for (let i = barsOnNegativeSide.length - 1; i >= 0; i--) {
|
|
599
643
|
const bar = barsOnNegativeSide[i];
|
|
600
644
|
const newLength = bar.length * barScaleFactor;
|
|
@@ -675,16 +719,23 @@ function applyBorderRadiusLogic(bars, layout, stackGap) {
|
|
|
675
719
|
export const EPSILON = 1e-4;
|
|
676
720
|
|
|
677
721
|
/**
|
|
678
|
-
* Computes and clamps the
|
|
722
|
+
* Computes and clamps the value-axis baseline position in pixels.
|
|
723
|
+
*
|
|
724
|
+
* When `baseline` (data space) is omitted, the baseline is chosen heuristically from the scale domain:
|
|
725
|
+
* - If the full domain is positive, use domain min.
|
|
726
|
+
* - If the full domain is negative, use domain max.
|
|
727
|
+
* - If the domain crosses zero, use `0`.
|
|
728
|
+
* When `baseline` is set, that value is used as the data-space baseline instead.
|
|
679
729
|
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
730
|
+
* @param valueScale - Scale for the value axis
|
|
731
|
+
* @param stackRect - Bounding rect of the stack in pixels
|
|
732
|
+
* @param layout - Chart layout
|
|
733
|
+
* @param baseline - Optional value-axis baseline in data space
|
|
683
734
|
*/
|
|
684
|
-
export function
|
|
735
|
+
export function getBaselinePx(valueScale, stackRect, layout, baseline) {
|
|
685
736
|
const [domainMin, domainMax] = valueScale.domain();
|
|
686
|
-
const
|
|
687
|
-
const baselinePos = valueScale(
|
|
737
|
+
const baselineInData = baseline != null ? baseline : domainMin >= 0 ? domainMin : domainMax <= 0 ? domainMax : 0;
|
|
738
|
+
const baselinePos = valueScale(baselineInData);
|
|
688
739
|
if (layout === 'vertical') {
|
|
689
740
|
return Math.max(stackRect.y, Math.min(baselinePos != null ? baselinePos : stackRect.y + stackRect.height, stackRect.y + stackRect.height));
|
|
690
741
|
}
|
|
@@ -725,7 +776,8 @@ function getStackSizeForLayout(layout, stackRect) {
|
|
|
725
776
|
* @param params.seriesGradients - Precomputed gradient configs per series (undefined entries are skipped)
|
|
726
777
|
* @param params.roundBaseline - Whether to round the face touching the baseline
|
|
727
778
|
* @param params.layout - The layout of the chart
|
|
728
|
-
* @param params.baseline -
|
|
779
|
+
* @param params.baseline - Value-axis baseline in data space
|
|
780
|
+
* @param params.baselinePx - Pixel position of the value-axis baseline on the value axis
|
|
729
781
|
* @param params.stackGap - Gap between adjacent bars in pixels
|
|
730
782
|
* @param params.barMinSize - Minimum individual bar size in pixels
|
|
731
783
|
* @param params.stackMinSize - Minimum total stack size in pixels
|
|
@@ -744,7 +796,8 @@ export function getBars(params) {
|
|
|
744
796
|
seriesGradients,
|
|
745
797
|
roundBaseline,
|
|
746
798
|
layout,
|
|
747
|
-
baseline,
|
|
799
|
+
baseline = 0,
|
|
800
|
+
baselinePx,
|
|
748
801
|
stackGap,
|
|
749
802
|
barMinSize,
|
|
750
803
|
stackMinSize,
|
|
@@ -766,10 +819,10 @@ export function getBars(params) {
|
|
|
766
819
|
const originalValue = originalData == null ? void 0 : originalData[categoryIndex];
|
|
767
820
|
const shouldApplyGap = !Array.isArray(originalValue);
|
|
768
821
|
const [bottom, top] = [...value].sort((a, b) => a - b);
|
|
769
|
-
const edgeBottom = (_valueScale = valueScale(bottom)) != null ? _valueScale :
|
|
770
|
-
const edgeTop = (_valueScale2 = valueScale(top)) != null ? _valueScale2 :
|
|
771
|
-
const roundTop = roundBaseline || Math.abs(
|
|
772
|
-
const roundBottom = roundBaseline || Math.abs(
|
|
822
|
+
const edgeBottom = (_valueScale = valueScale(bottom)) != null ? _valueScale : baselinePx;
|
|
823
|
+
const edgeTop = (_valueScale2 = valueScale(top)) != null ? _valueScale2 : baselinePx;
|
|
824
|
+
const roundTop = roundBaseline || Math.abs(top - baseline) >= EPSILON && Math.abs(edgeTop - baselinePx) >= EPSILON;
|
|
825
|
+
const roundBottom = roundBaseline || Math.abs(bottom - baseline) >= EPSILON && Math.abs(edgeBottom - baselinePx) >= EPSILON;
|
|
773
826
|
const length = Math.abs(edgeBottom - edgeTop);
|
|
774
827
|
const valuePos = Math.min(edgeBottom, edgeTop);
|
|
775
828
|
if (length <= 0) return;
|
|
@@ -809,12 +862,12 @@ export function getBars(params) {
|
|
|
809
862
|
|
|
810
863
|
// Apply proportional gap distribution to maintain total stack length
|
|
811
864
|
if (stackGap && allBars.length > 1) {
|
|
812
|
-
allBars = applyStackGap(allBars, stackGap, layout, baseline);
|
|
865
|
+
allBars = applyStackGap(allBars, stackGap, layout, baseline, baselinePx);
|
|
813
866
|
}
|
|
814
867
|
|
|
815
868
|
// Apply barMinSize constraints
|
|
816
869
|
if (barMinSize) {
|
|
817
|
-
allBars = applyBarMinSize(allBars, barMinSize,
|
|
870
|
+
allBars = applyBarMinSize(allBars, barMinSize, baseline, baselinePx, layout);
|
|
818
871
|
}
|
|
819
872
|
allBars = applyBorderRadiusLogic(allBars, layout, stackGap);
|
|
820
873
|
|
|
@@ -824,7 +877,7 @@ export function getBars(params) {
|
|
|
824
877
|
const maxValuePos = Math.max(...allBars.map(bar => bar.valuePos + bar.length));
|
|
825
878
|
const stackSize = maxValuePos - minValuePos;
|
|
826
879
|
const stackBounds = getStackBoundsForLayout(layout, indexPos, thickness, minValuePos, stackSize);
|
|
827
|
-
const result = applyStackMinSize(allBars, stackMinSize, stackSize, stackBounds, layout, indexPos, thickness, baseline);
|
|
880
|
+
const result = applyStackMinSize(allBars, stackMinSize, stackSize, stackBounds, layout, indexPos, thickness, baseline, baselinePx);
|
|
828
881
|
allBars = result.bars;
|
|
829
882
|
|
|
830
883
|
// Reapply border radius logic only if we actually scaled
|
|
@@ -834,7 +887,7 @@ export function getBars(params) {
|
|
|
834
887
|
}
|
|
835
888
|
}
|
|
836
889
|
const initialBarMinSizes = getInitialBarMinSizes(allBars, barMinSize, stackMinSize);
|
|
837
|
-
const barOrigins = getBarOrigins(allBars, initialBarMinSizes, stackGap != null ? stackGap : 0, baseline, layout);
|
|
890
|
+
const barOrigins = getBarOrigins(allBars, initialBarMinSizes, stackGap != null ? stackGap : 0, baseline, baselinePx, layout);
|
|
838
891
|
return allBars.map((bar, i) => _extends({}, bar, {
|
|
839
892
|
x: layout === 'vertical' ? indexPos : bar.valuePos,
|
|
840
893
|
y: layout === 'vertical' ? bar.valuePos : indexPos,
|
package/esm/chart/utils/chart.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isSharedValue } from 'react-native-reanimated';
|
|
2
2
|
import { stack as d3Stack, stackOffsetDiverging, stackOrderNone } from 'd3-shape';
|
|
3
|
+
import { defaultAxisId } from './axis';
|
|
3
4
|
export const defaultStackId = 'DEFAULT_STACK_ID';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -58,17 +59,45 @@ const createStackKey = series => {
|
|
|
58
59
|
return series.stackId + ":" + xAxisId + ":" + yAxisId;
|
|
59
60
|
};
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Get the baseline for a series on the value axis for a series (stacking and plain numeric points).
|
|
64
|
+
* @returns The baseline for the series on the value axis, or `0` if none.
|
|
65
|
+
*/
|
|
66
|
+
const getValueAxisBaselineForSeries = (layout, series, xAxisConfigs, yAxisConfigs) => {
|
|
67
|
+
var _series$yAxisId, _yAxisConfigs$find$ba, _yAxisConfigs$find;
|
|
68
|
+
if (layout === 'horizontal') {
|
|
69
|
+
var _series$xAxisId, _xAxisConfigs$find$ba, _xAxisConfigs$find;
|
|
70
|
+
const seriesAxisId = (_series$xAxisId = series.xAxisId) != null ? _series$xAxisId : defaultAxisId;
|
|
71
|
+
return (_xAxisConfigs$find$ba = (_xAxisConfigs$find = xAxisConfigs.find(a => a.id === seriesAxisId)) == null ? void 0 : _xAxisConfigs$find.baseline) != null ? _xAxisConfigs$find$ba : 0;
|
|
72
|
+
}
|
|
73
|
+
const seriesAxisId = (_series$yAxisId = series.yAxisId) != null ? _series$yAxisId : defaultAxisId;
|
|
74
|
+
return (_yAxisConfigs$find$ba = (_yAxisConfigs$find = yAxisConfigs.find(a => a.id === seriesAxisId)) == null ? void 0 : _yAxisConfigs$find.baseline) != null ? _yAxisConfigs$find$ba : 0;
|
|
75
|
+
};
|
|
76
|
+
|
|
61
77
|
/**
|
|
62
78
|
* Transforms series data into stacked data using D3's stack algorithm.
|
|
63
79
|
* Returns a map of series ID to transformed [baseline, value] tuples.
|
|
64
80
|
*
|
|
65
81
|
* @param series - Array of series with potential stack properties
|
|
82
|
+
* @param layout - When set with axis configs, value-axis baselines are resolved for stacking
|
|
66
83
|
* @returns Map of series ID to stacked data arrays
|
|
67
84
|
*/
|
|
68
|
-
export const getStackedSeriesData = series => {
|
|
85
|
+
export const getStackedSeriesData = (series, layout, xAxisConfigs, yAxisConfigs) => {
|
|
69
86
|
const stackedDataMap = new Map();
|
|
70
87
|
const numericStackGroups = new Map();
|
|
71
88
|
const individualSeries = [];
|
|
89
|
+
const normalizeSeriesData = seriesItem => {
|
|
90
|
+
if (!seriesItem.data) return;
|
|
91
|
+
const baseline = getValueAxisBaselineForSeries(layout, seriesItem, xAxisConfigs, yAxisConfigs);
|
|
92
|
+
return seriesItem.data.map(val => {
|
|
93
|
+
if (val === null) return null;
|
|
94
|
+
if (Array.isArray(val)) {
|
|
95
|
+
return val;
|
|
96
|
+
}
|
|
97
|
+
if (typeof val === 'number') return [baseline, val];
|
|
98
|
+
return null;
|
|
99
|
+
});
|
|
100
|
+
};
|
|
72
101
|
series.forEach(s => {
|
|
73
102
|
var _s$data2;
|
|
74
103
|
const stackKey = createStackKey(s);
|
|
@@ -83,31 +112,34 @@ export const getStackedSeriesData = series => {
|
|
|
83
112
|
}
|
|
84
113
|
});
|
|
85
114
|
individualSeries.forEach(s => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (val === null) return null;
|
|
89
|
-
if (Array.isArray(val)) {
|
|
90
|
-
return val;
|
|
91
|
-
}
|
|
92
|
-
if (typeof val === 'number') {
|
|
93
|
-
return [0, val];
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
96
|
-
});
|
|
115
|
+
const normalizedData = normalizeSeriesData(s);
|
|
116
|
+
if (!normalizedData) return;
|
|
97
117
|
stackedDataMap.set(s.id, normalizedData);
|
|
98
118
|
});
|
|
99
|
-
numericStackGroups.forEach(
|
|
119
|
+
numericStackGroups.forEach(groupSeries => {
|
|
120
|
+
// A lone series with stackId should still behave like a non-stacked series.
|
|
121
|
+
if (groupSeries.length < 2) {
|
|
122
|
+
groupSeries.forEach(singleSeries => {
|
|
123
|
+
const normalizedData = normalizeSeriesData(singleSeries);
|
|
124
|
+
if (!normalizedData) return;
|
|
125
|
+
stackedDataMap.set(singleSeries.id, normalizedData);
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
100
129
|
const maxLength = Math.max(...groupSeries.map(s => {
|
|
101
130
|
var _s$data3;
|
|
102
131
|
return ((_s$data3 = s.data) == null ? void 0 : _s$data3.length) || 0;
|
|
103
132
|
}));
|
|
104
133
|
if (maxLength === 0) return;
|
|
134
|
+
const first = groupSeries[0];
|
|
135
|
+
const groupBaseline = getValueAxisBaselineForSeries(layout, first, xAxisConfigs, yAxisConfigs);
|
|
105
136
|
const dataset = new Array(maxLength).fill(undefined).map((_, i) => {
|
|
106
137
|
const row = {};
|
|
107
138
|
for (const s of groupSeries) {
|
|
108
139
|
var _s$data4;
|
|
109
140
|
const val = (_s$data4 = s.data) == null ? void 0 : _s$data4[i];
|
|
110
|
-
|
|
141
|
+
// Stack around baseline by translating values into baseline-relative deltas.
|
|
142
|
+
const num = typeof val === 'number' ? val - groupBaseline : 0;
|
|
111
143
|
row[s.id] = num;
|
|
112
144
|
}
|
|
113
145
|
return row;
|
|
@@ -118,7 +150,7 @@ export const getStackedSeriesData = series => {
|
|
|
118
150
|
const seriesId = keys[layerIndex];
|
|
119
151
|
const stackedData = layer.map(_ref => {
|
|
120
152
|
let [bottom, top] = _ref;
|
|
121
|
-
return [bottom, top];
|
|
153
|
+
return [bottom + groupBaseline, top + groupBaseline];
|
|
122
154
|
});
|
|
123
155
|
stackedDataMap.set(seriesId, stackedData);
|
|
124
156
|
});
|
|
@@ -157,7 +189,7 @@ export const getLineData = data => {
|
|
|
157
189
|
* Range represents the range of y-values from the data.
|
|
158
190
|
* Handles stacking by transforming data when series have stack properties.
|
|
159
191
|
*/
|
|
160
|
-
export const getChartRange = (series, min, max) => {
|
|
192
|
+
export const getChartRange = (series, layout, xAxisConfigs, yAxisConfigs, min, max) => {
|
|
161
193
|
const range = {
|
|
162
194
|
min,
|
|
163
195
|
max
|
|
@@ -183,11 +215,11 @@ export const getChartRange = (series, min, max) => {
|
|
|
183
215
|
const hasStacks = Array.from(stackGroups.keys()).some(k => k !== undefined);
|
|
184
216
|
if (hasStacks) {
|
|
185
217
|
// Get stacked data using the shared function
|
|
186
|
-
const stackedDataMap = getStackedSeriesData(series);
|
|
218
|
+
const stackedDataMap = getStackedSeriesData(series, layout, xAxisConfigs, yAxisConfigs);
|
|
187
219
|
|
|
188
220
|
// Find the extreme values from the stacked data
|
|
189
|
-
let stackedMax =
|
|
190
|
-
let stackedMin =
|
|
221
|
+
let stackedMax = -Infinity;
|
|
222
|
+
let stackedMin = Infinity;
|
|
191
223
|
stackedDataMap.forEach(stackedData => {
|
|
192
224
|
stackedData.forEach(point => {
|
|
193
225
|
if (point !== null) {
|
|
@@ -199,8 +231,8 @@ export const getChartRange = (series, min, max) => {
|
|
|
199
231
|
});
|
|
200
232
|
|
|
201
233
|
// Don't add padding - let D3's nice() function handle axis padding
|
|
202
|
-
if (range.min === undefined) range.min =
|
|
203
|
-
if (range.max === undefined) range.max =
|
|
234
|
+
if (range.min === undefined) range.min = stackedMin === Infinity ? 0 : stackedMin;
|
|
235
|
+
if (range.max === undefined) range.max = stackedMax === -Infinity ? 0 : stackedMax;
|
|
204
236
|
} else {
|
|
205
237
|
// No stacking, calculate range from raw values
|
|
206
238
|
const allValues = [];
|
|
@@ -9,6 +9,14 @@ import { applySerializableScale, isCategoricalScale, isSerializableScale } from
|
|
|
9
9
|
* Defines a gradient.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Resolves the axis used for gradient processing.
|
|
14
|
+
*/
|
|
15
|
+
export const getGradientAxis = (gradient, layout) => {
|
|
16
|
+
var _gradient$axis;
|
|
17
|
+
return (_gradient$axis = gradient.axis) != null ? _gradient$axis : layout === 'horizontal' ? 'x' : 'y';
|
|
18
|
+
};
|
|
19
|
+
|
|
12
20
|
/**
|
|
13
21
|
* Resolves gradient stops, handling both static arrays and function forms.
|
|
14
22
|
* When stops is a function, calls it with the domain bounds.
|
|
@@ -87,9 +95,10 @@ export const getColorWithOpacity = (color1, opacity) => {
|
|
|
87
95
|
* Processes a GradientDefinition into a renderable GradientConfig.
|
|
88
96
|
* Supports both numeric scales (linear, log) and categorical scales (band).
|
|
89
97
|
*
|
|
90
|
-
* @param gradient - GradientDefinition configuration
|
|
91
|
-
* @param xScale - X-axis scale
|
|
92
|
-
* @param yScale - Y-axis scale
|
|
98
|
+
* @param gradient - GradientDefinition configuration
|
|
99
|
+
* @param xScale - X-axis scale
|
|
100
|
+
* @param yScale - Y-axis scale
|
|
101
|
+
* @param layout - Chart layout
|
|
93
102
|
* @returns GradientConfig or null if gradient processing fails
|
|
94
103
|
*
|
|
95
104
|
* @example
|
|
@@ -110,11 +119,12 @@ export const getColorWithOpacity = (color1, opacity) => {
|
|
|
110
119
|
* );
|
|
111
120
|
* }
|
|
112
121
|
*/
|
|
113
|
-
export const getGradientConfig = (gradient, xScale, yScale) => {
|
|
122
|
+
export const getGradientConfig = (gradient, xScale, yScale, layout) => {
|
|
114
123
|
if (!gradient) return;
|
|
115
124
|
|
|
116
125
|
// Get the scale based on axis
|
|
117
|
-
const
|
|
126
|
+
const axis = getGradientAxis(gradient, layout);
|
|
127
|
+
const scale = axis === 'x' ? xScale : yScale;
|
|
118
128
|
if (!scale) return;
|
|
119
129
|
|
|
120
130
|
// Extract domain from scale
|