rn-native-ios-charts 0.1.0 → 0.2.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 CHANGED
@@ -6,6 +6,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] — 2026-05-11
10
+
11
+ Production-grade upgrades: trading-chart props, multi-series support,
12
+ axis value formatters, richer tooltip + selection payloads, and an
13
+ axis-config bug fix that surfaced once people actually customized
14
+ their axes.
15
+
16
+ ### Added
17
+
18
+ - **`tightX`** prop on `<Chart />` / `<LineChart />` / `<BarChart />`
19
+ — zeros out SwiftUI Charts' default plot-dimension X padding so
20
+ the first and last data points sit flush against the chart's left
21
+ and right edges. The "Robinhood / Apple Stocks" look. Pair with
22
+ `xAxis={{ hidden: true }}` for a clean trading-chart aesthetic.
23
+ - **`scrollableX`** + **`visibleXCount`** — enables SwiftUI's
24
+ native `chartScrollableAxes(.horizontal)` plus
25
+ `chartXVisibleDomain(length:)`. Use this instead of wrapping the
26
+ chart in an RN `<ScrollView horizontal>` — native scrolling
27
+ keeps tooltip coordinates correct and avoids gesture conflicts
28
+ with the selection scrubber.
29
+ - **`categoryColors`** chart-level prop — maps `category` →
30
+ `ColorValue`. Translates to SwiftUI's
31
+ `chartForegroundStyleScale`. Define a palette once at the chart
32
+ level instead of repeating `color` on every datum.
33
+ - **Axis value formatters.** `AxisConfig` now honors `valueFormat`
34
+ (`"raw" | "currency" | "percent" | "abbreviated" | "decimal"`),
35
+ `currencyCode`, `valueDecimals`, `valuePrefix`, and `valueSuffix`.
36
+ Common patterns:
37
+ ```tsx
38
+ yAxis={{ valueFormat: "currency", currencyCode: "USD" }}
39
+ yAxis={{ valuePrefix: "$", valueDecimals: 0 }} // symbol-only
40
+ yAxis={{ valueFormat: "abbreviated" }} // 1K / 1.2M
41
+ yAxis={{ valueFormat: "percent" }} // 0.5 → "50%"
42
+ ```
43
+ - **Multi-series tooltip.** `tooltip.multiSeries` renders one row
44
+ per cartesian mark at the selected X (color dot + series name +
45
+ formatted value). Drops to a single-row callout automatically
46
+ when only one mark is present. Useful for OHLC stock charts and
47
+ side-by-side comparisons.
48
+ - **`markIndex` + `pointIndex` in the `onSelect` payload.** Lets
49
+ consumers locate the datum in their `marks` array
50
+ deterministically — value-only matching is fragile when two
51
+ slices share the same y. Existing single-key consumers continue
52
+ to work; the indices are additive.
53
+ - **Multi-line `<LineChart>` via the new `series` prop.** Pass an
54
+ array of `{ name, color, data, ...overrides }` to draw multiple
55
+ lines on the same plot. Each series' `name` becomes the
56
+ `category` key, the legend label, and the row label in the
57
+ multi-series tooltip. The existing `data` prop still works for
58
+ single-series charts.
59
+ - **Bar chart enhancements.** Two new fields on `bar` marks (and
60
+ the matching `<BarChart>` props):
61
+ - `position: "auto" | "stacked" | "grouped"` — multi-series
62
+ positioning. `"stacked"` applies SwiftUI's
63
+ `positionAdjustment(.stacking)`; `"grouped"` applies
64
+ `position(by: .value("Series", category))` so bars sit
65
+ side-by-side.
66
+ - `horizontal: boolean` — swaps the X and Y axes for `bar`
67
+ marks. Use for Top-N lists and ranked leaderboards.
68
+
69
+ ### Fixed
70
+
71
+ - **Axis customization fields now actually apply.** `xAxis` /
72
+ `yAxis` config has supported `labelColor`, `labelFontSize`,
73
+ `gridColor`, `gridLines`, and `tickLabels` since v0.1.0, but the
74
+ Swift side only toggled `.hidden` vs `.automatic` and silently
75
+ ignored the rest. Replaced the boolean toggle with a full
76
+ `AxisMarks` builder so every field is honored.
77
+
78
+ **Migration note:** if you were passing these fields and worked
79
+ around the lack of effect (e.g. extra padding to hide the
80
+ default label color), your chart will now render with the
81
+ requested style. Visual diffs are possible.
82
+
9
83
  ## [0.1.0] — 2026-05-11
10
84
 
11
85
  Initial public release. iOS-only Expo module that bridges every SwiftUI
@@ -57,5 +131,6 @@ primitive plus convenience wrappers.
57
131
  `View` so consuming code doesn't need to feature-detect. Pair with
58
132
  `isChartSupported()` to swap in an alternative renderer.
59
133
 
60
- [Unreleased]: https://github.com/abdallaemadeldin/rn-native-ios-charts/compare/v0.1.0...HEAD
134
+ [Unreleased]: https://github.com/abdallaemadeldin/rn-native-ios-charts/compare/v0.2.0...HEAD
135
+ [0.2.0]: https://github.com/abdallaemadeldin/rn-native-ios-charts/compare/v0.1.0...v0.2.0
61
136
  [0.1.0]: https://github.com/abdallaemadeldin/rn-native-ios-charts/releases/tag/v0.1.0
package/README.md CHANGED
@@ -136,6 +136,227 @@ import { LineChart } from "rn-native-ios-charts";
136
136
  />
137
137
  ```
138
138
 
139
+ ## Trading-chart preset — `tightX` + hidden axes
140
+
141
+ For the Robinhood / Apple Stocks aesthetic — line flush against
142
+ both screen edges, no axes, no grid, deep gradient fill. The `tightX`
143
+ prop zeros out SwiftUI Charts' default plot-dimension padding so the
144
+ first and last points sit at the chart's left and right edges.
145
+
146
+ ```tsx
147
+ import { LineChart } from "rn-native-ios-charts";
148
+
149
+ <LineChart
150
+ style={{ width: "100%", height: 260 }}
151
+ data={dailyPrice}
152
+ color="#1FA92E"
153
+ lineWidth={3}
154
+ interpolation="catmullRom"
155
+ area={{ startOpacity: 0.55, endOpacity: 0 }}
156
+ tightX
157
+ xAxis={{ hidden: true }}
158
+ yAxis={{ hidden: true }}
159
+ tooltip={{ enabled: true, valuePrefix: "$" }}
160
+ />
161
+ ```
162
+
163
+ ## Multi-line charts — `<LineChart series={…} />`
164
+
165
+ Render multiple lines on the same plot by passing a `series` array
166
+ instead of a single `data` array. Each series has its own color and
167
+ its own line config (width, dash, interpolation, points, symbol,
168
+ area) — chart-level props act as fallbacks.
169
+
170
+ ```tsx
171
+ import { LineChart } from "rn-native-ios-charts";
172
+
173
+ <LineChart
174
+ // Chart-level defaults
175
+ lineWidth={2}
176
+ interpolation="catmullRom"
177
+ series={[
178
+ {
179
+ name: "Revenue",
180
+ color: "#1FA92E",
181
+ data: revenueByMonth,
182
+ area: { startOpacity: 0.4, endOpacity: 0 }, // shaded under this one
183
+ },
184
+ {
185
+ name: "Expenses",
186
+ color: "#F59E0B",
187
+ data: expensesByMonth,
188
+ lineWidth: 3,
189
+ dashArray: [6, 4], // dashed
190
+ },
191
+ {
192
+ name: "Forecast",
193
+ color: "#3B82F6",
194
+ data: forecastByMonth,
195
+ interpolation: "linear",
196
+ showPoints: true,
197
+ symbol: "diamond",
198
+ },
199
+ ]}
200
+ tooltip={{ enabled: true, multiSeries: true, valuePrefix: "$" }}
201
+ />
202
+ ```
203
+
204
+ Each series' `name` becomes:
205
+
206
+ - the `category` key on every point (so SwiftUI groups them into one
207
+ continuous line),
208
+ - the legend label, and
209
+ - the row label in the multi-series tooltip (see below).
210
+
211
+ The single-series `data` prop still works for one-line charts — pass
212
+ either `data` **or** `series`, not both. If both, `series` wins.
213
+
214
+ ## Multi-series tooltip
215
+
216
+ Pair multi-line charts with `tooltip.multiSeries` to get a stacked-row
217
+ callout: one row per cartesian mark at the selected X, each with the
218
+ series' color dot, name, and formatted value.
219
+
220
+ ```tsx
221
+ <LineChart
222
+ series={[ /* multiple series as above */ ]}
223
+ tooltip={{
224
+ enabled: true,
225
+ multiSeries: true, // <-- enables the stacked-row callout
226
+ valuePrefix: "$",
227
+ backgroundColor: "#161618",
228
+ textColor: "#FFFFFF",
229
+ borderColor: "#2A2A2D",
230
+ }}
231
+ />
232
+ ```
233
+
234
+ When the chart has only one cartesian mark, `multiSeries` silently
235
+ falls back to the regular single-row tooltip — safe to leave on.
236
+
237
+ ## Axis value formatters
238
+
239
+ Format the tick labels on either axis without writing custom Swift.
240
+ Supports four common formats plus optional prefix/suffix; works on
241
+ numeric axes (in practice: the Y axis, since X is `String`).
242
+
243
+ ```tsx
244
+ <LineChart
245
+ data={annualRevenue}
246
+ yAxis={{ valueFormat: "currency", currencyCode: "USD" }}
247
+ />
248
+
249
+ <LineChart
250
+ data={percentReturns}
251
+ yAxis={{ valueFormat: "percent" }} // 0.5 → "50%"
252
+ />
253
+
254
+ <LineChart
255
+ data={networthOverTime}
256
+ yAxis={{ valueFormat: "abbreviated" }} // 1K, 1.2M, 3.4B
257
+ />
258
+
259
+ <LineChart
260
+ data={returns}
261
+ yAxis={{ valuePrefix: "$", valueDecimals: 0 }} // symbol-only "$50,000"
262
+ />
263
+ ```
264
+
265
+ | `valueFormat` | Output (en-US) | Notes |
266
+ | ------------- | ------------------------- | -------------------------------------- |
267
+ | `"raw"` (default) | `50000` | Plain number with `valueDecimals`. |
268
+ | `"currency"` | `$50,000.00` | Locale-aware. Uses `currencyCode`. |
269
+ | `"percent"` | `50%` | SwiftUI multiplies by 100 — pass `0.5` to render `"50%"`. For pre-scaled (0–100) values, use `valueSuffix: "%"` instead. |
270
+ | `"abbreviated"` | `50K`, `1.2M`, `3.4B` | Compact notation. |
271
+ | `"decimal"` | `50,000.00` | Plain decimal with thousands separators. |
272
+
273
+ `valuePrefix` / `valueSuffix` are applied after the format style, so
274
+ `valueFormat: "decimal" + valuePrefix: "$"` gives you symbol-only
275
+ currency without locale code lookups.
276
+
277
+ ## Category color palettes — `categoryColors`
278
+
279
+ When your data has `category` values, set a chart-level palette
280
+ instead of repeating `color` on every datum. Translates to SwiftUI's
281
+ `chartForegroundStyleScale`.
282
+
283
+ ```tsx
284
+ <LineChart
285
+ series={[
286
+ { name: "Cash", data: cashData },
287
+ { name: "Stocks", data: stocksData },
288
+ { name: "Bonds", data: bondsData },
289
+ ]}
290
+ categoryColors={{
291
+ Cash: "#1FA92E",
292
+ Stocks: "#3B82F6",
293
+ Bonds: "#F59E0B",
294
+ }}
295
+ />
296
+ ```
297
+
298
+ Per-series `color` (or per-point `color`) always overrides
299
+ `categoryColors` when both are set.
300
+
301
+ ## Bar charts — stacking & horizontal
302
+
303
+ `<BarChart>` (and `bar` marks on the generic `<Chart>`) accept two
304
+ extra fields for multi-series layouts:
305
+
306
+ ```tsx
307
+ // Stacked bars
308
+ <BarChart
309
+ data={[
310
+ { x: "Q1", y: 24, category: "Revenue" },
311
+ { x: "Q1", y: 18, category: "Expenses" },
312
+ { x: "Q2", y: 31, category: "Revenue" },
313
+ { x: "Q2", y: 22, category: "Expenses" },
314
+ ]}
315
+ position="stacked"
316
+ categoryColors={{ Revenue: "#1FA92E", Expenses: "#F59E0B" }}
317
+ />
318
+
319
+ // Grouped (side-by-side)
320
+ <BarChart
321
+ data={/* same data */}
322
+ position="grouped"
323
+ categoryColors={{ Revenue: "#1FA92E", Expenses: "#F59E0B" }}
324
+ />
325
+
326
+ // Horizontal bars — Top-N / ranked leaderboards
327
+ <BarChart
328
+ data={topAssetsByValue} // [{ x: "AAPL", y: 38000 }, ...]
329
+ horizontal
330
+ cornerRadius={4}
331
+ />
332
+ ```
333
+
334
+ | Prop | Effect |
335
+ | ------------ | ---------------------------------------------------------------------- |
336
+ | `position: "auto"` | SwiftUI's default. |
337
+ | `position: "stacked"` | Applies `.positionAdjustment(.stacking)`. |
338
+ | `position: "grouped"` | Applies `.position(by: .value("Series", category))`. |
339
+ | `horizontal: true` | Swaps X and Y on `BarMark` — labels on the Y axis. |
340
+
341
+ ## Horizontal scrolling for long time series
342
+
343
+ When you have more data points than fit on screen, use SwiftUI's
344
+ native `chartScrollableAxes(.horizontal)` instead of wrapping the
345
+ chart in an RN `<ScrollView horizontal>` — that wrapper would steal
346
+ the scrubber's pan gesture and shift the tooltip's touch coordinates.
347
+
348
+ ```tsx
349
+ <LineChart
350
+ data={twoYearsOfDailyData} // ~730 points
351
+ scrollableX // enables native horizontal scroll
352
+ visibleXCount={30} // show ~30 days per "page"
353
+ tooltip={{ enabled: true }} // scrubber + tooltip still work
354
+ />
355
+ ```
356
+
357
+ `visibleXCount` is optional — omit it (or pass 0) to let SwiftUI
358
+ auto-decide.
359
+
139
360
  ## Interactivity — native tooltips & selection
140
361
 
141
362
  All cartesian charts (line, area, bar, point, rectangle) support
@@ -189,9 +410,24 @@ the leftmost / rightmost / topmost data points.
189
410
  Fires every time the selection changes (including when it clears):
190
411
 
191
412
  ```ts
192
- onSelect?: (point: { x: string; y: number } | null) => void;
413
+ type SelectedPoint = {
414
+ x: string;
415
+ y: number;
416
+ /** Index of the mark this point belongs to (0-based). */
417
+ markIndex: number;
418
+ /** Index of the point within that mark's data (0-based). */
419
+ pointIndex: number;
420
+ } | null;
421
+
422
+ onSelect?: (point: SelectedPoint) => void;
193
423
  ```
194
424
 
425
+ The `markIndex` + `pointIndex` pair locates the datum in the caller's
426
+ `marks` array deterministically — value-only matching is fragile
427
+ when two slices or points share the same y. Pies emit the slice
428
+ index on tap; cartesian charts emit the first cartesian mark's
429
+ index for the selected X.
430
+
195
431
  For pie / donut charts there's no visual callout — `onSelect` fires
196
432
  on slice taps via `chartAngleSelection`, and the natural place to
197
433
  display the info is the `centerLabel`:
@@ -257,8 +493,25 @@ const [center, setCenter] = useState({ value: "$148K", label: "Total" });
257
493
  [Interactivity](#interactivity--native-tooltips--selection) above.
258
494
  - `onSelect(point)`: event fired when the user picks a point via the
259
495
  scrubber or taps a pie sector. Payload is `{ x, y }` or `null`.
496
+ - `tightX`: zero out plot-dimension X padding so the line / area
497
+ bleeds to both edges. See [Trading-chart preset](#trading-chart-preset--tightx--hidden-axes).
498
+ - `scrollableX` + `visibleXCount`: enable SwiftUI's native horizontal
499
+ scrolling. See [Horizontal scrolling](#horizontal-scrolling-for-long-time-series).
500
+ - `categoryColors`: map `category` strings → colors. See
501
+ [Category color palettes](#category-color-palettes--categorycolors).
260
502
  - `animate`: toggle SwiftUI's native ease-in-out on data changes.
261
503
 
504
+ `xAxis` / `yAxis` honor every field — `labelColor`, `labelFontSize`,
505
+ `gridColor`, `gridLines`, `tickLabels`, plus optional `[domainMin,
506
+ domainMax]` and `valueFormat` / `currencyCode` / `valueDecimals` /
507
+ `valuePrefix` / `valueSuffix` for tick label formatting. See
508
+ [Axis value formatters](#axis-value-formatters). (Fully wired from
509
+ v0.2.0 onward — v0.1.0 silently ignored everything except `hidden`.)
510
+
511
+ Bar marks additionally accept `position: "auto" | "stacked" |
512
+ "grouped"` and `horizontal: boolean` — see
513
+ [Bar charts — stacking & horizontal](#bar-charts--stacking--horizontal).
514
+
262
515
  Top-level utility:
263
516
 
264
517
  - `isChartSupported()`: runtime feature-detection helper — `true` on
@@ -14,6 +14,23 @@ internal struct ChartAxisConfig: Record {
14
14
  /// Explicit numeric domain. Both must be set to take effect.
15
15
  @Field var domainMin: Double?
16
16
  @Field var domainMax: Double?
17
+ /// Value-label format. Only meaningful for axes whose values are
18
+ /// Double (in practice: the Y axis — our X axis is `String`):
19
+ /// - "" / "raw" — no formatting, pass through
20
+ /// - "currency" — locale-aware currency (`currencyCode`)
21
+ /// - "percent" — multiplied by 100 with "%"
22
+ /// - "abbreviated" — compact "1K", "1.2M", "3.4B"
23
+ /// - "decimal" — plain number with `decimals` fraction digits
24
+ @Field var valueFormat: String = ""
25
+ /// Used when `valueFormat == "currency"`. Default "USD".
26
+ @Field var currencyCode: String = "USD"
27
+ /// Fraction digits for numeric formatters that respect it. Default 0.
28
+ @Field var valueDecimals: Int = 0
29
+ /// Prepended to the formatted value, e.g. "$" when not using the
30
+ /// currency format. Useful for custom prefixes/suffixes.
31
+ @Field var valuePrefix: String = ""
32
+ /// Appended to the formatted value, e.g. "%" or " years".
33
+ @Field var valueSuffix: String = ""
17
34
 
18
35
  init() {}
19
36
  }
@@ -56,6 +73,11 @@ internal struct ChartTooltipConfig: Record {
56
73
  @Field var showDot: Bool = true
57
74
  /// Show the x label above the y value in the callout.
58
75
  @Field var showTitle: Bool = true
76
+ /// Render one row per mark at the selected X (color dot + series
77
+ /// name + value). Falls back to single-row mode when the chart has
78
+ /// only one cartesian mark anyway. Useful for OHLC stock charts
79
+ /// and side-by-side series comparisons.
80
+ @Field var multiSeries: Bool = false
59
81
  @Field var backgroundColor: UIColor?
60
82
  @Field var textColor: UIColor?
61
83
  @Field var borderColor: UIColor?
@@ -43,6 +43,14 @@ internal struct ChartMark: Record {
43
43
  @Field var cornerRadius: Double = 0
44
44
  /// Bar width (pt). 0 = auto.
45
45
  @Field var barWidth: Double = 0
46
+ /// Bar positioning when multiple BarMarks share an X:
47
+ /// - "auto" (default) — SwiftUI's default behavior
48
+ /// - "stacked" — explicit `.positionAdjustment(.stacking)`
49
+ /// - "grouped" — side-by-side via `.position(by: category)`
50
+ @Field var position: String = "auto"
51
+ /// Horizontal bars — swap the X and Y axes for `bar` marks. Use
52
+ /// for Top-N lists, ranked leaderboards, etc.
53
+ @Field var horizontal: Bool = false
46
54
 
47
55
  // ─── Sector (pie / donut) ───
48
56
  /// Inner radius as a ratio of outer, 0–1. 0 = full pie, 0.62 = thin donut.