Skip to content

Commit 12c3396

Browse files
authored
Merge branch 'master' into sverg/datepicker-mobile
2 parents 4b25572 + d18d603 commit 12c3396

34 files changed

+1892
-104
lines changed

packages/mobile-visualization/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ All notable changes to this project will be documented in this file.
88

99
<!-- template-start -->
1010

11+
## 3.4.0-beta.22 (3/4/2026 PST)
12+
13+
#### 🚀 Updates
14+
15+
- Improve PeriodSelector types. [[#464](https://github.com/coinbase/cds/pull/464)]
16+
- Skip null path transitions. [[#464](https://github.com/coinbase/cds/pull/464)]
17+
- Fix path transition on incompatible paths. [[#464](https://github.com/coinbase/cds/pull/464)]
18+
1119
## 3.4.0-beta.21 (3/2/2026 PST)
1220

1321
#### 🚀 Updates

packages/mobile-visualization/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coinbase/cds-mobile-visualization",
3-
"version": "3.4.0-beta.21",
3+
"version": "3.4.0-beta.22",
44
"description": "Coinbase Design System - Mobile Visualization Native",
55
"repository": {
66
"type": "git",

packages/mobile-visualization/src/chart/Path.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,7 @@ export type PathProps = PathBaseProps &
133133
};
134134

135135
const AnimatedPath = memo<
136-
Omit<
137-
PathProps,
138-
'animate' | 'clipRect' | 'clipOffset' | 'clipPath' | 'transitions' | 'transition'
139-
> & {
140-
transitions?: { enter?: Transition; update?: Transition };
141-
}
136+
Omit<PathProps, 'animate' | 'clipRect' | 'clipOffset' | 'clipPath' | 'transition'>
142137
>(
143138
({
144139
d = '',
@@ -246,18 +241,20 @@ export const Path = memo<PathProps>((props) => {
246241
[animate, transitions?.update, transition],
247242
);
248243

244+
const shouldAnimateClip = animate && enterTransition !== null;
245+
249246
// The clip offset provides extra padding to prevent path from being cut off
250247
// Area charts typically use offset=0 for exact clipping, while lines use offset=2 for breathing room
251248
const totalOffset = clipOffset * 2; // Applied on both sides
252249

253250
// Animation progress for clip path reveal
254-
const clipProgress = useSharedValue(animate ? 0 : 1);
251+
const clipProgress = useSharedValue(shouldAnimateClip ? 0 : 1);
255252

256253
useEffect(() => {
257-
if (animate && isReady) {
254+
if (shouldAnimateClip && isReady) {
258255
clipProgress.value = buildTransition(1, enterTransition);
259256
}
260-
}, [animate, isReady, clipProgress, enterTransition]);
257+
}, [shouldAnimateClip, isReady, clipProgress, enterTransition]);
261258

262259
// Create initial and target clip paths for animation
263260
const { initialClipPath, targetClipPath } = useMemo(() => {
@@ -288,7 +285,7 @@ export const Path = memo<PathProps>((props) => {
288285
const animatedClipPath = usePathInterpolation(
289286
clipProgress,
290287
[0, 1],
291-
animate && initialClipPath && targetClipPath
288+
shouldAnimateClip && initialClipPath && targetClipPath
292289
? [initialClipPath, targetClipPath]
293290
: targetClipPath
294291
? [targetClipPath, targetClipPath]
@@ -306,13 +303,13 @@ export const Path = memo<PathProps>((props) => {
306303
}
307304

308305
// If not animating or paths are null, return target clip path
309-
if (!animate || !targetClipPath) {
306+
if (!shouldAnimateClip || !targetClipPath) {
310307
return targetClipPath;
311308
}
312309

313310
// Return undefined here since we'll use animatedClipPath directly
314311
return undefined;
315-
}, [clipPathProp, animate, targetClipPath]);
312+
}, [clipPathProp, shouldAnimateClip, targetClipPath]);
316313

317314
// Convert SVG path string to SkPath for static rendering
318315
const staticPath = useDerivedValue(() => {
@@ -365,7 +362,7 @@ export const Path = memo<PathProps>((props) => {
365362

366363
// Determine which clip path to use
367364
const finalClipPath =
368-
animate && resolvedClipPath === undefined ? animatedClipPath : resolvedClipPath;
365+
shouldAnimateClip && resolvedClipPath === undefined ? animatedClipPath : resolvedClipPath;
369366

370367
// If finalClipPath is null, render without clipping
371368
if (finalClipPath === null) {

packages/mobile-visualization/src/chart/PeriodSelector.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,7 @@ const PeriodSelectorTab: TabComponent = memo(
134134
)),
135135
);
136136

137-
export type PeriodSelectorProps = Omit<SegmentedTabsProps, 'styles'> &
138-
Pick<SegmentedTabsProps, 'styles'>;
137+
export type PeriodSelectorProps = SegmentedTabsProps;
139138

140139
/**
141140
* PeriodSelector is a specialized version of SegmentedTabs optimized for chart period selection.

packages/mobile-visualization/src/chart/__stories__/ChartTransitions.stories.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ExampleScreen } from '@coinbase/cds-mobile/examples/ExampleScreen';
55
import { Box, HStack, VStack } from '@coinbase/cds-mobile/layout';
66
import { Text } from '@coinbase/cds-mobile/typography';
77

8-
import { Area } from '../area/Area';
8+
import { AreaChart } from '../area/AreaChart';
99
import type { BarProps } from '../bar/Bar';
1010
import { BarChart } from '../bar/BarChart';
1111
import { CartesianChart } from '../CartesianChart';
@@ -108,21 +108,21 @@ const TransitionAreaChart = memo<{
108108
idlePulse?: boolean;
109109
scrubberRef?: React.RefObject<ScrubberRef | null>;
110110
}>(({ data, transitions, idlePulse, scrubberRef }) => (
111-
<CartesianChart
111+
<AreaChart
112112
enableScrubbing
113+
showLines
113114
height={200}
114115
inset={{ top: 16, bottom: 16, left: 16, right: 16 }}
115116
series={[{ id: 'values', data }]}
117+
transitions={transitions}
116118
>
117-
<Area seriesId="values" transitions={transitions} />
118-
<Line seriesId="values" transitions={transitions} />
119119
<Scrubber
120120
ref={scrubberRef as React.RefObject<ScrubberRef>}
121121
hideOverlay
122122
idlePulse={idlePulse}
123123
transitions={transitions}
124124
/>
125-
</CartesianChart>
125+
</AreaChart>
126126
));
127127

128128
const MultiLineChart = memo<{

packages/mobile-visualization/src/chart/area/AreaChart.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,22 @@ export type AreaSeries = Series &
2222
Partial<
2323
Pick<
2424
AreaProps,
25-
'AreaComponent' | 'curve' | 'fillOpacity' | 'type' | 'fill' | 'connectNulls' | 'transition'
25+
| 'AreaComponent'
26+
| 'curve'
27+
| 'fillOpacity'
28+
| 'type'
29+
| 'fill'
30+
| 'connectNulls'
31+
| 'transition'
32+
| 'transitions'
2633
>
2734
> &
28-
Partial<Pick<LineProps, 'LineComponent' | 'strokeWidth' | 'stroke' | 'opacity'>> & {
35+
Partial<
36+
Pick<
37+
LineProps,
38+
'LineComponent' | 'strokeWidth' | 'stroke' | 'opacity' | 'transition' | 'transitions'
39+
>
40+
> & {
2941
/**
3042
* The type of line to render for this series.
3143
* Overrides the chart-level lineType if provided.
@@ -37,7 +49,13 @@ export type AreaSeries = Series &
3749
export type AreaChartBaseProps = Omit<CartesianChartBaseProps, 'xAxis' | 'yAxis' | 'series'> &
3850
Pick<
3951
AreaProps,
40-
'AreaComponent' | 'curve' | 'fillOpacity' | 'type' | 'connectNulls' | 'transition'
52+
| 'AreaComponent'
53+
| 'curve'
54+
| 'fillOpacity'
55+
| 'type'
56+
| 'connectNulls'
57+
| 'transition'
58+
| 'transitions'
4159
> &
4260
Pick<LineProps, 'LineComponent' | 'strokeWidth'> & {
4361
/**
@@ -101,6 +119,7 @@ export const AreaChart = memo(
101119
type,
102120
connectNulls,
103121
transition,
122+
transitions,
104123
LineComponent,
105124
strokeWidth,
106125
showXAxis,
@@ -221,6 +240,7 @@ export const AreaChart = memo(
221240
fillOpacity={fillOpacity}
222241
seriesId={id}
223242
transition={transition}
243+
transitions={transitions}
224244
type={type}
225245
{...areaPropsFromSeries}
226246
/>
@@ -238,7 +258,6 @@ export const AreaChart = memo(
238258
fillOpacity,
239259
stackId,
240260
type, // Area type (don't pass to Line)
241-
lineType: seriesLineType,
242261
...otherPropsFromSeries
243262
}) => {
244263
return (
@@ -250,7 +269,8 @@ export const AreaChart = memo(
250269
seriesId={id}
251270
strokeWidth={strokeWidth}
252271
transition={transition}
253-
type={seriesLineType ?? lineType}
272+
transitions={transitions}
273+
type={lineType}
254274
{...otherPropsFromSeries}
255275
/>
256276
);

packages/mobile-visualization/src/chart/area/DottedArea.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Gradient } from '../gradient';
77
import { Path, type PathProps } from '../Path';
88
import { createGradient, getBaseline } from '../utils';
99
import { getDottedAreaPath } from '../utils/path';
10-
import { usePathTransition } from '../utils/transition';
10+
import { defaultTransition, usePathTransition } from '../utils/transition';
1111

1212
import type { AreaComponentProps } from './Area';
1313

@@ -73,13 +73,23 @@ export const DottedArea = memo<DottedAreaProps>(
7373
const theme = useTheme();
7474
const { drawingArea, animate, getYAxis } = useCartesianChartContext();
7575

76+
const shouldAnimate = animateProp ?? animate;
77+
7678
const yAxisConfig = getYAxis(yAxisId);
7779

7880
const fill = useMemo(
7981
() => fillProp ?? theme.color.fgPrimary,
8082
[fillProp, theme.color.fgPrimary],
8183
);
8284

85+
const updateTransition = useMemo(() => {
86+
return transitions?.update !== undefined
87+
? transitions.update
88+
: transition !== undefined
89+
? transition
90+
: defaultTransition;
91+
}, [transitions?.update, transition]);
92+
8393
const dottedPath = useMemo(() => {
8494
if (!drawingArea) return '';
8595

@@ -95,16 +105,11 @@ export const DottedArea = memo<DottedAreaProps>(
95105
);
96106
}, [drawingArea, patternSize, dotSize]);
97107

98-
const animatedClipPath = usePathTransition({
108+
const clipPath = usePathTransition({
99109
currentPath: d,
100-
transitions: { update: transition },
110+
transitions: { update: shouldAnimate ? updateTransition : null },
101111
});
102112

103-
const staticClipPath = useMemo(() => {
104-
if (!d) return;
105-
return Skia.Path.MakeFromSVGString(d) ?? undefined;
106-
}, [d]);
107-
108113
const gradient = useMemo(() => {
109114
if (gradientProp) return gradientProp;
110115
if (!yAxisConfig) return;
@@ -113,10 +118,11 @@ export const DottedArea = memo<DottedAreaProps>(
113118
return createGradient(yAxisConfig.domain, baselineValue, fill, peakOpacity, baselineOpacity);
114119
}, [gradientProp, yAxisConfig, fill, baseline, peakOpacity, baselineOpacity]);
115120

121+
// Update transition is used for clip path, we skip update animation on Path itself
116122
return (
117-
<Group clip={animate ? animatedClipPath : staticClipPath}>
123+
<Group clip={clipPath}>
118124
<Path
119-
animate={animateProp ?? animate}
125+
animate={shouldAnimate}
120126
d={dottedPath}
121127
fill={fill}
122128
transition={transition}

packages/mobile-visualization/src/chart/line/Line.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,12 @@ export const Line = memo<LineProps>(
292292
// If points is true, render with defaults
293293
if (points === true) {
294294
return (
295-
<Point key={`${seriesId}-${xValue}`} transition={transition} {...defaults} />
295+
<Point
296+
key={`${seriesId}-${xValue}`}
297+
transition={transition}
298+
transitions={transitions}
299+
{...defaults}
300+
/>
296301
);
297302
}
298303

@@ -307,6 +312,7 @@ export const Line = memo<LineProps>(
307312
<Point
308313
key={`${seriesId}-${xValue}`}
309314
transition={transition}
315+
transitions={transitions}
310316
{...defaults}
311317
{...pointConfig}
312318
/>

packages/mobile-visualization/src/chart/line/LineChart.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export type LineSeries = Series &
3030
| 'points'
3131
| 'connectNulls'
3232
| 'transition'
33+
| 'transitions'
3334
>
3435
>;
3536

@@ -47,6 +48,7 @@ export type LineChartBaseProps = Omit<CartesianChartBaseProps, 'xAxis' | 'yAxis'
4748
| 'strokeOpacity'
4849
| 'connectNulls'
4950
| 'transition'
51+
| 'transitions'
5052
| 'opacity'
5153
> & {
5254
/**
@@ -95,6 +97,7 @@ export const LineChart = memo(
9597
strokeOpacity,
9698
connectNulls,
9799
transition,
100+
transitions,
98101
opacity,
99102
showXAxis,
100103
showYAxis,
@@ -189,7 +192,8 @@ export const LineChart = memo(
189192
showArea={showArea}
190193
strokeOpacity={strokeOpacity}
191194
strokeWidth={strokeWidth}
192-
transition={linePropsFromSeries.transition ?? transition}
195+
transition={transition}
196+
transitions={transitions}
193197
type={type}
194198
{...linePropsFromSeries}
195199
/>

packages/mobile-visualization/src/chart/scrubber/DefaultScrubberBeacon.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
buildTransition,
2121
defaultTransition,
2222
getTransition,
23-
instantTransition,
2423
type Transition,
2524
} from '../utils/transition';
2625

0 commit comments

Comments
 (0)