rn-native-ios-charts 0.2.3 → 1.0.0-alpha.4

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 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.0...HEAD
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) — every chart type, native gradients, interactive tooltips, and pie center-label snapping.
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 there's no visual callout `onSelect` fires
432
- on slice taps via `chartAngleSelection`, and the natural place to
433
- display the info is the `centerLabel`:
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`: toggle SwiftUI's native ease-in-out on data changes.
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,
Binary file
Binary file