@coinbase/cds-mobile-visualization 3.4.0-beta.8 → 3.4.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 +142 -0
- package/dts/chart/CartesianChart.d.ts +92 -7
- package/dts/chart/CartesianChart.d.ts.map +1 -1
- package/dts/chart/ChartContextBridge.d.ts.map +1 -1
- package/dts/chart/ChartProvider.d.ts +3 -0
- package/dts/chart/ChartProvider.d.ts.map +1 -1
- package/dts/chart/Path.d.ts +36 -13
- package/dts/chart/Path.d.ts.map +1 -1
- package/dts/chart/PeriodSelector.d.ts +21 -6
- package/dts/chart/PeriodSelector.d.ts.map +1 -1
- package/dts/chart/area/Area.d.ts +14 -11
- package/dts/chart/area/Area.d.ts.map +1 -1
- package/dts/chart/area/AreaChart.d.ts +33 -9
- 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/axis/Axis.d.ts +22 -42
- package/dts/chart/axis/Axis.d.ts.map +1 -1
- package/dts/chart/axis/XAxis.d.ts +6 -0
- package/dts/chart/axis/XAxis.d.ts.map +1 -1
- package/dts/chart/axis/YAxis.d.ts +1 -0
- package/dts/chart/axis/YAxis.d.ts.map +1 -1
- package/dts/chart/bar/Bar.d.ts +51 -51
- package/dts/chart/bar/Bar.d.ts.map +1 -1
- package/dts/chart/bar/BarChart.d.ts +56 -11
- package/dts/chart/bar/BarChart.d.ts.map +1 -1
- package/dts/chart/bar/BarPlot.d.ts +2 -1
- package/dts/chart/bar/BarPlot.d.ts.map +1 -1
- package/dts/chart/bar/BarStack.d.ts +45 -20
- package/dts/chart/bar/BarStack.d.ts.map +1 -1
- package/dts/chart/bar/BarStackGroup.d.ts +2 -1
- package/dts/chart/bar/BarStackGroup.d.ts.map +1 -1
- package/dts/chart/bar/DefaultBar.d.ts.map +1 -1
- package/dts/chart/bar/DefaultBarStack.d.ts.map +1 -1
- package/dts/chart/gradient/Gradient.d.ts +5 -0
- package/dts/chart/gradient/Gradient.d.ts.map +1 -1
- package/dts/chart/index.d.ts +1 -0
- package/dts/chart/index.d.ts.map +1 -1
- package/dts/chart/legend/DefaultLegendEntry.d.ts +5 -0
- package/dts/chart/legend/DefaultLegendEntry.d.ts.map +1 -0
- package/dts/chart/legend/DefaultLegendShape.d.ts +5 -0
- package/dts/chart/legend/DefaultLegendShape.d.ts.map +1 -0
- package/dts/chart/legend/Legend.d.ts +168 -0
- package/dts/chart/legend/Legend.d.ts.map +1 -0
- package/dts/chart/legend/index.d.ts +4 -0
- package/dts/chart/legend/index.d.ts.map +1 -0
- package/dts/chart/line/DottedLine.d.ts.map +1 -1
- package/dts/chart/line/Line.d.ts +23 -19
- package/dts/chart/line/Line.d.ts.map +1 -1
- package/dts/chart/line/LineChart.d.ts +26 -9
- package/dts/chart/line/LineChart.d.ts.map +1 -1
- package/dts/chart/line/ReferenceLine.d.ts +1 -0
- package/dts/chart/line/ReferenceLine.d.ts.map +1 -1
- package/dts/chart/line/SolidLine.d.ts.map +1 -1
- package/dts/chart/point/Point.d.ts +26 -2
- package/dts/chart/point/Point.d.ts.map +1 -1
- package/dts/chart/scrubber/DefaultScrubberBeacon.d.ts +32 -2
- package/dts/chart/scrubber/DefaultScrubberBeacon.d.ts.map +1 -1
- package/dts/chart/scrubber/DefaultScrubberLabel.d.ts +2 -1
- package/dts/chart/scrubber/DefaultScrubberLabel.d.ts.map +1 -1
- package/dts/chart/scrubber/Scrubber.d.ts +86 -17
- package/dts/chart/scrubber/Scrubber.d.ts.map +1 -1
- package/dts/chart/scrubber/ScrubberAccessibilityView.d.ts +12 -0
- package/dts/chart/scrubber/ScrubberAccessibilityView.d.ts.map +1 -0
- package/dts/chart/scrubber/ScrubberBeaconGroup.d.ts +10 -0
- package/dts/chart/scrubber/ScrubberBeaconGroup.d.ts.map +1 -1
- package/dts/chart/scrubber/ScrubberBeaconLabelGroup.d.ts +16 -1
- package/dts/chart/scrubber/ScrubberBeaconLabelGroup.d.ts.map +1 -1
- package/dts/chart/scrubber/ScrubberProvider.d.ts.map +1 -1
- package/dts/chart/utils/axis.d.ts +45 -10
- package/dts/chart/utils/axis.d.ts.map +1 -1
- package/dts/chart/utils/bar.d.ts +190 -0
- package/dts/chart/utils/bar.d.ts.map +1 -1
- package/dts/chart/utils/chart.d.ts +32 -0
- package/dts/chart/utils/chart.d.ts.map +1 -1
- package/dts/chart/utils/context.d.ts +21 -6
- package/dts/chart/utils/context.d.ts.map +1 -1
- package/dts/chart/utils/gradient.d.ts +3 -1
- package/dts/chart/utils/gradient.d.ts.map +1 -1
- package/dts/chart/utils/path.d.ts +26 -0
- package/dts/chart/utils/path.d.ts.map +1 -1
- package/dts/chart/utils/point.d.ts +24 -12
- package/dts/chart/utils/point.d.ts.map +1 -1
- package/dts/chart/utils/scale.d.ts +11 -0
- package/dts/chart/utils/scale.d.ts.map +1 -1
- package/dts/chart/utils/scrubber.d.ts +2 -1
- package/dts/chart/utils/scrubber.d.ts.map +1 -1
- package/dts/chart/utils/transition.d.ts +63 -22
- package/dts/chart/utils/transition.d.ts.map +1 -1
- package/dts/sparkline/Sparkline.d.ts +2 -1
- package/dts/sparkline/Sparkline.d.ts.map +1 -1
- package/dts/sparkline/SparklineArea.d.ts +2 -1
- package/dts/sparkline/SparklineArea.d.ts.map +1 -1
- package/dts/sparkline/SparklineGradient.d.ts +2 -1
- package/dts/sparkline/SparklineGradient.d.ts.map +1 -1
- package/dts/sparkline/sparkline-interactive/SparklineInteractive.d.ts +2 -1
- package/dts/sparkline/sparkline-interactive/SparklineInteractive.d.ts.map +1 -1
- package/esm/chart/CartesianChart.js +176 -82
- package/esm/chart/ChartContextBridge.js +14 -3
- package/esm/chart/ChartProvider.js +2 -2
- package/esm/chart/Path.js +34 -29
- package/esm/chart/PeriodSelector.js +6 -2
- package/esm/chart/__stories__/CartesianChart.stories.js +27 -86
- package/esm/chart/__stories__/ChartAccessibility.stories.js +721 -0
- package/esm/chart/__stories__/ChartTransitions.stories.js +625 -0
- package/esm/chart/__stories__/PeriodSelector.stories.js +102 -4
- package/esm/chart/area/Area.js +21 -9
- package/esm/chart/area/AreaChart.js +18 -13
- package/esm/chart/area/DottedArea.js +28 -18
- package/esm/chart/area/GradientArea.js +14 -7
- package/esm/chart/area/SolidArea.js +6 -2
- package/esm/chart/area/__stories__/AreaChart.stories.js +47 -5
- package/esm/chart/axis/Axis.js +5 -41
- package/esm/chart/axis/XAxis.js +116 -47
- package/esm/chart/axis/YAxis.js +105 -26
- package/esm/chart/axis/__stories__/Axis.stories.js +324 -48
- package/esm/chart/bar/Bar.js +17 -15
- package/esm/chart/bar/BarChart.js +38 -33
- package/esm/chart/bar/BarPlot.js +40 -45
- package/esm/chart/bar/BarStack.js +92 -475
- package/esm/chart/bar/BarStackGroup.js +37 -27
- package/esm/chart/bar/DefaultBar.js +27 -18
- package/esm/chart/bar/DefaultBarStack.js +25 -9
- package/esm/chart/bar/__stories__/BarChart.stories.js +728 -54
- package/esm/chart/gradient/Gradient.js +2 -1
- package/esm/chart/index.js +1 -0
- package/esm/chart/legend/DefaultLegendEntry.js +42 -0
- package/esm/chart/legend/DefaultLegendShape.js +64 -0
- package/esm/chart/legend/Legend.js +59 -0
- package/esm/chart/legend/__stories__/Legend.stories.js +574 -0
- package/esm/chart/legend/index.js +3 -0
- package/esm/chart/line/DottedLine.js +6 -2
- package/esm/chart/line/Line.js +42 -38
- package/esm/chart/line/LineChart.js +36 -12
- package/esm/chart/line/SolidLine.js +6 -2
- package/esm/chart/line/__stories__/LineChart.stories.js +241 -594
- package/esm/chart/line/__stories__/ReferenceLine.stories.js +95 -1
- package/esm/chart/point/Point.js +35 -36
- package/esm/chart/scrubber/DefaultScrubberBeacon.js +41 -38
- package/esm/chart/scrubber/DefaultScrubberLabel.js +26 -10
- package/esm/chart/scrubber/Scrubber.js +67 -35
- package/esm/chart/scrubber/ScrubberAccessibilityView.js +177 -0
- package/esm/chart/scrubber/ScrubberBeaconGroup.js +30 -22
- package/esm/chart/scrubber/ScrubberBeaconLabelGroup.js +35 -8
- package/esm/chart/scrubber/ScrubberProvider.js +29 -24
- package/esm/chart/scrubber/__stories__/Scrubber.stories.js +946 -0
- package/esm/chart/utils/axis.js +88 -44
- package/esm/chart/utils/bar.js +820 -0
- package/esm/chart/utils/chart.js +34 -7
- package/esm/chart/utils/context.js +7 -0
- package/esm/chart/utils/gradient.js +8 -4
- package/esm/chart/utils/path.js +91 -61
- package/esm/chart/utils/point.js +92 -39
- package/esm/chart/utils/scale.js +13 -2
- package/esm/chart/utils/scrubber.js +12 -5
- package/esm/chart/utils/transition.js +108 -60
- package/esm/sparkline/Sparkline.js +2 -1
- package/esm/sparkline/SparklineArea.js +2 -1
- package/esm/sparkline/SparklineGradient.js +2 -1
- package/esm/sparkline/__figma__/Sparkline.figma.js +1 -1
- package/esm/sparkline/__stories__/Sparkline.stories.js +11 -7
- package/esm/sparkline/__stories__/SparklineGradient.stories.js +7 -4
- package/esm/sparkline/sparkline-interactive/SparklineInteractive.js +2 -1
- package/esm/sparkline/sparkline-interactive/__figma__/SparklineInteractive.figma.js +1 -1
- package/esm/sparkline/sparkline-interactive/__stories__/SparklineInteractive.stories.js +51 -26
- package/esm/sparkline/sparkline-interactive-header/__figma__/SparklineInteractiveHeader.figma.js +1 -1
- package/esm/sparkline/sparkline-interactive-header/__stories__/SparklineInteractiveHeader.stories.js +19 -9
- package/package.json +13 -10
- package/esm/chart/__stories__/Chart.stories.js +0 -77
package/esm/chart/utils/bar.js
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
1
|
+
const _excluded = ["staggerDelay"];
|
|
2
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
3
|
+
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
|
|
4
|
+
import { defaultAxisId as fallbackAxisId } from './axis';
|
|
5
|
+
import { evaluateGradientAtValue } from './gradient';
|
|
6
|
+
import { defaultTransition } from './transition';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A bar-specific transition that extends Transition with stagger support.
|
|
10
|
+
* When `staggerDelay` is provided, bars will animate with increasing delays
|
|
11
|
+
* based on their position along the category axis (vertical: left-to-right,
|
|
12
|
+
* horizontal: top-to-bottom).
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Bars stagger in from left to right over 250ms, each animating for 750ms
|
|
16
|
+
* { type: 'timing', duration: 750, staggerDelay: 250 }
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Computes a bar's normalized [0, 1] position along the category axis, used for
|
|
21
|
+
* stagger-delay calculations.
|
|
22
|
+
*
|
|
23
|
+
* Vertical charts stagger left-to-right (x axis); horizontal charts stagger
|
|
24
|
+
* top-to-bottom (y axis). Returns 0 when the drawing area has no extent.
|
|
25
|
+
*
|
|
26
|
+
* @param layout - The layout of the chart
|
|
27
|
+
* @param x - Bar's left edge in pixels
|
|
28
|
+
* @param y - Bar's top edge in pixels
|
|
29
|
+
*/
|
|
30
|
+
export const getNormalizedStagger = (layout, x, y, drawingArea) => {
|
|
31
|
+
if (layout === 'horizontal') {
|
|
32
|
+
return drawingArea.height > 0 ? (y - drawingArea.y) / drawingArea.height : 0;
|
|
33
|
+
}
|
|
34
|
+
return drawingArea.width > 0 ? (x - drawingArea.x) / drawingArea.width : 0;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Strips `staggerDelay` from a transition and computes a positional delay.
|
|
39
|
+
*
|
|
40
|
+
* @param transition - The transition config (may include staggerDelay)
|
|
41
|
+
* @param normalizedPosition - The bar's normalized position along the category axis (0–1)
|
|
42
|
+
* @returns A standard Transition with computed delay
|
|
43
|
+
*/
|
|
44
|
+
export const withStaggerDelayTransition = (transition, normalizedPosition) => {
|
|
45
|
+
var _baseTransition$delay;
|
|
46
|
+
if (!transition) return null;
|
|
47
|
+
const {
|
|
48
|
+
staggerDelay
|
|
49
|
+
} = transition,
|
|
50
|
+
baseTransition = _objectWithoutPropertiesLoose(transition, _excluded);
|
|
51
|
+
if (!staggerDelay) return transition;
|
|
52
|
+
return _extends({}, baseTransition, {
|
|
53
|
+
delay: ((_baseTransition$delay = baseTransition == null ? void 0 : baseTransition.delay) != null ? _baseTransition$delay : 0) + normalizedPosition * staggerDelay
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Default bar enter transition. Uses the default spring with a stagger delay
|
|
59
|
+
* so bars spring into place from left to right.
|
|
60
|
+
* `{ type: 'spring', stiffness: 900, damping: 120, staggerDelay: 250 }`
|
|
61
|
+
*/
|
|
62
|
+
export const defaultBarEnterTransition = _extends({}, defaultTransition, {
|
|
63
|
+
staggerDelay: 250
|
|
64
|
+
});
|
|
65
|
+
|
|
1
66
|
/**
|
|
2
67
|
* Calculates the size adjustment needed for bars when accounting for gaps between them.
|
|
3
68
|
* This function helps determine how much to reduce each bar's width to accommodate
|
|
@@ -21,4 +86,759 @@ export function getBarSizeAdjustment(barCount, gapSize) {
|
|
|
21
86
|
return 0;
|
|
22
87
|
}
|
|
23
88
|
return gapSize * (barCount - 1) / barCount;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Groups bar series into stack groups scoped by stackId + axis IDs.
|
|
92
|
+
*
|
|
93
|
+
* Series with no `stackId` are treated as independent stacks keyed by series id.
|
|
94
|
+
* Axis IDs are included in the group key so series on different axes never stack together.
|
|
95
|
+
*/
|
|
96
|
+
export function getStackGroups(series, defaultAxisId) {
|
|
97
|
+
if (defaultAxisId === void 0) {
|
|
98
|
+
defaultAxisId = fallbackAxisId;
|
|
99
|
+
}
|
|
100
|
+
const groups = {};
|
|
101
|
+
series.forEach(entry => {
|
|
102
|
+
var _entry$xAxisId, _entry$yAxisId;
|
|
103
|
+
const xAxisId = (_entry$xAxisId = entry.xAxisId) != null ? _entry$xAxisId : defaultAxisId;
|
|
104
|
+
const yAxisId = (_entry$yAxisId = entry.yAxisId) != null ? _entry$yAxisId : defaultAxisId;
|
|
105
|
+
const stackId = entry.stackId || "individual-" + entry.id;
|
|
106
|
+
const stackKey = stackId + ":" + xAxisId + ":" + yAxisId;
|
|
107
|
+
if (!groups[stackKey]) {
|
|
108
|
+
groups[stackKey] = {
|
|
109
|
+
stackId: stackKey,
|
|
110
|
+
series: [],
|
|
111
|
+
xAxisId: entry.xAxisId,
|
|
112
|
+
yAxisId: entry.yAxisId
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
groups[stackKey].series.push(entry);
|
|
116
|
+
});
|
|
117
|
+
return Object.values(groups);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Applies proportional gap distribution to a stack of bars, maintaining total stack length.
|
|
121
|
+
* Gaps are only inserted between bars that have `shouldApplyGap = true`.
|
|
122
|
+
* Positive (above-baseline) and negative (below-baseline) groups are gapped independently.
|
|
123
|
+
*
|
|
124
|
+
* @param bars - Array of bar items with current valuePos and length
|
|
125
|
+
* @param stackGap - Gap size in pixels between adjacent bars
|
|
126
|
+
* @param layout - The layout of the chart
|
|
127
|
+
* @param baseline - Pixel position of the zero value on the value axis
|
|
128
|
+
* @returns New array of bars with adjusted valuePos and length
|
|
129
|
+
*/
|
|
130
|
+
function applyStackGap(bars, stackGap, layout, baseline) {
|
|
131
|
+
if (!stackGap || bars.length <= 1) return bars;
|
|
132
|
+
const result = [...bars];
|
|
133
|
+
const barsAboveBaseline = bars.filter(bar => {
|
|
134
|
+
const [bottom, top] = [...bar.dataValue].sort((a, b) => a - b);
|
|
135
|
+
return bottom >= 0 && top !== bottom && bar.shouldApplyGap;
|
|
136
|
+
});
|
|
137
|
+
const barsBelowBaseline = bars.filter(bar => {
|
|
138
|
+
const [bottom, top] = [...bar.dataValue].sort((a, b) => a - b);
|
|
139
|
+
return top <= 0 && bottom !== top && bar.shouldApplyGap;
|
|
140
|
+
});
|
|
141
|
+
const applyGapGroup = (group, growing) => {
|
|
142
|
+
if (group.length <= 1) return;
|
|
143
|
+
const totalGapSpace = stackGap * (group.length - 1);
|
|
144
|
+
const totalDataLength = group.reduce((sum, bar) => sum + bar.length, 0);
|
|
145
|
+
const lengthReduction = totalGapSpace / totalDataLength;
|
|
146
|
+
const sortedBars = growing ? [...group].sort((a, b) => b.valuePos - a.valuePos) : [...group].sort((a, b) => a.valuePos - b.valuePos);
|
|
147
|
+
let currentEdge = baseline;
|
|
148
|
+
sortedBars.forEach((bar, index) => {
|
|
149
|
+
const newLength = bar.length * (1 - lengthReduction);
|
|
150
|
+
let newValuePos;
|
|
151
|
+
if (growing) {
|
|
152
|
+
newValuePos = currentEdge - newLength;
|
|
153
|
+
currentEdge = newValuePos - (index < sortedBars.length - 1 ? stackGap : 0);
|
|
154
|
+
} else {
|
|
155
|
+
newValuePos = currentEdge;
|
|
156
|
+
currentEdge = newValuePos + newLength + (index < sortedBars.length - 1 ? stackGap : 0);
|
|
157
|
+
}
|
|
158
|
+
const barIndex = result.findIndex(b => b.seriesId === bar.seriesId);
|
|
159
|
+
if (barIndex !== -1) {
|
|
160
|
+
result[barIndex] = _extends({}, result[barIndex], {
|
|
161
|
+
length: newLength,
|
|
162
|
+
valuePos: newValuePos
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Positive bars: grow up in vertical (decreasing Y), grow right in horizontal (increasing X)
|
|
169
|
+
applyGapGroup(barsAboveBaseline, layout === 'vertical');
|
|
170
|
+
// Negative bars: grow down in vertical (increasing Y), grow left in horizontal (decreasing X)
|
|
171
|
+
applyGapGroup(barsBelowBaseline, layout !== 'vertical');
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Computes per-bar initial animation origin positions for bar entrance animations.
|
|
177
|
+
*
|
|
178
|
+
* Bars are stacked from the baseline in their respective directions so they start at
|
|
179
|
+
* distinct, non-overlapping positions with the gap already applied:
|
|
180
|
+
* - Positive bars: stack rightward (horizontal) / upward (vertical) from the baseline.
|
|
181
|
+
* - Negative bars: stack leftward (horizontal) / downward (vertical) from the baseline.
|
|
182
|
+
*
|
|
183
|
+
* The bar closest to the baseline always gets index 0 and starts exactly at the baseline.
|
|
184
|
+
*
|
|
185
|
+
* @param bars - Array of bar items with final valuePos, length, and dataValue
|
|
186
|
+
* @param initialBarMinSizes - Per-bar initial sizes in pixels for entrance animation
|
|
187
|
+
* @param stackGap - Gap between adjacent bars in pixels
|
|
188
|
+
* @param baseline - Pixel position of the zero value on the value axis
|
|
189
|
+
* @param layout - The layout of the chart
|
|
190
|
+
* @returns Array of origin positions (one per bar, parallel to input), all defaulting to baseline
|
|
191
|
+
*/
|
|
192
|
+
function getBarOrigins(bars, initialBarMinSizes, stackGap, baseline, layout) {
|
|
193
|
+
const result = bars.map(() => baseline);
|
|
194
|
+
if (bars.length === 0 || initialBarMinSizes.every(size => !size)) return result;
|
|
195
|
+
const isPositive = bar => {
|
|
196
|
+
const [lo, hi] = [...bar.dataValue].sort((a, b) => a - b);
|
|
197
|
+
return lo >= 0 && hi !== lo;
|
|
198
|
+
};
|
|
199
|
+
const isNegative = bar => {
|
|
200
|
+
const [lo, hi] = [...bar.dataValue].sort((a, b) => a - b);
|
|
201
|
+
return hi <= 0 && hi !== lo;
|
|
202
|
+
};
|
|
203
|
+
const positiveBars = bars.map((bar, i) => ({
|
|
204
|
+
bar,
|
|
205
|
+
i
|
|
206
|
+
})).filter(_ref => {
|
|
207
|
+
let {
|
|
208
|
+
bar
|
|
209
|
+
} = _ref;
|
|
210
|
+
return isPositive(bar);
|
|
211
|
+
}).sort((a, b) => layout === 'vertical' ? b.bar.valuePos - a.bar.valuePos : a.bar.valuePos - b.bar.valuePos);
|
|
212
|
+
if (layout === 'vertical') {
|
|
213
|
+
let currentPositive = baseline;
|
|
214
|
+
positiveBars.forEach((_ref2, idx) => {
|
|
215
|
+
var _initialBarMinSizes$i;
|
|
216
|
+
let {
|
|
217
|
+
i
|
|
218
|
+
} = _ref2;
|
|
219
|
+
const initialSize = (_initialBarMinSizes$i = initialBarMinSizes[i]) != null ? _initialBarMinSizes$i : 0;
|
|
220
|
+
currentPositive -= initialSize;
|
|
221
|
+
result[i] = currentPositive;
|
|
222
|
+
if (idx < positiveBars.length - 1) {
|
|
223
|
+
currentPositive -= stackGap;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
let currentPositive = baseline;
|
|
228
|
+
positiveBars.forEach((_ref3, idx) => {
|
|
229
|
+
var _initialBarMinSizes$i2;
|
|
230
|
+
let {
|
|
231
|
+
i
|
|
232
|
+
} = _ref3;
|
|
233
|
+
const initialSize = (_initialBarMinSizes$i2 = initialBarMinSizes[i]) != null ? _initialBarMinSizes$i2 : 0;
|
|
234
|
+
result[i] = currentPositive;
|
|
235
|
+
currentPositive += initialSize;
|
|
236
|
+
if (idx < positiveBars.length - 1) {
|
|
237
|
+
currentPositive += stackGap;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const negativeBars = bars.map((bar, i) => ({
|
|
242
|
+
bar,
|
|
243
|
+
i
|
|
244
|
+
})).filter(_ref4 => {
|
|
245
|
+
let {
|
|
246
|
+
bar
|
|
247
|
+
} = _ref4;
|
|
248
|
+
return isNegative(bar);
|
|
249
|
+
}).sort((a, b) => layout === 'vertical' ? a.bar.valuePos - b.bar.valuePos : b.bar.valuePos + b.bar.length - (a.bar.valuePos + a.bar.length));
|
|
250
|
+
if (layout === 'vertical') {
|
|
251
|
+
let currentNegative = baseline;
|
|
252
|
+
negativeBars.forEach((_ref5, idx) => {
|
|
253
|
+
var _initialBarMinSizes$i3;
|
|
254
|
+
let {
|
|
255
|
+
i
|
|
256
|
+
} = _ref5;
|
|
257
|
+
const initialSize = (_initialBarMinSizes$i3 = initialBarMinSizes[i]) != null ? _initialBarMinSizes$i3 : 0;
|
|
258
|
+
result[i] = currentNegative;
|
|
259
|
+
currentNegative += initialSize;
|
|
260
|
+
if (idx < negativeBars.length - 1) {
|
|
261
|
+
currentNegative += stackGap;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
let currentNegative = baseline;
|
|
266
|
+
negativeBars.forEach((_ref6, idx) => {
|
|
267
|
+
var _initialBarMinSizes$i4;
|
|
268
|
+
let {
|
|
269
|
+
i
|
|
270
|
+
} = _ref6;
|
|
271
|
+
const initialSize = (_initialBarMinSizes$i4 = initialBarMinSizes[i]) != null ? _initialBarMinSizes$i4 : 0;
|
|
272
|
+
currentNegative -= initialSize;
|
|
273
|
+
result[i] = currentNegative;
|
|
274
|
+
if (idx < negativeBars.length - 1) {
|
|
275
|
+
currentNegative -= stackGap;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Computes stack clip origin [start, end] that covers the bounding box
|
|
284
|
+
* of all bars at their stacked starting positions (as computed by `getBarOrigins`).
|
|
285
|
+
*
|
|
286
|
+
* This is passed to `DefaultBarStack` so the clip animation starts in sync with the
|
|
287
|
+
* individual bar animations — no bars leak outside the clip on frame 0.
|
|
288
|
+
*
|
|
289
|
+
* @param barOrigins - Per-bar initial origins from `getBarOrigins`
|
|
290
|
+
* @param barMinSizes - Per-bar minimum sizes in pixels (or a uniform value)
|
|
291
|
+
* @returns [originStart, originEnd] or undefined when barMinSize is 0 / no bars
|
|
292
|
+
*/
|
|
293
|
+
export function getStackOrigin(barOrigins, barMinSizes) {
|
|
294
|
+
if (barOrigins.length === 0) return undefined;
|
|
295
|
+
const minSizes = Array.isArray(barMinSizes) ? barMinSizes : barOrigins.map(() => barMinSizes);
|
|
296
|
+
let rangeStart = Number.POSITIVE_INFINITY;
|
|
297
|
+
let rangeEnd = Number.NEGATIVE_INFINITY;
|
|
298
|
+
for (let i = 0; i < barOrigins.length; i++) {
|
|
299
|
+
var _minSizes$i;
|
|
300
|
+
const minSize = (_minSizes$i = minSizes[i]) != null ? _minSizes$i : 0;
|
|
301
|
+
if (minSize <= 0) continue;
|
|
302
|
+
const barStart = barOrigins[i];
|
|
303
|
+
const barEnd = barStart + minSize;
|
|
304
|
+
rangeStart = Math.min(rangeStart, barStart, barEnd);
|
|
305
|
+
rangeEnd = Math.max(rangeEnd, barStart, barEnd);
|
|
306
|
+
}
|
|
307
|
+
if (!Number.isFinite(rangeStart) || !Number.isFinite(rangeEnd)) return undefined;
|
|
308
|
+
return [rangeStart, rangeEnd];
|
|
309
|
+
}
|
|
310
|
+
function getInitialBarMinSizes(bars, barMinSize, stackMinSize) {
|
|
311
|
+
const perBarMinFromBarMinSize = barMinSize != null ? barMinSize : 0;
|
|
312
|
+
if (bars.length === 0) return [];
|
|
313
|
+
if (!stackMinSize) {
|
|
314
|
+
return bars.map(() => perBarMinFromBarMinSize);
|
|
315
|
+
}
|
|
316
|
+
const totalBarLength = bars.reduce((sum, bar) => sum + bar.length, 0);
|
|
317
|
+
const perBarMinFromStack = totalBarLength ? bars.map(bar => stackMinSize * bar.length / totalBarLength) : bars.map(() => stackMinSize / bars.length);
|
|
318
|
+
return perBarMinFromStack.map(stackMin => Math.max(perBarMinFromBarMinSize, stackMin));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Computes the initial clip rect used for stack enter animations.
|
|
323
|
+
*/
|
|
324
|
+
export function getStackInitialClipRect(stackRect, layout, origin) {
|
|
325
|
+
const {
|
|
326
|
+
x,
|
|
327
|
+
y,
|
|
328
|
+
width,
|
|
329
|
+
height
|
|
330
|
+
} = stackRect;
|
|
331
|
+
if (Array.isArray(origin)) {
|
|
332
|
+
const [originStart, originEnd] = origin;
|
|
333
|
+
if (layout === 'vertical') {
|
|
334
|
+
return {
|
|
335
|
+
x,
|
|
336
|
+
y: originStart,
|
|
337
|
+
width,
|
|
338
|
+
height: originEnd - originStart
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
x: originStart,
|
|
343
|
+
y,
|
|
344
|
+
width: originEnd - originStart,
|
|
345
|
+
height
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const initialSize = 1;
|
|
349
|
+
if (layout === 'vertical') {
|
|
350
|
+
const valueBaseline = origin != null ? origin : y + height;
|
|
351
|
+
return {
|
|
352
|
+
x,
|
|
353
|
+
y: valueBaseline,
|
|
354
|
+
width,
|
|
355
|
+
height: initialSize
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const valueBaseline = origin != null ? origin : x;
|
|
359
|
+
return {
|
|
360
|
+
x: valueBaseline,
|
|
361
|
+
y,
|
|
362
|
+
width: initialSize,
|
|
363
|
+
height
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Expands bars that are shorter than `barMinSize` to the minimum size.
|
|
369
|
+
* Non-expanded bars are scaled down proportionally to keep the total bar length constant,
|
|
370
|
+
* preventing stacked bars from overflowing the chart area.
|
|
371
|
+
*
|
|
372
|
+
* Bars are then repositioned from the baseline, preserving original gaps between them.
|
|
373
|
+
*
|
|
374
|
+
* @param bars - Array of bar items with current valuePos and length
|
|
375
|
+
* @param barMinSize - Minimum bar size in pixels
|
|
376
|
+
* @param layout - The layout of the chart
|
|
377
|
+
* @param baseline - Pixel position of the zero value on the value axis
|
|
378
|
+
* @returns New array of bars with adjusted valuePos and length
|
|
379
|
+
*/
|
|
380
|
+
function applyBarMinSize(bars, barMinSize, layout, baseline) {
|
|
381
|
+
if (!barMinSize || bars.length === 0) return bars;
|
|
382
|
+
const originalTotalLength = bars.reduce((sum, bar) => sum + bar.length, 0);
|
|
383
|
+
const needsExpansion = bars.map(bar => bar.length < barMinSize);
|
|
384
|
+
const expandedTotalLength = bars.reduce((sum, bar, i) => sum + (needsExpansion[i] ? barMinSize : bar.length), 0);
|
|
385
|
+
let finalLengths;
|
|
386
|
+
if (expandedTotalLength > originalTotalLength) {
|
|
387
|
+
// Scale down non-expanded bars to keep total bar length constant
|
|
388
|
+
const spaceForExpanded = needsExpansion.filter(Boolean).length * barMinSize;
|
|
389
|
+
const spaceForNonExpanded = Math.max(0, originalTotalLength - spaceForExpanded);
|
|
390
|
+
const nonExpandedOrigTotal = bars.reduce((sum, bar, i) => !needsExpansion[i] ? sum + bar.length : sum, 0);
|
|
391
|
+
const scaleFactor = nonExpandedOrigTotal > 0 ? spaceForNonExpanded / nonExpandedOrigTotal : 0;
|
|
392
|
+
finalLengths = bars.map((bar, i) => needsExpansion[i] ? barMinSize : bar.length * scaleFactor);
|
|
393
|
+
} else {
|
|
394
|
+
finalLengths = bars.map((bar, i) => needsExpansion[i] ? barMinSize : bar.length);
|
|
395
|
+
}
|
|
396
|
+
const expandedBars = bars.map((bar, i) => _extends({}, bar, {
|
|
397
|
+
length: finalLengths[i]
|
|
398
|
+
}));
|
|
399
|
+
const newPositions = new Map();
|
|
400
|
+
|
|
401
|
+
// Range bars (shouldApplyGap=false) float at data-defined coordinates independent of the
|
|
402
|
+
// baseline. Restacking them from the zero baseline would place them off-screen when the
|
|
403
|
+
// y-axis domain doesn't include 0 (e.g., a price chart with domain [28000, 37000]).
|
|
404
|
+
// Instead, expand them in-place, centered on their original midpoint.
|
|
405
|
+
for (let i = 0; i < bars.length; i++) {
|
|
406
|
+
if (bars[i].shouldApplyGap === false) {
|
|
407
|
+
const originalMid = bars[i].valuePos + bars[i].length / 2;
|
|
408
|
+
newPositions.set(bars[i].seriesId, {
|
|
409
|
+
valuePos: originalMid - expandedBars[i].length / 2,
|
|
410
|
+
length: expandedBars[i].length
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Stacked bars (shouldApplyGap=true/undefined): classify by which side of the baseline
|
|
416
|
+
// they're on and restack from the baseline outward.
|
|
417
|
+
const stackedSortedBars = [...expandedBars].filter(bar => bar.shouldApplyGap !== false).sort((a, b) => a.valuePos - b.valuePos);
|
|
418
|
+
if (stackedSortedBars.length > 0) {
|
|
419
|
+
// Classify using dataValue to correctly identify which side of the baseline each bar is on,
|
|
420
|
+
// independent of the current valuePos (which hasn't been repositioned yet).
|
|
421
|
+
const barsAboveBaseline = stackedSortedBars.filter(bar => {
|
|
422
|
+
const [bottom, top] = [...bar.dataValue].sort((a, b) => a - b);
|
|
423
|
+
return layout === 'vertical' ? bottom >= 0 && top !== bottom : top <= 0 && top !== bottom;
|
|
424
|
+
});
|
|
425
|
+
const barsBelowBaseline = stackedSortedBars.filter(bar => {
|
|
426
|
+
const [bottom, top] = [...bar.dataValue].sort((a, b) => a - b);
|
|
427
|
+
return layout === 'vertical' ? top <= 0 && top !== bottom : bottom >= 0 && top !== bottom;
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Restack bars above baseline (growing away from it in the positive direction)
|
|
431
|
+
let currentAbove = baseline;
|
|
432
|
+
for (let i = barsAboveBaseline.length - 1; i >= 0; i--) {
|
|
433
|
+
const bar = barsAboveBaseline[i];
|
|
434
|
+
const newValuePos = currentAbove - bar.length;
|
|
435
|
+
newPositions.set(bar.seriesId, {
|
|
436
|
+
valuePos: newValuePos,
|
|
437
|
+
length: bar.length
|
|
438
|
+
});
|
|
439
|
+
if (i > 0) {
|
|
440
|
+
const nextBar = barsAboveBaseline[i - 1];
|
|
441
|
+
const originalCurrent = bars.find(b => b.seriesId === bar.seriesId);
|
|
442
|
+
const originalNext = bars.find(b => b.seriesId === nextBar.seriesId);
|
|
443
|
+
const originalGap = originalCurrent.valuePos - (originalNext.valuePos + originalNext.length);
|
|
444
|
+
currentAbove = newValuePos - originalGap;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Restack bars below baseline (growing away from it in the negative direction)
|
|
449
|
+
let currentBelow = baseline;
|
|
450
|
+
for (let i = 0; i < barsBelowBaseline.length; i++) {
|
|
451
|
+
const bar = barsBelowBaseline[i];
|
|
452
|
+
newPositions.set(bar.seriesId, {
|
|
453
|
+
valuePos: currentBelow,
|
|
454
|
+
length: bar.length
|
|
455
|
+
});
|
|
456
|
+
if (i < barsBelowBaseline.length - 1) {
|
|
457
|
+
const nextBar = barsBelowBaseline[i + 1];
|
|
458
|
+
const originalCurrent = bars.find(b => b.seriesId === bar.seriesId);
|
|
459
|
+
const originalNext = bars.find(b => b.seriesId === nextBar.seriesId);
|
|
460
|
+
const originalGap = originalNext.valuePos - (originalCurrent.valuePos + originalCurrent.length);
|
|
461
|
+
currentBelow = currentBelow + bar.length + originalGap;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return expandedBars.map(bar => {
|
|
466
|
+
const newPos = newPositions.get(bar.seriesId);
|
|
467
|
+
if (newPos) return _extends({}, bar, {
|
|
468
|
+
valuePos: newPos.valuePos,
|
|
469
|
+
length: newPos.length
|
|
470
|
+
});
|
|
471
|
+
return bar;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Scales a stack of bars up so the total stack extent meets `stackMinSize`.
|
|
477
|
+
* For a single bar, the bar is expanded away from the baseline.
|
|
478
|
+
* For multiple bars, all bars are scaled proportionally, preserving relative gaps.
|
|
479
|
+
*
|
|
480
|
+
* @param bars - Array of bar items with current valuePos and length
|
|
481
|
+
* @param stackMinSize - Minimum stack size in pixels
|
|
482
|
+
* @param stackSize - Current total pixel extent of the stack
|
|
483
|
+
* @param stackBounds - Current bounding rect of the stack
|
|
484
|
+
* @param layout - The layout of the chart
|
|
485
|
+
* @param indexPos - Pixel position along the categorical (index) axis
|
|
486
|
+
* @param thickness - Bar thickness in pixels
|
|
487
|
+
* @param baseline - Pixel position of the zero value on the value axis
|
|
488
|
+
* @returns Updated bars and stackBounds; unchanged if stackSize >= stackMinSize
|
|
489
|
+
*/
|
|
490
|
+
function applyStackMinSize(bars, stackMinSize, stackSize, stackBounds, layout, indexPos, thickness, baseline) {
|
|
491
|
+
if (!stackMinSize || stackSize >= stackMinSize) return {
|
|
492
|
+
bars,
|
|
493
|
+
stackBounds
|
|
494
|
+
};
|
|
495
|
+
if (bars.length === 0) return {
|
|
496
|
+
bars,
|
|
497
|
+
stackBounds
|
|
498
|
+
};
|
|
499
|
+
let updatedBars = [...bars];
|
|
500
|
+
let updatedBounds = _extends({}, stackBounds);
|
|
501
|
+
if (bars.length === 1) {
|
|
502
|
+
const bar = bars[0];
|
|
503
|
+
const sizeIncrease = stackMinSize - bar.length;
|
|
504
|
+
const [bottom, top] = [...bar.dataValue].sort((a, b) => a - b);
|
|
505
|
+
let newValuePos;
|
|
506
|
+
const newLength = stackMinSize;
|
|
507
|
+
if (bottom >= 0 && top !== bottom) {
|
|
508
|
+
// Bar is on the positive side: vertical→expands upward (↑), horizontal→expands rightward (→)
|
|
509
|
+
newValuePos = layout === 'vertical' ? bar.valuePos - sizeIncrease : bar.valuePos;
|
|
510
|
+
} else if (top <= 0 && top !== bottom) {
|
|
511
|
+
// Bar is on the negative side: vertical→expands downward (↓), horizontal→expands leftward (←)
|
|
512
|
+
newValuePos = layout === 'vertical' ? bar.valuePos : bar.valuePos - sizeIncrease;
|
|
513
|
+
} else {
|
|
514
|
+
// Bar spans baseline or is zero: expand equally in both directions
|
|
515
|
+
newValuePos = bar.valuePos - sizeIncrease / 2;
|
|
516
|
+
}
|
|
517
|
+
updatedBars = [_extends({}, bar, {
|
|
518
|
+
valuePos: newValuePos,
|
|
519
|
+
length: newLength
|
|
520
|
+
})];
|
|
521
|
+
updatedBounds = {
|
|
522
|
+
x: layout === 'vertical' ? indexPos : newValuePos,
|
|
523
|
+
y: layout === 'vertical' ? newValuePos : indexPos,
|
|
524
|
+
width: layout === 'vertical' ? thickness : newLength,
|
|
525
|
+
height: layout === 'vertical' ? newLength : thickness
|
|
526
|
+
};
|
|
527
|
+
} else {
|
|
528
|
+
const totalBarLength = bars.reduce((sum, bar) => sum + bar.length, 0);
|
|
529
|
+
const totalGapLength = stackSize - totalBarLength;
|
|
530
|
+
const requiredBarLength = stackMinSize - totalGapLength;
|
|
531
|
+
const barScaleFactor = requiredBarLength / totalBarLength;
|
|
532
|
+
const sortedBars = [...bars].sort((a, b) => a.valuePos - b.valuePos);
|
|
533
|
+
|
|
534
|
+
// For vertical: positive bars are above baseline (smaller Y), negative bars are below (larger Y)
|
|
535
|
+
// For horizontal: positive bars are right of baseline (larger X), negative bars are left (smaller X)
|
|
536
|
+
const barsOnPositiveSide = layout === 'vertical' ? sortedBars.filter(bar => bar.valuePos + bar.length <= baseline) : sortedBars.filter(bar => bar.valuePos >= baseline);
|
|
537
|
+
const barsOnNegativeSide = layout === 'vertical' ? sortedBars.filter(bar => bar.valuePos >= baseline) : sortedBars.filter(bar => bar.valuePos + bar.length <= baseline);
|
|
538
|
+
const newPositions = new Map();
|
|
539
|
+
if (layout === 'vertical') {
|
|
540
|
+
// Stack from baseline upward (decreasing valuePos) for positive bars
|
|
541
|
+
let currentPos = baseline;
|
|
542
|
+
for (let i = barsOnPositiveSide.length - 1; i >= 0; i--) {
|
|
543
|
+
const bar = barsOnPositiveSide[i];
|
|
544
|
+
const newLength = bar.length * barScaleFactor;
|
|
545
|
+
const newValuePos = currentPos - newLength;
|
|
546
|
+
newPositions.set(bar.seriesId, {
|
|
547
|
+
valuePos: newValuePos,
|
|
548
|
+
length: newLength
|
|
549
|
+
});
|
|
550
|
+
if (i > 0) {
|
|
551
|
+
const nextBar = barsOnPositiveSide[i - 1];
|
|
552
|
+
const originalGap = bar.valuePos - (nextBar.valuePos + nextBar.length);
|
|
553
|
+
currentPos = newValuePos - originalGap;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// Stack from baseline downward (increasing valuePos) for negative bars
|
|
557
|
+
let currentPosBelow = baseline;
|
|
558
|
+
for (let i = 0; i < barsOnNegativeSide.length; i++) {
|
|
559
|
+
const bar = barsOnNegativeSide[i];
|
|
560
|
+
const newLength = bar.length * barScaleFactor;
|
|
561
|
+
newPositions.set(bar.seriesId, {
|
|
562
|
+
valuePos: currentPosBelow,
|
|
563
|
+
length: newLength
|
|
564
|
+
});
|
|
565
|
+
if (i < barsOnNegativeSide.length - 1) {
|
|
566
|
+
const nextBar = barsOnNegativeSide[i + 1];
|
|
567
|
+
const originalGap = nextBar.valuePos - (bar.valuePos + bar.length);
|
|
568
|
+
currentPosBelow = currentPosBelow + newLength + originalGap;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
} else {
|
|
572
|
+
// Stack from baseline rightward (increasing valuePos) for positive bars
|
|
573
|
+
let currentPos = baseline;
|
|
574
|
+
for (let i = 0; i < barsOnPositiveSide.length; i++) {
|
|
575
|
+
const bar = barsOnPositiveSide[i];
|
|
576
|
+
const newLength = bar.length * barScaleFactor;
|
|
577
|
+
newPositions.set(bar.seriesId, {
|
|
578
|
+
valuePos: currentPos,
|
|
579
|
+
length: newLength
|
|
580
|
+
});
|
|
581
|
+
if (i < barsOnPositiveSide.length - 1) {
|
|
582
|
+
const nextBar = barsOnPositiveSide[i + 1];
|
|
583
|
+
const originalGap = nextBar.valuePos - (bar.valuePos + bar.length);
|
|
584
|
+
currentPos = currentPos + newLength + originalGap;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// Stack from baseline leftward (decreasing valuePos) for negative bars
|
|
588
|
+
let currentPosLeft = baseline;
|
|
589
|
+
for (let i = barsOnNegativeSide.length - 1; i >= 0; i--) {
|
|
590
|
+
const bar = barsOnNegativeSide[i];
|
|
591
|
+
const newLength = bar.length * barScaleFactor;
|
|
592
|
+
const newValuePos = currentPosLeft - newLength;
|
|
593
|
+
newPositions.set(bar.seriesId, {
|
|
594
|
+
valuePos: newValuePos,
|
|
595
|
+
length: newLength
|
|
596
|
+
});
|
|
597
|
+
if (i > 0) {
|
|
598
|
+
const nextBar = barsOnNegativeSide[i - 1];
|
|
599
|
+
const originalGap = bar.valuePos - (nextBar.valuePos + nextBar.length);
|
|
600
|
+
currentPosLeft = newValuePos - originalGap;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
updatedBars = bars.map(bar => {
|
|
605
|
+
const newPos = newPositions.get(bar.seriesId);
|
|
606
|
+
if (!newPos) return bar;
|
|
607
|
+
return _extends({}, bar, {
|
|
608
|
+
length: newPos.length,
|
|
609
|
+
valuePos: newPos.valuePos
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
const newMinValuePos = Math.min(...updatedBars.map(bar => bar.valuePos));
|
|
613
|
+
const newMaxValuePos = Math.max(...updatedBars.map(bar => bar.valuePos + bar.length));
|
|
614
|
+
updatedBounds = {
|
|
615
|
+
x: layout === 'vertical' ? indexPos : newMinValuePos,
|
|
616
|
+
y: layout === 'vertical' ? newMinValuePos : indexPos,
|
|
617
|
+
width: layout === 'vertical' ? thickness : newMaxValuePos - newMinValuePos,
|
|
618
|
+
height: layout === 'vertical' ? newMaxValuePos - newMinValuePos : thickness
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
bars: updatedBars,
|
|
623
|
+
stackBounds: updatedBounds
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Applies border-radius flags to a sorted stack of bars.
|
|
629
|
+
*
|
|
630
|
+
* Faces at the outer edges of the stack remain rounded; faces where two bars
|
|
631
|
+
* touch internally are squared. When `stackGap` is non-zero every face keeps
|
|
632
|
+
* its rounded corner because all bars are visually separated.
|
|
633
|
+
*
|
|
634
|
+
* @param bars - Bars with `roundTop`/`roundBottom` flags and position data
|
|
635
|
+
* @param layout - The layout of the chart
|
|
636
|
+
* @param stackGap - Pixel gap between adjacent bars (non-zero ⇒ all faces stay rounded)
|
|
637
|
+
* @returns New array of bars with corrected `roundTop`/`roundBottom` flags
|
|
638
|
+
*/
|
|
639
|
+
function applyBorderRadiusLogic(bars, layout, stackGap) {
|
|
640
|
+
if (bars.length === 0) return bars;
|
|
641
|
+
|
|
642
|
+
// Sort from "lower coordinate" face to "higher coordinate" face along the value axis:
|
|
643
|
+
// Vertical → descending valuePos (largest Y first = closest to baseline)
|
|
644
|
+
// Horizontal → ascending valuePos (smallest X first = closest to baseline)
|
|
645
|
+
const sortedBars = layout === 'vertical' ? [...bars].sort((a, b) => b.valuePos - a.valuePos) : [...bars].sort((a, b) => a.valuePos - b.valuePos);
|
|
646
|
+
return sortedBars.map((a, index) => {
|
|
647
|
+
const barBefore = index > 0 ? sortedBars[index - 1] : null;
|
|
648
|
+
const barAfter = index < sortedBars.length - 1 ? sortedBars[index + 1] : null;
|
|
649
|
+
|
|
650
|
+
// shouldRoundLower: face with the smaller coordinate (top in vertical, left in horizontal)
|
|
651
|
+
const shouldRoundLower = (layout === 'vertical' ? index === sortedBars.length - 1 : index === 0) || Boolean(a.shouldApplyGap && stackGap) || !a.shouldApplyGap && barAfter !== null && barAfter.valuePos + barAfter.length !== a.valuePos;
|
|
652
|
+
|
|
653
|
+
// shouldRoundHigher: face with the larger coordinate (bottom in vertical, right in horizontal)
|
|
654
|
+
const shouldRoundHigher = (layout === 'vertical' ? index === 0 : index === sortedBars.length - 1) || Boolean(a.shouldApplyGap && stackGap) || !a.shouldApplyGap && barBefore !== null && barBefore.valuePos !== a.valuePos + a.length;
|
|
655
|
+
return _extends({}, a, {
|
|
656
|
+
roundTop: Boolean(a.roundTop && (layout === 'vertical' ? shouldRoundLower : shouldRoundHigher)),
|
|
657
|
+
roundBottom: Boolean(a.roundBottom && (layout === 'vertical' ? shouldRoundHigher : shouldRoundLower))
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Threshold for treating a position as touching the baseline.
|
|
664
|
+
* Positions within this distance are considered at the baseline for rounding purposes.
|
|
665
|
+
*/
|
|
666
|
+
export const EPSILON = 1e-4;
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Computes and clamps the stack baseline position on the value axis.
|
|
670
|
+
*
|
|
671
|
+
* - If the full domain is positive, baseline is domain min.
|
|
672
|
+
* - If the full domain is negative, baseline is domain max.
|
|
673
|
+
* - If the domain crosses zero, baseline is 0.
|
|
674
|
+
*/
|
|
675
|
+
export function getStackBaseline(valueScale, stackRect, layout) {
|
|
676
|
+
const [domainMin, domainMax] = valueScale.domain();
|
|
677
|
+
const baselineValue = domainMin >= 0 ? domainMin : domainMax <= 0 ? domainMax : 0;
|
|
678
|
+
const baselinePos = valueScale(baselineValue);
|
|
679
|
+
if (layout === 'vertical') {
|
|
680
|
+
return Math.max(stackRect.y, Math.min(baselinePos != null ? baselinePos : stackRect.y + stackRect.height, stackRect.y + stackRect.height));
|
|
681
|
+
}
|
|
682
|
+
return Math.max(stackRect.x, Math.min(baselinePos != null ? baselinePos : stackRect.x, stackRect.x + stackRect.width));
|
|
683
|
+
}
|
|
684
|
+
function getStackBoundsForLayout(layout, indexPos, thickness, minValuePos, stackSize) {
|
|
685
|
+
if (layout === 'vertical') {
|
|
686
|
+
return {
|
|
687
|
+
x: indexPos,
|
|
688
|
+
y: minValuePos,
|
|
689
|
+
width: thickness,
|
|
690
|
+
height: stackSize
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
x: minValuePos,
|
|
695
|
+
y: indexPos,
|
|
696
|
+
width: stackSize,
|
|
697
|
+
height: thickness
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function getStackSizeForLayout(layout, stackRect) {
|
|
701
|
+
return layout === 'vertical' ? stackRect.height : stackRect.width;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Computes the positioned bar entries and bounding rect for a single stack at one category index.
|
|
706
|
+
*
|
|
707
|
+
* This is the pure computation extracted from `BarStack`'s `useMemo` so it can be tested
|
|
708
|
+
* independently and reused across contexts.
|
|
709
|
+
*
|
|
710
|
+
* @param params.series - Series configs for this stack
|
|
711
|
+
* @param params.seriesData - Stacked data for each series, keyed by series id
|
|
712
|
+
* @param params.categoryIndex - Index of the category being rendered
|
|
713
|
+
* @param params.indexPos - Pixel position along the categorical axis
|
|
714
|
+
* @param params.thickness - Bar thickness in pixels
|
|
715
|
+
* @param params.valueScale - Scale function for the value axis
|
|
716
|
+
* @param params.seriesGradients - Precomputed gradient configs per series (undefined entries are skipped)
|
|
717
|
+
* @param params.roundBaseline - Whether to round the face touching the baseline
|
|
718
|
+
* @param params.layout - The layout of the chart
|
|
719
|
+
* @param params.baseline - Pixel position of the zero value on the value axis
|
|
720
|
+
* @param params.stackGap - Gap between adjacent bars in pixels
|
|
721
|
+
* @param params.barMinSize - Minimum individual bar size in pixels
|
|
722
|
+
* @param params.stackMinSize - Minimum total stack size in pixels
|
|
723
|
+
* @param params.defaultFill - Fallback fill color when a series has no color or gradient
|
|
724
|
+
* @returns Positioned bar entries and the stack's bounding rect
|
|
725
|
+
*/
|
|
726
|
+
export function getBars(params) {
|
|
727
|
+
const {
|
|
728
|
+
series,
|
|
729
|
+
seriesData,
|
|
730
|
+
categoryIndex,
|
|
731
|
+
categoryValue,
|
|
732
|
+
indexPos,
|
|
733
|
+
thickness,
|
|
734
|
+
valueScale,
|
|
735
|
+
seriesGradients,
|
|
736
|
+
roundBaseline,
|
|
737
|
+
layout,
|
|
738
|
+
baseline,
|
|
739
|
+
stackGap,
|
|
740
|
+
barMinSize,
|
|
741
|
+
stackMinSize,
|
|
742
|
+
defaultFill,
|
|
743
|
+
borderRadius,
|
|
744
|
+
defaultFillOpacity,
|
|
745
|
+
defaultStroke,
|
|
746
|
+
defaultStrokeWidth,
|
|
747
|
+
defaultBarComponent
|
|
748
|
+
} = params;
|
|
749
|
+
let allBars = [];
|
|
750
|
+
series.forEach(s => {
|
|
751
|
+
var _valueScale, _valueScale2;
|
|
752
|
+
const data = seriesData[s.id];
|
|
753
|
+
if (!data) return;
|
|
754
|
+
const value = data[categoryIndex];
|
|
755
|
+
if (value === null || value === undefined) return;
|
|
756
|
+
const originalData = s.data;
|
|
757
|
+
const originalValue = originalData == null ? void 0 : originalData[categoryIndex];
|
|
758
|
+
const shouldApplyGap = !Array.isArray(originalValue);
|
|
759
|
+
const [bottom, top] = [...value].sort((a, b) => a - b);
|
|
760
|
+
const edgeBottom = (_valueScale = valueScale(bottom)) != null ? _valueScale : baseline;
|
|
761
|
+
const edgeTop = (_valueScale2 = valueScale(top)) != null ? _valueScale2 : baseline;
|
|
762
|
+
const roundTop = roundBaseline || Math.abs(edgeTop - baseline) >= EPSILON;
|
|
763
|
+
const roundBottom = roundBaseline || Math.abs(edgeBottom - baseline) >= EPSILON;
|
|
764
|
+
const length = Math.abs(edgeBottom - edgeTop);
|
|
765
|
+
const valuePos = Math.min(edgeBottom, edgeTop);
|
|
766
|
+
if (length <= 0) return;
|
|
767
|
+
let barFill = s.color || defaultFill;
|
|
768
|
+
const seriesGradientConfig = seriesGradients.find(g => (g == null ? void 0 : g.seriesId) === s.id);
|
|
769
|
+
if (seriesGradientConfig && originalValue !== null && originalValue !== undefined) {
|
|
770
|
+
var _seriesGradientConfig;
|
|
771
|
+
const axis = (_seriesGradientConfig = seriesGradientConfig.gradient.axis) != null ? _seriesGradientConfig : 'y';
|
|
772
|
+
let evalValue;
|
|
773
|
+
if (axis === 'x') {
|
|
774
|
+
evalValue = layout === 'vertical' ? categoryIndex : Array.isArray(originalValue) ? originalValue[1] : originalValue;
|
|
775
|
+
} else {
|
|
776
|
+
evalValue = layout === 'vertical' ? Array.isArray(originalValue) ? originalValue[1] : originalValue : categoryIndex;
|
|
777
|
+
}
|
|
778
|
+
const evaluatedColor = evaluateGradientAtValue(seriesGradientConfig.stops, evalValue, seriesGradientConfig.scale);
|
|
779
|
+
if (evaluatedColor) {
|
|
780
|
+
barFill = evaluatedColor;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
allBars.push({
|
|
784
|
+
seriesId: s.id,
|
|
785
|
+
valuePos,
|
|
786
|
+
length,
|
|
787
|
+
dataValue: value,
|
|
788
|
+
fill: barFill,
|
|
789
|
+
roundTop,
|
|
790
|
+
roundBottom,
|
|
791
|
+
shouldApplyGap,
|
|
792
|
+
BarComponent: s.BarComponent,
|
|
793
|
+
x: 0,
|
|
794
|
+
y: 0,
|
|
795
|
+
width: 0,
|
|
796
|
+
height: 0,
|
|
797
|
+
origin: 0
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// Apply proportional gap distribution to maintain total stack length
|
|
802
|
+
if (stackGap && allBars.length > 1) {
|
|
803
|
+
allBars = applyStackGap(allBars, stackGap, layout, baseline);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Apply barMinSize constraints
|
|
807
|
+
if (barMinSize) {
|
|
808
|
+
allBars = applyBarMinSize(allBars, barMinSize, layout, baseline);
|
|
809
|
+
}
|
|
810
|
+
allBars = applyBorderRadiusLogic(allBars, layout, stackGap);
|
|
811
|
+
|
|
812
|
+
// Apply stackMinSize constraints
|
|
813
|
+
if (stackMinSize && allBars.length > 0) {
|
|
814
|
+
const minValuePos = Math.min(...allBars.map(bar => bar.valuePos));
|
|
815
|
+
const maxValuePos = Math.max(...allBars.map(bar => bar.valuePos + bar.length));
|
|
816
|
+
const stackSize = maxValuePos - minValuePos;
|
|
817
|
+
const stackBounds = getStackBoundsForLayout(layout, indexPos, thickness, minValuePos, stackSize);
|
|
818
|
+
const result = applyStackMinSize(allBars, stackMinSize, stackSize, stackBounds, layout, indexPos, thickness, baseline);
|
|
819
|
+
allBars = result.bars;
|
|
820
|
+
|
|
821
|
+
// Reapply border radius logic only if we actually scaled
|
|
822
|
+
const newStackSize = getStackSizeForLayout(layout, result.stackBounds);
|
|
823
|
+
if (newStackSize < stackMinSize) {
|
|
824
|
+
allBars = applyBorderRadiusLogic(allBars, layout, stackGap);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const initialBarMinSizes = getInitialBarMinSizes(allBars, barMinSize, stackMinSize);
|
|
828
|
+
const barOrigins = getBarOrigins(allBars, initialBarMinSizes, stackGap != null ? stackGap : 0, baseline, layout);
|
|
829
|
+
return allBars.map((bar, i) => _extends({}, bar, {
|
|
830
|
+
x: layout === 'vertical' ? indexPos : bar.valuePos,
|
|
831
|
+
y: layout === 'vertical' ? bar.valuePos : indexPos,
|
|
832
|
+
width: layout === 'vertical' ? thickness : bar.length,
|
|
833
|
+
height: layout === 'vertical' ? bar.length : thickness,
|
|
834
|
+
dataX: layout === 'vertical' ? categoryValue : bar.dataValue,
|
|
835
|
+
dataY: layout === 'vertical' ? bar.dataValue : categoryValue,
|
|
836
|
+
origin: barOrigins[i],
|
|
837
|
+
borderRadius,
|
|
838
|
+
fillOpacity: defaultFillOpacity,
|
|
839
|
+
stroke: defaultStroke,
|
|
840
|
+
strokeWidth: defaultStrokeWidth,
|
|
841
|
+
minSize: initialBarMinSizes[i],
|
|
842
|
+
BarComponent: bar.BarComponent || defaultBarComponent
|
|
843
|
+
}));
|
|
24
844
|
}
|