rn-native-ios-charts 0.2.2 → 1.0.0-alpha.3
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 +212 -2
- package/README.md +405 -6
- package/docs/demo-v1.gif +0 -0
- package/docs/demo-v1.mp4 +0 -0
- package/examples/DemoScreen.tsx +1048 -0
- package/ios/ChartConfig.swift +68 -0
- package/ios/ChartDataPoint.swift +14 -0
- package/ios/ChartView.swift +1109 -166
- package/ios/NativeIosChartsModule.swift +14 -0
- package/package.json +9 -2
- package/src/AreaChart.tsx +65 -40
- package/src/BarChart.tsx +74 -47
- package/src/Chart.tsx +89 -3
- package/src/LineChart.tsx +71 -43
- package/src/PieChart.tsx +95 -41
- package/src/RangeBarChart.tsx +59 -40
- package/src/ScatterChart.tsx +64 -41
- package/src/ScrollAwareChart.tsx +101 -0
- package/src/index.ts +25 -1
- package/src/types.ts +187 -5
- package/src/useChartHandle.ts +38 -0
- package/src/useChartScrollScale.ts +156 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,217 @@ All notable changes to **rn-native-ios-charts** are documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [Unreleased]
|
|
7
|
+
## [Unreleased] — 1.0.0 (in progress)
|
|
8
|
+
|
|
9
|
+
The 1.0.0 release lands the production features the dashboard
|
|
10
|
+
charts have been waiting for: date axes (breaking — `x` becomes
|
|
11
|
+
`string | Date`), log scales, mark annotations + range bands,
|
|
12
|
+
chart-level animation modes, a JS-side scroll-driven scale wrapper
|
|
13
|
+
(uses `react-native-reanimated`), and the pie tooltip / highlight /
|
|
14
|
+
dismiss suite. Shipping incrementally on `master`; track this
|
|
15
|
+
section for what's already merged.
|
|
16
|
+
|
|
17
|
+
### Added (merged)
|
|
18
|
+
|
|
19
|
+
- **Pie tooltip with leader line + callout.** Enabling `tooltip`
|
|
20
|
+
on `<PieChart>` now draws a short radial leader line from the
|
|
21
|
+
selected slice's outer edge to a callout, anchored at the slice's
|
|
22
|
+
midpoint angle just outside the chart's outer radius. The callout
|
|
23
|
+
is clamped to the plot frame so it never overflows the chart's
|
|
24
|
+
bounds, even at the topmost / bottommost slice angles. Mirrors
|
|
25
|
+
the existing cartesian tooltip API — same `backgroundColor`,
|
|
26
|
+
`textColor`, `borderColor`, `valuePrefix`, `valueSuffix`,
|
|
27
|
+
`valueDecimals` fields.
|
|
28
|
+
|
|
29
|
+
- **Selected-slice highlight (animated).** When a slice is tapped
|
|
30
|
+
and `tooltip.enabled` is true, the selected slice bumps outward
|
|
31
|
+
slightly while unselected slices fade to `tooltip.dimOpacity`
|
|
32
|
+
(default `0.3`). The scale-up is implemented by shrinking the
|
|
33
|
+
unselected slices in tandem (configurable via
|
|
34
|
+
`tooltip.sliceScale`, default `1.05`), so the effect can't
|
|
35
|
+
overflow the chart frame. Animated with a spring (`response:
|
|
36
|
+
0.32, dampingFraction: 0.72`) — independent of the slower data-
|
|
37
|
+
change ease so taps feel snappier than redraws.
|
|
38
|
+
|
|
39
|
+
- **Tap-same-slice-to-toggle.** Tapping the currently-selected
|
|
40
|
+
slice clears the selection instead of re-selecting it. Natural
|
|
41
|
+
iOS feel — same as deselecting a row in a list.
|
|
42
|
+
|
|
43
|
+
- **In-chart miss dismisses selection.** A transparent backdrop
|
|
44
|
+
layered behind the chart catches taps that miss every slice (the
|
|
45
|
+
donut hole, corners of the bounding rect, gaps between bars),
|
|
46
|
+
clearing both `selectedAngleY` (pie) and `selectedX` (cartesian).
|
|
47
|
+
Slice / datum hits still go to `chartAngleSelection` /
|
|
48
|
+
`chartXSelection` — only "misses" fall through.
|
|
49
|
+
|
|
50
|
+
- **`clearSelection()` imperative ref on `<PieChart>`.** New
|
|
51
|
+
`PieChartHandle` type exposed via `React.forwardRef`. Call
|
|
52
|
+
`chartRef.current?.clearSelection()` from a parent gesture
|
|
53
|
+
handler (e.g. a `<Pressable>` covering the screen) to dismiss a
|
|
54
|
+
sticky slice selection when the user taps outside the chart's
|
|
55
|
+
host view — the case the in-chart backdrop can't reach.
|
|
56
|
+
|
|
57
|
+
- **`tooltip` prop on `<PieChart>`.** Previously absent; pies were
|
|
58
|
+
selection-only with `onSelect` driving `centerLabel`. The
|
|
59
|
+
centerLabel pattern still works; the new visual tooltip is opt-in
|
|
60
|
+
via `tooltip.enabled`.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- **Pie chart didn't redraw / re-animate consistently on data
|
|
65
|
+
prop changes** — the v0.x→1.0-alpha behavior had the chart
|
|
66
|
+
animating new/exiting slices but skipping value updates on
|
|
67
|
+
existing slices, and not animating color changes at all. Root
|
|
68
|
+
cause: SwiftUI Charts doesn't reliably interpolate `SectorMark`
|
|
69
|
+
angle and fill changes when the data binding lives directly on
|
|
70
|
+
an `@ObservedObject` (`@Published`-driven updates bypass the
|
|
71
|
+
framework's animation interpolator, and `AnyShapeStyle` fills
|
|
72
|
+
don't always animate either). Fix: mirror `props.marks` into a
|
|
73
|
+
local `@State` (`renderedMarks`) and drive every data change
|
|
74
|
+
through an explicit `withAnimation { renderedMarks = props.marks }`
|
|
75
|
+
inside `.onChange(of: marksFingerprint)`. SwiftUI Charts treats
|
|
76
|
+
this as a single animated transaction and interpolates angle,
|
|
77
|
+
position, AND fill consistently regardless of how small the
|
|
78
|
+
value delta is. The fingerprint also now includes `color` so
|
|
79
|
+
per-slice palette swaps trigger the animation. State-lookup
|
|
80
|
+
helpers (`findActivePoint`, `selectedSliceData`, `emitSelect`)
|
|
81
|
+
continue to read `props.marks` directly so taps reflect the
|
|
82
|
+
latest data even mid-animation.
|
|
83
|
+
|
|
84
|
+
- **Pie chart redraw on data prop changes** (1.0.0-alpha.0 fix,
|
|
85
|
+
re-stated). `ForEach(..., id: \.offset)` is now
|
|
86
|
+
`\.element.identityKey` (`x|category`), stable across data
|
|
87
|
+
swaps; slices get proper enter/exit transitions when label
|
|
88
|
+
sets change.
|
|
89
|
+
|
|
90
|
+
### Added (merged in this push)
|
|
91
|
+
|
|
92
|
+
- **`animation` chart-level config.** New `AnimationConfig` type
|
|
93
|
+
on every wrapper (and `<Chart>`) supersedes the boolean
|
|
94
|
+
`animate` shorthand. Fields:
|
|
95
|
+
- `enabled` (default true) — master toggle
|
|
96
|
+
- `duration` (default 400ms) — data-change duration
|
|
97
|
+
- `curve` — `"easeInOut" | "easeIn" | "easeOut" | "linear" |
|
|
98
|
+
"spring"` (default `"easeInOut"`; spring tuning is fixed
|
|
99
|
+
to a tap-feedback-friendly preset)
|
|
100
|
+
- `entrance` (default false) — fade + scale 0.96→1.0 on first
|
|
101
|
+
mount, capped at 600ms
|
|
102
|
+
- `cartesianDimOnSelect` (default false) — dim non-active
|
|
103
|
+
cartesian marks to `tooltip.dimOpacity` when the scrubber
|
|
104
|
+
is engaged. Mirrors the pie's slice-dim for line / area /
|
|
105
|
+
bar / point charts. Pie always dims when `tooltip.enabled`,
|
|
106
|
+
regardless of this flag.
|
|
107
|
+
|
|
108
|
+
The legacy `animate?: boolean` prop is still honored as a
|
|
109
|
+
shorthand for `{ enabled: true }`; when both are passed,
|
|
110
|
+
`animation` wins.
|
|
111
|
+
|
|
112
|
+
- **Date axis (breaking).** `DataPoint.x` is now `string | Date`,
|
|
113
|
+
same for the per-wrapper `LinePoint`/`BarDatum`/`AreaDatum`/
|
|
114
|
+
`ScatterDatum`/`RangeDatum`/`LineSeries.data` types and
|
|
115
|
+
`Annotation.x` / `Annotation.xRange`. `Chart.tsx` normalizes
|
|
116
|
+
`Date` instances to ISO-8601 strings before the Expo bridge;
|
|
117
|
+
the SwiftUI side keeps a categorical X scale and reformats
|
|
118
|
+
tick labels via `xAxis.valueFormat: "date"` + `xAxis.dateFormat`
|
|
119
|
+
(default `"MMM yy"`, e.g. "Jan 26"). Pair with the date
|
|
120
|
+
formatter for time-series:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
<LineChart
|
|
124
|
+
data={dailyPrices} // [{ x: new Date(...), y: ... }, ...]
|
|
125
|
+
xAxis={{ valueFormat: "date", dateFormat: "MMM yy" }}
|
|
126
|
+
tooltip={{ enabled: true, valuePrefix: "$" }}
|
|
127
|
+
/>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Tooltip X labels also honor `xAxis.dateFormat` automatically.
|
|
131
|
+
|
|
132
|
+
**Note on scope.** This is the pragmatic version of date
|
|
133
|
+
support — the chart's internal scale stays categorical, so
|
|
134
|
+
you don't get SwiftUI's auto month/year tick aggregation for
|
|
135
|
+
very long ranges. A true Date-domain `chartXScale` is on the
|
|
136
|
+
roadmap for a follow-up. For typical 1–5y dashboards the
|
|
137
|
+
visual output is identical.
|
|
138
|
+
|
|
139
|
+
- **Log scale (Y).** `AxisConfig.scaleType: "linear" | "log"`.
|
|
140
|
+
Y-only — X stays categorical/date in this release.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
<LineChart
|
|
144
|
+
data={networthOverDecades}
|
|
145
|
+
yAxis={{ scaleType: "log", domainMin: 1000, valueFormat: "abbreviated" }}
|
|
146
|
+
/>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Log scales require all values strictly > 0. Set `domainMin` to
|
|
150
|
+
clamp out zeros / negatives.
|
|
151
|
+
|
|
152
|
+
- **Annotations + range bands.** New `Annotation` type and
|
|
153
|
+
`<Chart annotations={[]}>` prop. Two flavors:
|
|
154
|
+
|
|
155
|
+
- **Datum-anchored** (set `x`) — floating label at that X.
|
|
156
|
+
- **Range band** (set `xRange: [start, end]`) — shaded
|
|
157
|
+
vertical band, optional centered/top/bottom label.
|
|
158
|
+
|
|
159
|
+
`yRange?: [number, number]` constrains the vertical extent for
|
|
160
|
+
bands (defaults to full plot height). Both styles accept
|
|
161
|
+
`Date` for `x` / `xRange`, normalized identically to data.
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
<LineChart
|
|
165
|
+
data={pricesByDate}
|
|
166
|
+
xAxis={{ valueFormat: "date" }}
|
|
167
|
+
annotations={[
|
|
168
|
+
{ x: new Date("2025-03-15"), text: "Earnings", position: "top" },
|
|
169
|
+
{
|
|
170
|
+
xRange: [new Date("2025-10-01"), new Date("2025-12-31")],
|
|
171
|
+
text: "Q4",
|
|
172
|
+
color: "#3B82F6",
|
|
173
|
+
position: "inside",
|
|
174
|
+
},
|
|
175
|
+
]}
|
|
176
|
+
/>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Drawn in `chartOverlay` under the tooltip, so the active
|
|
180
|
+
callout always paints on top.
|
|
181
|
+
|
|
182
|
+
- **`clearSelection()` ref on every wrapper.** `<LineChart>`,
|
|
183
|
+
`<BarChart>`, `<AreaChart>`, `<ScatterChart>`, `<RangeBarChart>`
|
|
184
|
+
now `forwardRef` the same `ChartHandle` interface as
|
|
185
|
+
`<PieChart>`. The hook is also exported (`useChartHandle`)
|
|
186
|
+
for consumers building their own wrappers. `PieChartHandle`
|
|
187
|
+
remains as a type alias for `ChartHandle`.
|
|
188
|
+
|
|
189
|
+
### Added (scroll-driven animation)
|
|
190
|
+
|
|
191
|
+
- **`<ScrollAwareChart>` + `useChartScrollScale` hook.** JS-side
|
|
192
|
+
scale + fade interpolation driven by a parent
|
|
193
|
+
`Animated.ScrollView`'s scroll position. Runs entirely on the
|
|
194
|
+
UI thread via Reanimated worklets — no bridge crossings. Adds
|
|
195
|
+
`react-native-reanimated >= 3.0.0` as an **optional** peer
|
|
196
|
+
dependency (only required if you import the scroll wrapper).
|
|
197
|
+
Documented setup includes the `CADisableMinimumFrameDurationOnPhone`
|
|
198
|
+
Info.plist flag needed for 120Hz on ProMotion. Compatible with
|
|
199
|
+
Reanimated 4's `useScrollOffset` as an alternative scroll
|
|
200
|
+
source. Hook + component are exported separately so consumers
|
|
201
|
+
can compose the scroll-scale style with their own animated
|
|
202
|
+
transforms.
|
|
203
|
+
|
|
204
|
+
### Added (docs & demo)
|
|
205
|
+
|
|
206
|
+
- **`examples/DemoScreen.tsx`** — comprehensive demo of every
|
|
207
|
+
chart and every feature in one scrollable screen. Self-
|
|
208
|
+
contained (no theme/parent deps beyond `react`, `react-native`,
|
|
209
|
+
`react-native-reanimated`). Doubles as the visual regression
|
|
210
|
+
sweep and the README's screenshot source. Now shipped inside
|
|
211
|
+
the npm package via the `examples` files entry.
|
|
212
|
+
|
|
213
|
+
### Pending (still on the roadmap)
|
|
214
|
+
|
|
215
|
+
- **True Date-domain `chartXScale`** — auto month/year tick
|
|
216
|
+
aggregation for multi-year time-series. The current date-axis
|
|
217
|
+
feature gives nicely formatted ticks but stays categorical.
|
|
8
218
|
|
|
9
219
|
## [0.2.0] — 2026-05-11
|
|
10
220
|
|
|
@@ -132,6 +342,6 @@ primitive plus convenience wrappers.
|
|
|
132
342
|
`View` so consuming code doesn't need to feature-detect. Pair with
|
|
133
343
|
`isChartSupported()` to swap in an alternative renderer.
|
|
134
344
|
|
|
135
|
-
[Unreleased]: https://github.com/abdallaemadeldin/rn-native-ios-charts/compare/v0.2.
|
|
345
|
+
[Unreleased]: https://github.com/abdallaemadeldin/rn-native-ios-charts/compare/v0.2.3...HEAD
|
|
136
346
|
[0.2.0]: https://github.com/abdallaemadeldin/rn-native-ios-charts/compare/v0.1.0...v0.2.0
|
|
137
347
|
[0.1.0]: https://github.com/abdallaemadeldin/rn-native-ios-charts/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
> own `Charts` framework.
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<img src="https://raw.githubusercontent.com/abdallaemadeldin/rn-native-ios-charts/HEAD/docs/demo.gif" alt="rn-native-ios-charts demo" width="360" />
|
|
8
|
+
<img src="https://raw.githubusercontent.com/abdallaemadeldin/rn-native-ios-charts/HEAD/docs/demo-v1.gif" alt="rn-native-ios-charts 1.0 demo" width="360" />
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
> [▶ Watch HD version](https://github.com/abdallaemadeldin/rn-native-ios-charts/raw/HEAD/docs/demo.mp4) —
|
|
11
|
+
> [▶ Watch HD version](https://github.com/abdallaemadeldin/rn-native-ios-charts/raw/HEAD/docs/demo-v1.mp4) — 1.0 walkthrough: pie tooltip + slice highlight + dismiss, date axis with annotations, log scale, multi-series stacked tooltip, scroll-aware scale, and every other chart type. ([0.x demo](https://github.com/abdallaemadeldin/rn-native-ios-charts/raw/HEAD/docs/demo.mp4) is still archived for reference.)
|
|
12
12
|
|
|
13
13
|
Cross-platform RN chart libraries (Victory, Skia, gifted-charts, etc.) all
|
|
14
14
|
hit the same iOS ceilings:
|
|
@@ -29,6 +29,43 @@ every mark type and every modifier we've needed in production. iOS-only
|
|
|
29
29
|
by design — Android / web mount a no-op `<View />` so consuming code
|
|
30
30
|
doesn't need to feature-detect.
|
|
31
31
|
|
|
32
|
+
## See it all in one place — `examples/DemoScreen.tsx`
|
|
33
|
+
|
|
34
|
+
The package ships a comprehensive demo screen at
|
|
35
|
+
[`examples/DemoScreen.tsx`](./examples/DemoScreen.tsx) that
|
|
36
|
+
exercises every chart type and every feature in this README:
|
|
37
|
+
|
|
38
|
+
- Pie with tooltip + slice highlight + tap-outside dismiss, plus
|
|
39
|
+
tab-switching across three data shapes (same labels, different
|
|
40
|
+
labels, different counts) so you can verify the redraw fix.
|
|
41
|
+
- Line — single series with area, multi-series with stacked
|
|
42
|
+
tooltip + cartesian dim-on-select, `tightX` trading-chart preset.
|
|
43
|
+
- Date axis with annotations + range bands.
|
|
44
|
+
- Log Y-scale for long-horizon growth.
|
|
45
|
+
- Area with native gradient fill.
|
|
46
|
+
- Bar — grouped, stacked, horizontal Top-N.
|
|
47
|
+
- Scatter with per-category palette.
|
|
48
|
+
- Range bar (OHLC-style).
|
|
49
|
+
- Generic `<Chart>` with mixed marks (area + line + reference rule
|
|
50
|
+
+ annotation).
|
|
51
|
+
- `<ScrollAwareChart>` wrapping a chart at the bottom — scroll the
|
|
52
|
+
page to feel the scale + fade interpolation.
|
|
53
|
+
|
|
54
|
+
Drop it into any Expo route to use as a regression sweep or as a
|
|
55
|
+
copy-paste-friendly starting point:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// app/charts-demo.tsx
|
|
59
|
+
import { DemoScreen } from "rn-native-ios-charts/examples/DemoScreen";
|
|
60
|
+
export default function ChartsDemoRoute() {
|
|
61
|
+
return <DemoScreen />;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Self-contained — no theme system, no parent-project deps beyond
|
|
66
|
+
`react`, `react-native`, `react-native-reanimated`, and this
|
|
67
|
+
package.
|
|
68
|
+
|
|
32
69
|
## Components
|
|
33
70
|
|
|
34
71
|
### `<Chart />` — the generic, composable view
|
|
@@ -234,6 +271,242 @@ series' color dot, name, and formatted value.
|
|
|
234
271
|
When the chart has only one cartesian mark, `multiSeries` silently
|
|
235
272
|
falls back to the regular single-row tooltip — safe to leave on.
|
|
236
273
|
|
|
274
|
+
## Scroll-aware scale — `<ScrollAwareChart>`
|
|
275
|
+
|
|
276
|
+
Native-iOS feel for "card scales up when centered in the viewport"
|
|
277
|
+
dashboards. The scale (and optional fade) interpolates against
|
|
278
|
+
the chart's distance from viewport center, driven by your
|
|
279
|
+
`Animated.ScrollView`'s scroll position — all frame computation
|
|
280
|
+
stays on the UI thread via Reanimated worklets, so no JS bridge
|
|
281
|
+
crossings and no jank.
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import Animated, {
|
|
285
|
+
useAnimatedScrollHandler,
|
|
286
|
+
useSharedValue,
|
|
287
|
+
} from "react-native-reanimated";
|
|
288
|
+
import { ScrollAwareChart, LineChart } from "rn-native-ios-charts";
|
|
289
|
+
|
|
290
|
+
const scrollY = useSharedValue(0);
|
|
291
|
+
const onScroll = useAnimatedScrollHandler({
|
|
292
|
+
onScroll: (e) => { scrollY.value = e.contentOffset.y; },
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
<Animated.ScrollView onScroll={onScroll} scrollEventThrottle={16}>
|
|
296
|
+
<ScrollAwareChart scrollY={scrollY} fadeOut>
|
|
297
|
+
<LineChart {...} tooltip={{ enabled: true }} />
|
|
298
|
+
</ScrollAwareChart>
|
|
299
|
+
{/* …other cards… */}
|
|
300
|
+
</Animated.ScrollView>
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Options
|
|
304
|
+
|
|
305
|
+
| Field | Default | Effect |
|
|
306
|
+
| --- | --- | --- |
|
|
307
|
+
| `scrollY` | required | `SharedValue<number>` driven by the parent scroll handler. |
|
|
308
|
+
| `minScale` | `0.92` | Scale when the chart sits at the edges of `range`. |
|
|
309
|
+
| `maxScale` | `1.0` | Scale when centered in the viewport. |
|
|
310
|
+
| `fadeOut` | `false` | Also interpolate opacity. |
|
|
311
|
+
| `minOpacity` | `0.5` | Opacity at the edges of `range` when `fadeOut: true`. |
|
|
312
|
+
| `range` | `320` | Distance from viewport center (px) at which scale reaches `minScale`. Larger = gentler ramp. |
|
|
313
|
+
| `viewportHeight` | window height | Override if your ScrollView is inset (modal sheet, behind a tab bar). |
|
|
314
|
+
|
|
315
|
+
### Just the hook
|
|
316
|
+
|
|
317
|
+
If you want to compose the scroll-scale style with your own
|
|
318
|
+
animated transforms (shadows, tilt, parallax), use the hook
|
|
319
|
+
directly:
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
import { useChartScrollScale } from "rn-native-ios-charts";
|
|
323
|
+
|
|
324
|
+
const { onLayout, style } = useChartScrollScale(scrollY, { fadeOut: true });
|
|
325
|
+
|
|
326
|
+
<Animated.View onLayout={onLayout} style={[style, myCardShadow]}>
|
|
327
|
+
<LineChart {...} />
|
|
328
|
+
</Animated.View>
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Requirements
|
|
332
|
+
|
|
333
|
+
- **`react-native-reanimated >= 3.0.0`** as a peer dependency.
|
|
334
|
+
Declared optional, but importing `<ScrollAwareChart>` without it
|
|
335
|
+
installed will throw at module load — install it.
|
|
336
|
+
- **For 120Hz on ProMotion devices**, add to your app's `Info.plist`:
|
|
337
|
+
```xml
|
|
338
|
+
<key>CADisableMinimumFrameDurationOnPhone</key>
|
|
339
|
+
<true/>
|
|
340
|
+
```
|
|
341
|
+
iOS caps third-party apps at 60Hz on ProMotion without this
|
|
342
|
+
flag, regardless of what Reanimated does. With it set and a UI-
|
|
343
|
+
thread-only worklet (the default for `useAnimatedStyle`), you
|
|
344
|
+
get 120Hz "for free."
|
|
345
|
+
- In Reanimated 4+, `useScrollOffset(scrollRef)` is a cleaner
|
|
346
|
+
one-liner alternative to `useAnimatedScrollHandler` when you
|
|
347
|
+
don't need momentum/drag callbacks — feel free to use it as
|
|
348
|
+
the `scrollY` source instead.
|
|
349
|
+
|
|
350
|
+
### Don't use inside recycled list cells
|
|
351
|
+
|
|
352
|
+
`FlatList`/`FlashList` reuse cell instances, which keeps the
|
|
353
|
+
shared values bound to the old row's layout. The result is
|
|
354
|
+
stale scale values on the new row. Either:
|
|
355
|
+
|
|
356
|
+
1. Wrap each chart at the screen level (outside the list), or
|
|
357
|
+
2. Key your row component on the item id to force a fresh mount.
|
|
358
|
+
|
|
359
|
+
For per-row scroll animation inside a recycled list, prefer
|
|
360
|
+
Reanimated's `useAnimatedRef` + `measure()` worklet pattern with
|
|
361
|
+
per-row shared values.
|
|
362
|
+
|
|
363
|
+
## Animation config — `animation`
|
|
364
|
+
|
|
365
|
+
Every wrapper (and `<Chart>`) accepts an `animation` prop that
|
|
366
|
+
controls both data-change transitions and an optional entrance
|
|
367
|
+
animation:
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
<LineChart
|
|
371
|
+
data={monthlyRevenue}
|
|
372
|
+
animation={{
|
|
373
|
+
enabled: true,
|
|
374
|
+
duration: 400, // ms
|
|
375
|
+
curve: "easeInOut", // or "easeIn" | "easeOut" | "linear" | "spring"
|
|
376
|
+
entrance: true, // fade + scale 0.96→1 on first mount
|
|
377
|
+
cartesianDimOnSelect: true, // dim non-active marks when scrubber engages
|
|
378
|
+
}}
|
|
379
|
+
tooltip={{ enabled: true }}
|
|
380
|
+
/>
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
| Field | Default | Effect |
|
|
384
|
+
| --- | --- | --- |
|
|
385
|
+
| `enabled` | `true` | Master toggle. `false` kills every animation including entrance and selection feedback. |
|
|
386
|
+
| `duration` | `400` | Milliseconds for data-change transitions. Ignored when `curve` is `"spring"`. |
|
|
387
|
+
| `curve` | `"easeInOut"` | One of `"easeInOut" \| "easeIn" \| "easeOut" \| "linear" \| "spring"`. |
|
|
388
|
+
| `entrance` | `false` | Scale-from-0.96 + fade-in on first mount. Capped at 600ms regardless of `duration`. |
|
|
389
|
+
| `cartesianDimOnSelect` | `false` | When the scrubber tooltip is active, fade non-active cartesian marks to `tooltip.dimOpacity`. Pie always dims when `tooltip.enabled` — this only affects line/area/bar/point. |
|
|
390
|
+
|
|
391
|
+
The legacy `animate?: boolean` shorthand still works
|
|
392
|
+
(`animate: false` disables everything, same as `animation: { enabled: false }`).
|
|
393
|
+
When both `animate` and `animation` are passed, `animation` wins.
|
|
394
|
+
|
|
395
|
+
Selection animations (pie slice scale + dim, cartesian dim-on-select)
|
|
396
|
+
use a fixed spring tuned for tap feedback rather than the
|
|
397
|
+
data-change curve — taps shouldn't feel as slow as redraws.
|
|
398
|
+
|
|
399
|
+
## Date axis — pass `Date` objects for `x`
|
|
400
|
+
|
|
401
|
+
Time-series charts can pass `Date` objects directly as the `x`
|
|
402
|
+
value. The chart serializes them to ISO-8601 on the bridge and
|
|
403
|
+
formats tick labels via `xAxis.valueFormat: "date"`:
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
<LineChart
|
|
407
|
+
data={[
|
|
408
|
+
{ x: new Date("2025-01-01"), y: 12000 },
|
|
409
|
+
{ x: new Date("2025-06-01"), y: 38000 },
|
|
410
|
+
{ x: new Date("2026-01-01"), y: 86000 },
|
|
411
|
+
]}
|
|
412
|
+
xAxis={{
|
|
413
|
+
valueFormat: "date",
|
|
414
|
+
dateFormat: "MMM yy", // → "Jan 25", "Jun 25", "Jan 26"
|
|
415
|
+
}}
|
|
416
|
+
tooltip={{ enabled: true, valuePrefix: "$" }}
|
|
417
|
+
/>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
| `dateFormat` | Output |
|
|
421
|
+
| --- | --- |
|
|
422
|
+
| `"MMM yy"` (default) | `Jan 26` |
|
|
423
|
+
| `"MMM d"` | `Jan 15` |
|
|
424
|
+
| `"yyyy"` | `2026` |
|
|
425
|
+
| `"MMM d, yyyy"` | `Jan 15, 2026` |
|
|
426
|
+
| `"HH:mm"` | `14:30` |
|
|
427
|
+
|
|
428
|
+
Apple `DateFormatter` syntax (UTS #35) — see
|
|
429
|
+
[nsdateformatter.com](https://nsdateformatter.com) for a live
|
|
430
|
+
preview. Tooltip X labels honor the same format automatically.
|
|
431
|
+
|
|
432
|
+
**Scope note.** The chart's internal scale stays categorical —
|
|
433
|
+
each date you pass becomes one tick. For multi-year ranges with
|
|
434
|
+
daily data you'll want to thin the input array yourself (e.g.
|
|
435
|
+
"first business day of each month") rather than relying on
|
|
436
|
+
auto-aggregation. A true `Date`-domain `chartXScale` with
|
|
437
|
+
auto-tick aggregation is on the roadmap.
|
|
438
|
+
|
|
439
|
+
## Log scale — `yAxis.scaleType`
|
|
440
|
+
|
|
441
|
+
For long-horizon growth charts (where linear flattens the early
|
|
442
|
+
years into nothing), set `yAxis.scaleType: "log"`:
|
|
443
|
+
|
|
444
|
+
```tsx
|
|
445
|
+
<LineChart
|
|
446
|
+
data={networthSince2010} // [{ x: new Date(...), y: 10000 }, ... { y: 1_500_000 }]
|
|
447
|
+
yAxis={{
|
|
448
|
+
scaleType: "log",
|
|
449
|
+
domainMin: 1000, // log scales require y > 0; clamp out outliers
|
|
450
|
+
valueFormat: "abbreviated",
|
|
451
|
+
}}
|
|
452
|
+
xAxis={{ valueFormat: "date" }}
|
|
453
|
+
/>
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Y-only for this release. Log scales require strictly positive
|
|
457
|
+
values — set a positive `domainMin` to clip zeros / negatives.
|
|
458
|
+
|
|
459
|
+
## Annotations & range bands
|
|
460
|
+
|
|
461
|
+
Annotations are commentary layered on top of the marks —
|
|
462
|
+
datum-anchored labels (a "Q1 earnings" callout above one bar) or
|
|
463
|
+
shaded vertical bands (a "Q4" shaded region across a date range).
|
|
464
|
+
They live outside `marks` so toggling commentary doesn't touch
|
|
465
|
+
the data:
|
|
466
|
+
|
|
467
|
+
```tsx
|
|
468
|
+
<LineChart
|
|
469
|
+
data={pricesByDate}
|
|
470
|
+
xAxis={{ valueFormat: "date" }}
|
|
471
|
+
annotations={[
|
|
472
|
+
// Datum-anchored — floats near the top of the plot at this X.
|
|
473
|
+
{
|
|
474
|
+
x: new Date("2025-03-15"),
|
|
475
|
+
text: "Earnings",
|
|
476
|
+
color: "#1FA92E",
|
|
477
|
+
position: "top",
|
|
478
|
+
},
|
|
479
|
+
// Range band — shaded vertical region between two dates.
|
|
480
|
+
{
|
|
481
|
+
xRange: [new Date("2025-10-01"), new Date("2025-12-31")],
|
|
482
|
+
text: "Q4",
|
|
483
|
+
color: "#3B82F6",
|
|
484
|
+
position: "inside",
|
|
485
|
+
},
|
|
486
|
+
// Range band constrained to a Y window.
|
|
487
|
+
{
|
|
488
|
+
xRange: [new Date("2025-04-01"), new Date("2025-06-30")],
|
|
489
|
+
yRange: [40, 60],
|
|
490
|
+
text: "target zone",
|
|
491
|
+
color: "#F59E0B",
|
|
492
|
+
},
|
|
493
|
+
]}
|
|
494
|
+
/>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
| Field | Notes |
|
|
498
|
+
| --- | --- |
|
|
499
|
+
| `x` | Datum anchor (use **either** `x` or `xRange`, not both). Accepts `Date`. |
|
|
500
|
+
| `xRange: [from, to]` | Range band endpoints. Accepts `Date`. |
|
|
501
|
+
| `yRange?: [lo, hi]` | Optional vertical extent (data coords). Defaults to full plot. |
|
|
502
|
+
| `text?` | Optional label. Omit for marker-only bands. |
|
|
503
|
+
| `color?` | Band fill / label color. Defaults to system blue (bands) / label (labels). |
|
|
504
|
+
| `position?` | `"top" \| "bottom" \| "inside"`. Default `"top"`. |
|
|
505
|
+
| `fontSize?` | Label font size in pt. Default 11. |
|
|
506
|
+
|
|
507
|
+
Drawn under the tooltip so the active callout always paints on
|
|
508
|
+
top.
|
|
509
|
+
|
|
237
510
|
## Axis value formatters
|
|
238
511
|
|
|
239
512
|
Format the tick labels on either axis without writing custom Swift.
|
|
@@ -428,9 +701,11 @@ when two slices or points share the same y. Pies emit the slice
|
|
|
428
701
|
index on tap; cartesian charts emit the first cartesian mark's
|
|
429
702
|
index for the selected X.
|
|
430
703
|
|
|
431
|
-
For pie / donut charts
|
|
432
|
-
|
|
433
|
-
|
|
704
|
+
For pie / donut charts, `onSelect` fires on slice taps via
|
|
705
|
+
`chartAngleSelection`. As of 1.0, you can either:
|
|
706
|
+
|
|
707
|
+
1. **Drive `centerLabel` from the selection** — classic donut-hole
|
|
708
|
+
readout pattern (still the right call for compact dashboards):
|
|
434
709
|
|
|
435
710
|
```tsx
|
|
436
711
|
import { useState } from "react";
|
|
@@ -452,6 +727,122 @@ const [center, setCenter] = useState({ value: "$148K", label: "Total" });
|
|
|
452
727
|
/>
|
|
453
728
|
```
|
|
454
729
|
|
|
730
|
+
2. **Enable the visual callout** with `tooltip.enabled` — see
|
|
731
|
+
[Pie tooltip & slice highlight](#pie-tooltip--slice-highlight)
|
|
732
|
+
below. The callout, slice bump, and dim-others animation are all
|
|
733
|
+
native-drawn; you don't have to write any of it.
|
|
734
|
+
|
|
735
|
+
## Pie tooltip & slice highlight
|
|
736
|
+
|
|
737
|
+
Pass `tooltip` to `<PieChart>` and the chart will:
|
|
738
|
+
|
|
739
|
+
1. **Bump the selected slice** outward (`tooltip.sliceScale`,
|
|
740
|
+
default `1.05`). Implemented by shrinking the unselected slices
|
|
741
|
+
in tandem so the bump can't overflow the chart frame.
|
|
742
|
+
2. **Dim unselected slices** to `tooltip.dimOpacity` (default
|
|
743
|
+
`0.3`).
|
|
744
|
+
3. **Draw a leader line + callout** from the slice's outer edge to
|
|
745
|
+
a bubble anchored just outside the chart's outer radius at the
|
|
746
|
+
slice's midpoint angle. The callout is clamped to the chart's
|
|
747
|
+
bounds so it never spills past the host view.
|
|
748
|
+
4. **Toggle on re-tap** — tapping the same slice again clears the
|
|
749
|
+
selection.
|
|
750
|
+
5. **Dismiss on miss** — tapping empty area inside the chart frame
|
|
751
|
+
(the donut hole, corners, gaps) clears too.
|
|
752
|
+
|
|
753
|
+
```tsx
|
|
754
|
+
import { useRef } from "react";
|
|
755
|
+
import { Pressable, Text, View } from "react-native";
|
|
756
|
+
import { PieChart, type PieChartHandle } from "rn-native-ios-charts";
|
|
757
|
+
|
|
758
|
+
const chartRef = useRef<PieChartHandle>(null);
|
|
759
|
+
|
|
760
|
+
<View style={{ alignItems: "center" }}>
|
|
761
|
+
<PieChart
|
|
762
|
+
ref={chartRef}
|
|
763
|
+
style={{ width: 240, height: 240 }}
|
|
764
|
+
data={portfolio}
|
|
765
|
+
innerRadius={0.62}
|
|
766
|
+
angularInset={2}
|
|
767
|
+
cornerRadius={4}
|
|
768
|
+
tooltip={{
|
|
769
|
+
enabled: true,
|
|
770
|
+
valuePrefix: "$",
|
|
771
|
+
valueDecimals: 0,
|
|
772
|
+
backgroundColor: "#161618",
|
|
773
|
+
textColor: "#FFFFFF",
|
|
774
|
+
borderColor: "#2A2A2D",
|
|
775
|
+
// Pie-specific tuning:
|
|
776
|
+
dimOpacity: 0.3, // fade unselected slices
|
|
777
|
+
sliceScale: 1.05, // bump selected slice
|
|
778
|
+
}}
|
|
779
|
+
onSelect={(point) => {
|
|
780
|
+
if (point) console.log(`${point.x}: $${point.y}`);
|
|
781
|
+
}}
|
|
782
|
+
/>
|
|
783
|
+
{/* External dismiss button — sits OUTSIDE the chart so it
|
|
784
|
+
doesn't fight the chart's gestures. See the section below
|
|
785
|
+
for why wrapping the chart in <Pressable> doesn't work. */}
|
|
786
|
+
<Pressable onPress={() => chartRef.current?.clearSelection()}>
|
|
787
|
+
<Text>Clear selection</Text>
|
|
788
|
+
</Pressable>
|
|
789
|
+
</View>
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### Dismissing the selection
|
|
793
|
+
|
|
794
|
+
| Action | What happens |
|
|
795
|
+
| --- | --- |
|
|
796
|
+
| **Tap a different slice** | Selection switches to that slice. |
|
|
797
|
+
| **Tap the same selected slice** | Selection clears (toggle). |
|
|
798
|
+
| **`chartRef.current?.clearSelection()`** | Selection clears programmatically. |
|
|
799
|
+
|
|
800
|
+
> **Note.** An earlier alpha had a "tap empty area inside the chart frame to clear" path via a transparent backdrop, but the backdrop's `.onTapGesture` competed with `chartAngleSelection`'s slice-tap gesture and made the tooltip flicker / fail to appear. It's been removed. A geometry-aware version that only fires on taps outside the pie's angular footprint is on the roadmap.
|
|
801
|
+
|
|
802
|
+
### Pitfall — don't wrap the chart in `<Pressable>`
|
|
803
|
+
|
|
804
|
+
Tempting pattern: `<Pressable onPress={clear}><PieChart /></Pressable>`.
|
|
805
|
+
Broken. RN's responder chain claims taps inside the Pressable
|
|
806
|
+
*before* SwiftUI's `chartAngleSelection` sees them, so every slice
|
|
807
|
+
tap fires `clear()` instead of selecting the slice. The chart
|
|
808
|
+
appears unresponsive to taps.
|
|
809
|
+
|
|
810
|
+
For tap-outside-the-chart dismiss, place the `<Pressable>` as a
|
|
811
|
+
**sibling** of the chart (above, below, or absolutely positioned
|
|
812
|
+
behind it with `pointerEvents="box-only"`), never wrapping it.
|
|
813
|
+
The chart already handles "tap empty area inside my own bounds"
|
|
814
|
+
via its internal backdrop — you only need the external Pressable
|
|
815
|
+
for clicks well away from the chart.
|
|
816
|
+
|
|
817
|
+
`clearSelection()` is the single method on the shared `ChartHandle`
|
|
818
|
+
type — every wrapper (`PieChart`, `LineChart`, `BarChart`,
|
|
819
|
+
`AreaChart`, `ScatterChart`, `RangeBarChart`) `forwardRef`s the
|
|
820
|
+
same interface. `PieChartHandle` is kept as a type alias for
|
|
821
|
+
`ChartHandle` so existing code keeps working:
|
|
822
|
+
|
|
823
|
+
```tsx
|
|
824
|
+
import { useRef } from "react";
|
|
825
|
+
import { LineChart, type ChartHandle } from "rn-native-ios-charts";
|
|
826
|
+
|
|
827
|
+
const chartRef = useRef<ChartHandle>(null);
|
|
828
|
+
|
|
829
|
+
<LineChart ref={chartRef} data={...} tooltip={{ enabled: true }} />
|
|
830
|
+
|
|
831
|
+
// Anywhere:
|
|
832
|
+
chartRef.current?.clearSelection();
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
For consumers building their own wrappers, `useChartHandle(ref)`
|
|
836
|
+
is exported — it returns the `clearSelectionToken` you pass to
|
|
837
|
+
`<Chart>` and wires up the imperative method on the ref.
|
|
838
|
+
|
|
839
|
+
### When `tooltip.enabled` is `false`
|
|
840
|
+
|
|
841
|
+
`onSelect` still fires on slice taps (so the `centerLabel` pattern
|
|
842
|
+
keeps working), but no leader line / callout / highlight is drawn.
|
|
843
|
+
This mirrors the cartesian charts' opt-in tooltip behavior — charts
|
|
844
|
+
stay static unless you explicitly enable the interactive layer.
|
|
845
|
+
|
|
455
846
|
## Supported marks
|
|
456
847
|
|
|
457
848
|
| Mark type | What it draws |
|
|
@@ -499,7 +890,15 @@ const [center, setCenter] = useState({ value: "$148K", label: "Total" });
|
|
|
499
890
|
scrolling. See [Horizontal scrolling](#horizontal-scrolling-for-long-time-series).
|
|
500
891
|
- `categoryColors`: map `category` strings → colors. See
|
|
501
892
|
[Category color palettes](#category-color-palettes--categorycolors).
|
|
502
|
-
- `animate`:
|
|
893
|
+
- `animate`: legacy boolean toggle. Use `animation` (below) for
|
|
894
|
+
richer control; `animate` stays as a shorthand for
|
|
895
|
+
`{ enabled: true }`.
|
|
896
|
+
- `animation`: chart-level animation config — `enabled`,
|
|
897
|
+
`duration`, `curve`, `entrance`, `cartesianDimOnSelect`. See
|
|
898
|
+
[Animation config](#animation-config--animation).
|
|
899
|
+
- `annotations`: datum-anchored labels and shaded range bands
|
|
900
|
+
drawn over the marks. See
|
|
901
|
+
[Annotations & range bands](#annotations--range-bands).
|
|
503
902
|
|
|
504
903
|
`xAxis` / `yAxis` honor every field — `labelColor`, `labelFontSize`,
|
|
505
904
|
`gridColor`, `gridLines`, `tickLabels`, plus optional `[domainMin,
|
package/docs/demo-v1.gif
ADDED
|
Binary file
|
package/docs/demo-v1.mp4
ADDED
|
Binary file
|