Research date: 2026-07-30
Full React Native support is feasible without replacing the chart grammar or
the D3-based compiler. It is not an adapter-only project. The reusable unit is
the keyed ChartScene; sizing, text, paint resolution, rendering, gestures,
tooltips, animation, accessibility, and raster export need native
implementations.
Build in this order:
- Keep the compiler and existing focus algorithms DOM-free, extract the browser-owned interaction state, and introduce a renderer-neutral transition plan.
- Ship
react-native-svgas the default React Native renderer. - Add React Native Skia as an optional renderer for dense or frequently updated charts, after measuring the crossover.
- Use the same core refactor to test a NativeScript Core view backed by
@nativescript/canvas. - Treat Compose, SwiftUI, and Flutter as separate native products consuming a portable definition or scene schema. They cannot share the live TypeScript API.
React Native is the highest-confidence target because its JavaScript runtime can execute the existing definitions, accessors, imported D3 modules, scales, and scene compiler. NativeScript is expected to reuse those layers, subject to a bundler and runtime proof. Kotlin, Swift, and Dart cannot preserve arbitrary JavaScript callbacks without embedding a JavaScript runtime.
Parity should be measured by outcome, not by reproducing browser mechanics.
| Contract | Native parity |
|---|---|
| Data-to-scene behavior | The same definition and data produce equal normalized domains, mark keys, topology, and focus order; numeric geometry and runtime-formatted labels meet explicit tolerances. |
| Visual behavior | Every current scene primitive, mark family, coordinate system, guide, legend, gradient, and clip renders within documented tolerances. |
| Interaction behavior | Library-owned focus, grouped focus, activation, and pinning match. App-owned brush, zoom, scrub, range selection, and editing use documented native integration patterns. |
| Motion behavior | Stable-key enter/update/exit, interruption, reduced motion, and incompatible-path fallback retain the same semantics. |
| Accessibility behavior | A user can discover the chart, traverse or adjust the current datum, hear values, activate selections, and reach an application-provided complete data representation. |
| Export behavior | JavaScript targets retain deterministic resource-aware SVG. Native renderers add platform bitmap snapshots and file/share integration. |
| Web-only behavior | SSR, hydration, DOM callbacks, CSS selectors, and mouse-specific input remain supported by the web renderer; they are not native device requirements. |
Pixel-identical text across browser engines, Core Text, Android text, and Skia is not a useful acceptance criterion. Geometry can be exact when renderers consume the same scene; independently ported compilers need numeric tolerances for geometry too.
The repository already has most of the right compiler boundary:
createChartRuntimeandcreateChartScenetake definitions and logical dimensions and produce a scene.- Marks emit seven portable node types: group, rule, polyline, area, dot, rectangle, and label.
- Cartesian, facet, polar, and geographic marks all end at those primitives. Polar and geographic shapes carry path strings instead of requiring a DOM.
- Focusable
ChartPointrecords are separate from painted nodes. This permits renderer batching without losing datum identity or hit testing. - Resource-aware static SVG serialization through
renderChartSvgWithResourcesis already independent of a mounted browser surface. The lower-levelrenderChartSvgdoes not emit gradient and clip resources by itself. - The conformance corpus contains 100 cases, including 16 executable interaction cases and 51 interaction scenarios. It supplies coverage, but many cases combine definition, DOM mount, controls, selectors, and DOM-specific gesture code. Native conformance first requires extracting platform-neutral data and definition factories, then replacing mounts, application controllers, and drivers.
The public renderer and host boundary is browser-shaped:
| Evidence | Native consequence |
|---|---|
ChartSurface.element is Element; ChartRenderer.mount accepts HTMLElement; render callbacks expose HTMLElement and SVGSVGElement. |
A native renderer cannot implement the current public contract honestly. |
renderer.ts owns ResizeObserver, pointer and keyboard events, animation frames, DOM tooltips, focus, and reduced-motion queries. |
Reusable interaction state is mixed with browser services. |
canvas.ts creates HTML canvases and reads devicePixelRatio and media queries. |
Its painter can be extracted, but the current Canvas renderer cannot run in NativeScript or React Native. |
dom-text.ts reads computed CSS and measures through browser Canvas 2D. |
Native layout needs a different font-resolution and measurement service. |
RendererChart.tsx imports react-dom, mounts <div> elements, injects HTML, and portals tooltips into DOM elements. |
The React adapter is a web adapter, not a portable React component. |
Default theme colors use currentColor and CSS custom properties. |
Native renderers need concrete colors or platform-resolved semantic tokens. |
Gradient references are emitted as SVG/CSS url(#id) strings; scene nodes also carry className and ariaHidden. |
Structured paint references and semantic metadata should be separated from web serialization. |
SceneLabel and ChartTextMeasureOptions expose size and weight, but not family, style, stretch, direction, letter spacing, or locale. |
Native guide layout cannot reproduce the font that will paint the label. |
ResolvedScale.map is a function and ChartPoint.datum can contain any live value. |
The in-process scene is portable between JavaScript renderers, but is not a language-neutral serialized schema. |
The root remains browser-inclusive; @tanstack/charts/universal and @tanstack/charts/types select the DOM-free value and declaration graph. |
Native code has a supported boundary, while strict dependency checking still exposes the upstream ImageData declaration issue. |
Directional code inventory:
- The browser-heavy renderer, Canvas host, DOM export, DOM text measurement, and reconciliation files remain a substantial but isolated implementation layer.
- Most mark, scale, layout, polar, geographic, and scene construction code is renderer-independent.
Source counts change too quickly to serve as a credible reuse guarantee.
types.ts is now DOM-free, while renderer.ts and canvas.ts still mix
portable behavior with browser services. The React web adapter remains a
rewrite target because its UI is HTML plus react-dom.
flowchart TD
Definition["Live TypeScript chart definition"] --> Runtime["Shared runtime<br/>marks · D3 · scales · guides · layout"]
CompileInputs["Compiler inputs<br/>logical size · text metrics · theme input"] --> Runtime
Runtime --> Scene["In-process ChartScene<br/>geometry · keys · points · scale metadata"]
Scene --> State["Shared controller<br/>focus restoration · activation · pinning · transition plan · semantics"]
Host["Host capabilities<br/>frame clock · input · overlays · accessibility · snapshots"] --> State
State --> Web["Web<br/>SVG · Canvas · DOM overlays"]
State --> RNSVG["React Native<br/>react-native-svg · RN overlays"]
State --> Skia["React Native<br/>Skia · RN semantic overlays"]
State --> NS["NativeScript<br/>Canvas · native overlays"]
Portable["Portable definition / scene DTO"] --> Compose["Compose"]
Portable --> Flutter["Flutter"]
Portable --> Apple["SwiftUI / Core Graphics"]
Use narrow capabilities, not DOM-shaped polyfills.
Compiler inputs:
measureText(request): resolved font properties in; advance width and line metrics out, with optional ink bounds.resolvePaint(token): semantic or CSS-like theme input to renderer-usable paint.layoutSize: logical dimensions supplied by the host.
Host, controller, and surface capabilities:
requestFrame,cancelFrame, and reduced-motion state.- Platform coordinates to logical scene coordinates.
- Tooltip and application-owned overlay presentation.
- Focus-indicator geometry, style, visibility, and paint.
- Accessible summary, active-datum controls, actions, and announcements.
- Optional renderer-specific bitmap snapshots.
The compiler must remain a normal JavaScript operation. React Native worklets should only own frame-critical numeric state, such as gesture coordinates, crosshair position, preview transforms, paint interpolation, and a worklet-safe nearest-point index. Arbitrary accessors, D3 modules, tooltip formatters, and custom marks should not be serialized into the UI runtime. Reanimated worklets are a separate JavaScript context with serialization constraints.
Do not freeze the current ChartScene as a cross-language wire format.
- In-process scene: retains live data references, scale mapping functions, extension objects, and efficient indexes. React Native and NativeScript can consume this directly.
- Portable scene DTO: versioned JSON-compatible geometry, resolved paints, scale metadata, datum IDs, semantic descriptions, and interaction policies. Compose, SwiftUI, and Flutter can consume this, but live callbacks stay in their host language.
The proposed PORTABLE-CHART-SPEC.md registry
model is the right definition-level companion. $call and $data can describe
a supported portable subset. It should never claim to serialize arbitrary
JavaScript accessors, custom tooltip components, or renderer plugins.
- Keep the completed universal/DOM declaration split. Further separate shared renderer, focus, semantic, and transition contracts from browser host implementations.
- Keep the existing focus strategies, grouping, and spatial contracts. Move
focus restoration/order, pinned state, host input orchestration, default
tooltip content, and render-reason policy out of
renderer.tsinto a renderer-neutral controller. - Introduce keyed scene diffing, interpolation instructions, and interruption state as a transition planner. Current SVG motion is DOM attribute reconciliation; current Canvas motion is a whole-frame crossfade.
- Extract the Canvas 2D painter behind a structural drawing-context interface. Keep HTML canvas creation, pixel ratio, media queries, and export in the web host.
- Resolve theme colors before painting. Keep CSS variables and
currentColoras web-adapter inputs, not scene paints. - Expand typography to include family, size, weight, style, stretch, letter spacing, direction, locale, and font-scale policy. Measurement cache keys must include all of them.
- Preserve the resolved F-106 ownership rule: dynamic builders receive the default theme; application and platform overrides enter through typed inputs or the returned definition and merge during scene creation. Do not pass a builder the theme that it is itself responsible for returning.
- Keep deterministic, resource-aware SVG serialization in the shared JavaScript package.
- Define capability fallbacks for unsupported paint, path, filter, snapshot, and semantic features.
- Give gradients and other resources structured scene references; serialize
url(#id)only in the SVG renderer. - Treat typography and structured paints as public API migrations. Add backward-compatible unions and normalization first, or version the change; update migration docs and conformance before removing string paints or existing text-measure options.
This can remain additive for web consumers: add new internal contracts and
make the DOM host implement them. The browser-oriented root remains compatible;
cross-runtime consumers opt into @tanstack/charts/universal and
@tanstack/charts/types.
As of this research, React Native 0.86 is current stable. React Native 0.82 made the New Architecture mandatory. Set the initial support floor to React Native 0.86 and test Expo SDK 57 plus bare React Native 0.86. Supporting older minors later requires adding those CI lanes; a nominal floor without tests is not support. Version-one product scope is iOS and Android. Renderer dependencies may also run on macOS, Windows, web, or TV, but those platforms need their own input, accessibility, packaging, and conformance gates. React Native versions · React Native 0.82 · Expo SDK 57
The two supported dependency lanes differ:
| Dependency | Expo SDK 57 / RN 0.86 | Bare RN 0.86 |
|---|---|---|
react-native-svg |
Expo-pinned 15.15.4 | current compatible 15.15.x |
| Skia | Expo-pinned 2.6.2 | current compatible 2.x |
| Reanimated | Expo-pinned 4.5.1 | matching RN 0.86-compatible line |
| Worklets | managed with the Expo/Reanimated combination | matching line from the Reanimated compatibility table |
| Gesture Handler | Expo-pinned ~2.32.0 | current RN 0.86-compatible line |
Reanimated 4 is New-Architecture-only. Expo configures its supported plugin
combination; bare apps must install the matching Worklets package and plugin.
Gesture Handler also requires GestureHandlerRootView.
Expo Reanimated ·
Expo Gesture Handler ·
Expo SVG ·
Expo Skia ·
Reanimated compatibility
D3 is not expected to be the blocker. The imported d3-array, d3-scale,
d3-shape, and d3-geo code paths should be audited and executed under
Hermes rather than relying on a blanket package-family claim. d3-shape
explicitly supports path-string or Canvas-context output.
d3-shape
The scene maps directly:
| Scene primitive | React Native SVG |
|---|---|
| group, translate, clip | G, ClipPath, Rect |
| rule | Line or Path |
| polyline and area | Path |
| dot | Circle |
| rectangle | Rect |
| label | Text |
| linear gradient | Defs, LinearGradient, Stop |
react-native-svg supports Fabric and the relevant primitives on iOS,
Android, macOS, Windows, and web. Its 15.13+ compatibility line requires React
Native 0.78+, below the proposed floor. This is dependency support, not a
promise that the chart package supports every one of those platforms.
react-native-svg repository ·
usage reference
Why it should ship first:
- The current scene is already SVG-shaped.
- D3-generated polar and geographic path strings transfer directly.
- A retained node tree makes initial keyed updates and visual inspection straightforward.
- It avoids a required Skia binary for ordinary charts.
- It keeps the public renderer replaceable.
Known limits:
- The package Codegen configuration creates native component types for SVG primitives. A large tree may make React construction and Fabric commits expensive, but that is a benchmark hypothesis rather than a pre-measured limit.
- Native text metrics and baseline behavior differ from DOM SVG.
- The documented filter set is incomplete, and Android cannot apply a radial gradient focal point.
getBBox,isPointInFill, and related synchronous shape methods are marked experimental; core layout and hit testing must not depend on them. Shape implementation- RNSVG can parse stylesheet classes through
SvgCss, but the typed production renderer should resolve styles to props instead of depending on reparsed CSS. strokeDasharrayand uncommon line joins need normalization and compatibility tests.
Parsing the existing SVG string through SvgXml is useful as a one-day visual
probe, not a production architecture. SvgXml and SvgCss reparse markup,
lose scene identities and transition keys, and cannot preserve the current DOM
interaction and accessibility model.
RNSVG package Codegen configuration ·
RNSVG filter support ·
RNSVG CSS support
Skia should consume the same scene and host controller. It is the likely choice for dense scatterplots, heatmaps, streaming data, gesture-heavy charts, and animation, but the renderer crossover must be measured.
React Native Skia 2.x currently requires React Native 0.79+, React 19+, iOS 14+, and Android API 21+. Its documented binary contribution is roughly 6 MB on iOS and 4 MB on Android, so it should remain an isolated optional entry. Skia installation
Skia provides paths, clipping, gradients, synchronous font metrics, snapshots, and direct Reanimated shared-value integration. Canvas · text · gradients · animation
Do not replace the current web renderer with Skia. Its web target loads CanvasKit, adds roughly 2.9 MB compressed, and has different WebGL and API constraints. Skia web support
Do not begin with a custom Fabric surface. If profiling proves that SVG and Skia both fail a required workload, a single Codegen-backed native chart surface could accept a compact scene buffer. That decision would add iOS, Android, Codegen, serialization, and React Native version maintenance. React Native Codegen
Text is the largest visual-parity risk.
The current synchronous ChartTextMeasurer is a useful compiler seam, but a
React Native Text measurement may arrive after layout. The native host should
use this protocol:
- Resolve the complete font descriptor and issue keyed measurement requests.
- Return cached metrics synchronously when present.
- Use deterministic estimates for cache misses.
- Batch native measurement of misses.
- Rebuild the scene once when real measurements materially change guide bounds.
For the SVG renderer, test an offscreen native Text measurement surface and
onTextLayout; do not depend on experimental SVG bounding-box calls. For
Skia, Font can synchronously measure a resolved font, while complex scripts,
fallback, wrapping, and bidirectional text require Paragraph shaping. An
offscreen React Native Text result may not match RNSVG Text; measured and
painted output must agree closely enough to keep guide layout stable.
Skia font interface ·
Skia Paragraph
The test matrix must include:
- system and bundled fonts;
- unavailable weight fallback;
- emoji and mixed scripts;
- RTL;
- locale-specific shaping and number formatting;
- timezone, daylight-saving transitions, and non-Gregorian calendar settings;
- letter spacing and rotated labels;
- dynamic font scale;
- font loading after first render.
The public API needs an explicit choice for chart text scaling. Blindly applying unbounded system font scale can make a chart unreadable, while ignoring it harms accessibility. The likely contract is a host-level font-scaling policy plus an accessible nonvisual data representation.
- The React Native root
Viewsupplies logical width and height throughonLayout. - Scene coordinates should use density-independent logical units. Each renderer handles physical pixel density at paint or snapshot time.
- Orientation and container changes schedule a new scene build; they do not
require a
ResizeObserver. PlatformColorandDynamicColorIOSare opaque platform values, not portable strings. Each renderer resolves them, subscribes to appearance and accessibility changes, and usesprocessColorwhere native or animated props require it. Deterministic SVG export requires an explicit color scheme.- High-contrast capabilities differ across iOS and Android; the adapter maps the available accessibility state to semantic theme input rather than promising one cross-platform flag.
currentColor, CSS variables, inheritedfont-family, and CSS class styles remain web-only resolution mechanisms.
React Native Appearance · React Native AccessibilityInfo
Use one chart-level gesture detector, not one responder per mark:
- Convert touch, pointer, or hover coordinates to logical scene coordinates.
- Query the shared focus strategy and spatial index.
- Update the visual focus overlay at frame cadence.
- Commit semantic focus and pinned state at a coalesced cadence or gesture end, and emit application activation or domain-change events through typed integration points.
Simple tap and activation can use React Native responders. Scrubbing, pinch, pan, long press, and composed gestures should use Gesture Handler where available. For app-owned brush, zoom, scrubber, and editor behavior, the package supplies coordinates, focus primitives, integration contracts, and examples; it does not take ownership of application domain or range state. Gesture Handler recognizes gestures using native platform facilities and integrates with Reanimated. Gesture Handler
Input parity is capability-based:
| Web input | Native behavior |
|---|---|
| hover / pointer move | touch scrub, stylus/trackpad hover where available |
| click | tap |
| pinned click tooltip | tap or long press, with explicit dismiss |
| wheel zoom | pinch; trackpad/wheel where the platform reports it |
| drag brush | pan gesture with visible handles and minimum touch targets |
| arrow-key focus | adjustable screen-reader actions; hardware keys on supported targets |
| Escape | dismiss action, back action, or hardware key |
The 16 interaction conformance cases should call semantic drivers such as
focusAt, activate, pan, zoom, setBrush, and adjustDatum. Each
platform harness translates those operations to real input separately. Cases
whose controller is application-owned must extract and port that controller;
they are integration conformance, not new core behavior.
Launch gates include:
- arbitration with nested
ScrollView, navigation, and parent gestures; - axis locking, cancellation, and responder loss;
- coordinate conversion under view and chart transforms;
- Android back behavior for pinned state;
- the copy and memory cost of moving any spatial index into a worklet;
- minimum touch targets and handles near safe areas.
Painted geometry and accessibility must be separate outputs of the controller. Skia explicitly requires overlaid React Native views to make inner canvas elements accessible, and an SVG node per datum is not a scalable semantic tree. Skia canvas accessibility · React Native accessibility
Recommended native model:
- A non-accessible layout container with sibling semantic elements. React Native warns that VoiceOver can suppress nested accessible elements.
- One accessible summary element with label, description, and concise chart summary.
- One sibling adjustable active-datum controller with increment, decrement, and activate actions.
accessibilityValue.textcontaining series, category or time, value, and relevant interval.- Platform-appropriate announcement or live status when the active value changes; live-region behavior differs between iOS and Android.
- A visible focus marker and tooltip rendered through ordinary React Native views.
- Semantics and data hooks that let the application supply a virtualized list or table for complete inspection. Shipping that table is a new feature, not current package parity.
- Reduced-motion behavior driven by
AccessibilityInfo. - Visual renderer descendants hidden with the platform-appropriate
importantForAccessibility,accessibilityElementsHidden, or equivalent props to prevent duplicate or unusably large traversal.
VoiceOver and TalkBack testing on physical devices is an exit criterion. Thousands of transparent focusable views are not an acceptable implementation.
The shared semantic model can supply series and datum descriptions for
Apple’s AXChartDescriptor and audio graphs. React Native core, RNSVG, and
Skia do not expose that API; using it would require a custom iOS native
accessibility component and is not part of the first renderer.
Apple chart accessibility
The shared transition planner should produce stable-key enter, update, and exit operations from the currently painted state, not only previous and next scenes. Renderers then execute:
- numeric geometry and opacity interpolation;
- color interpolation;
- path interpolation when topology is compatible;
- a documented resampling or crossfade fallback when it is not;
- cancellation and restart from the current visual value;
- zero-duration output under reduced motion.
Skia’s path interpolation requires compatible path commands, so arbitrary path morphing cannot be delegated without normalization. Skia path interpolation
Per-frame React/RNSVG prop commits are safe only as a small-scene proof, not
the assumed production default. The motion spike must compare animated props
and UI-thread execution early, including path normalization, color processing,
and interruption from the value actually painted on the UI thread.
Reanimated useAnimatedProps
Transition state must live outside React render side effects. It must discard
stale concurrent scene builds, rebase after font/theme/size invalidation, and
pause or rebase when AppState moves between background and foreground.
- Keep resource-aware SVG string export in the shared JavaScript core. It does not require a mounted native surface.
- Define whether a PNG captures only chart paint or also React Native tooltip, legend, and focus overlays. Add explicit scale, background, color-scheme, and font-readiness options.
- Skia’s asynchronous snapshot runs on the UI thread and is required when textures are present. Generic view capture has separate native-surface and view-collapsing constraints; both paths need device proof.
- Add file and share helpers only in platform integration packages.
- Keep browser consumers on the existing root and DOM/SVG React adapter. React Native Web can render native components, but the current web implementation already has better SSR, hydration, CSS, text measurement, and semantic SVG behavior.
- Keep native callbacks typed to native surface and layout data; do not expose
fake
HTMLElementorSVGSVGElementvalues.
Skia canvas snapshots · Skia view snapshots
A WebView can demonstrate visual reuse, but it is not native support. Expo DOM components use a separate JavaScript engine, JSON-serialized asynchronous props, whole-tree rerenders, and no SSR/SSG or native children. Expo DOM components
@tanstack/charts
browser-compatible root
./universal
DOM-free definitions, marks, D3 integration, scene, focus, semantics
./types
DOM-free public contracts
@tanstack/react-charts
existing web React adapter
@tanstack/react-native-charts
native React adapter and react-native-svg renderer
./skia
optional Skia renderer subpath
The base native entry should peer-depend on React, React Native, and
react-native-svg. Skia, Reanimated, Worklets, and Gesture Handler should be
optional peers, declared through peerDependenciesMeta.optional, so a static
SVG chart does not acquire or autolink unrelated native modules. Do not use
optionalDependencies. The base implementation and declaration graph must
have no static reference to optional peers.
The native-only preview needs explicit types, react-native, and import
conditions. A future entry that serves different browser and native
implementations would also need an explicit browser condition before
react-native. Once an export matches, Metro does not append .native or
.web to that target. Test resolution through Metro/Expo, Node, and
TypeScript. An export target cannot point into another package, so any future
browser re-export needs a local shim and declared dependency or peer.
Metro package exports
NativeScript is the second-best reuse target because its runtime can execute the JavaScript built from the TypeScript core and expose native platform APIs directly. NativeScript Core supports custom native views, gestures, animation, and accessibility. Its Canvas project provides native-backed Canvas 2D, WebGL, and WebGPU packages. Ordinary JavaScript runs on the UI thread; Workers are isolated and communicate through JSON-serializable messages. Large scene compilation plus Canvas painting is therefore a first-order benchmark, not assumed reuse. NativeScript documentation · native APIs · custom native views · gestures · accessibility · multithreading · NativeScript Canvas
Recommended implementation:
- Extract the current Canvas painter from HTML canvas creation and DOM host behavior.
- Implement a NativeScript Core chart view that supplies size, pixel ratio, a Canvas context, frame scheduling, and gestures.
- Prove that the same definition and in-process scene compiler run unchanged through NativeScript’s bundler and runtime.
- Add Angular, Vue, Solid, Svelte, or React wrappers only after the Core view is stable.
React NativeScript is a React renderer over NativeScript views, not React Native compatibility. React Native components and ecosystem packages do not transfer. It should be a thin wrapper around a NativeScript Core chart view rather than the foundation. NativeScript React tutorial · React NativeScript repository
NativeScript proof requirements:
- correct text metrics, baselines, font fallback, and pixel ratio;
Path2D, clipping, gradients, dash arrays, joins, and transforms;- image snapshot/export;
- touch coordinate conversion and gesture composition;
- an accessible root and active-datum control implemented either with a small
number of real NativeScript views or custom iOS/Android accessibility
containers; NativeScript documents accessibility for
Viewobjects but no Canvas virtual-child semantic tree; - long-running animation and streaming stability;
- scene build and paint time at 1k, 10k, and 50k primitives;
- memory and UI-thread responsiveness on low-end Android.
If its Canvas implementation cannot meet these gates without platform-specific patches, stop rather than maintain a browser API imitation with divergent behavior.
| Target | Source reuse | Technical fit | Recommendation |
|---|---|---|---|
| Compose Multiplatform | No executable TypeScript reuse; definitions, scene DTOs, fixtures, and semantics can transfer. | Shared Kotlin API; Android uses Jetpack Compose and iOS Compose is canvas-rendered through Skiko. Custom Canvas semantics must be authored. | Best candidate for a shared Kotlin/Compose product after the portable contract is proven. |
| Flutter | No TypeScript reuse on native; requires a Dart port of the compiler/runtime or a Dart scene consumer. | CustomPainter maps cleanly to the scene and supports hit testing and virtual semantics; Flutter paints through its own engine/Impeller. |
Build only as a first-class Flutter product with dedicated ownership and add-to-app cost. |
SwiftUI Canvas |
Schema and fixtures only. | Uses GraphicsContext; Apple excludes per-element interaction and accessibility, so synthetic children/representations are required. |
Apple-only product or accessibility reference, not the cross-platform starting point. |
| UIKit / Core Graphics | Schema and fixtures only. | Separate lower-level custom-view path with maximum Apple control and no Android implementation. | Use only if SwiftUI Canvas cannot meet required fidelity or integration. |
| Swift Charts | Semantic mapping, not scene parity. | Supplies native marks, axes, legends, and chart accessibility, but owns layout and styling. | Optional native-style backend, not feature-for-feature TanStack rendering. |
| Lynx / ReactLynx | Pure TypeScript compiler may transfer after DOM removal. React Native components do not. | Lynx 4 exposes static-oriented SVG as one native view and no public Canvas element; per-mark input and semantics need overlays or custom native elements. | Watchlist until its graphics layer can support interactive dynamic scenes. |
| Separate UIKit and Android View renderers | Schema and fixtures only unless a JavaScript runtime is embedded. | Maximum control and maximum duplicated implementation. | Avoid unless profiling and commercial demand justify two permanent native engines. |
Primary references:
- Compose Multiplatform, iOS renderer explanation, iOS stable announcement, Compose custom graphics, Compose semantics, and Compose iOS accessibility
- Flutter
CustomPainter, Flutter architecture, add-to-app, and Flutter semantics - SwiftUI
Canvas, accessibility representations, AXChart, and Swift Charts - Lynx 4 elements, SVG element, and custom native components
For non-JavaScript targets, “same API” can mean equivalent chart concepts and conformance, not source compatibility. Accessors, formatters, custom scales, tooltip bodies, callbacks, and plugins must be declared as one of:
- portable declarative input;
- a named registry function;
- a host-language callback;
- JavaScript-only.
Sending a complete scene across a JavaScript-to-native or host-language boundary on every frame is not a durable dynamic-chart design. A portable scene DTO is appropriate for static charts, fixtures, export, and an initial renderer proof. A production native-language library eventually needs local layout, interaction, and animation logic.
| Capability | Shared work | React Native | NativeScript | Non-JS native |
|---|---|---|---|---|
| Definitions, accessors, transforms, D3 scales | Existing compiler through universal entries | Direct reuse after Hermes proof | Expected reuse after runtime proof | Declarative subset or reimplementation |
| Cartesian marks and guides | Existing scene | Render primitives | Canvas painter | Render DTO or reimplement |
| Facets and layered charts | Existing layout and groups | Direct reuse | Expected after runtime proof | Scene-compatible |
| Polar, pie, radar, gauge | Existing path-producing marks | SVG/Skia paths | Canvas paths | Scene-compatible |
| Geography and projections | Existing D3-generated paths | SVG/Skia paths | Canvas paths | Scene-compatible |
| Gradients and clipping | Existing scene data plus capability rules | New mapping, platform tests | New mapping, platform tests | New mapping |
| Text, axes, legend margins | Shared request/cache protocol | New native measurement | New native measurement | New native measurement |
| Theme and color | Shared semantic theme | New RN resolution | New NativeScript resolution | New platform resolution |
| Focus and grouped tooltip logic | Extract from web renderer | Reuse controller; new overlay | Reuse controller; new overlay | Port controller |
Activation / onSelect |
Shared callback contract | Tap and accessibility actions | Tap and accessibility actions | Platform input |
| Brush, zoom, scrubber, editor | Application-owned state plus semantic drivers | Native integration examples | Native integration examples | Platform implementations |
| Keyed animation | New shared transition planner | SVG and Skia execution | Canvas execution | Port planner |
| Focus indicator | New shared overlay contract | RN overlay or renderer layer | Canvas/native overlay | Platform overlay |
| Accessibility | New shared semantic model | RN sibling semantic controls | Feasibility gate: real views or native containers | Native semantics |
| Custom marks using scene primitives | Existing extension model | Reuse | Reuse | Equivalent host-language API |
| Renderer-specific custom output | None | Native renderer callback | Canvas/native callback | Platform-specific |
| Pure SVG export | Resource-aware JavaScript serializer | Reuse | Reuse if runtime proof passes | Native serializer or externally supplied SVG |
| Mounted PNG export | Platform capability | Snapshot/view capture | Canvas or platform capture, pending proof | Platform snapshot |
| SSR and hydration | Existing browser root and React host | Not applicable on device | Web target only | Not applicable on device |
Run the compiler under Hermes and NativeScript without DOM globals.
Pass conditions:
- Representative definitions import through Metro and NativeScript bundling.
- With locale, calendar, and timezone fixed, web, Hermes, and NativeScript produce exact normalized domains, keys, topology, and explicitly formatted labels. Compare numeric coordinates and normalized path data with a defined epsilon.
- DOM types are absent from the Charts declaration graph; strict dependency
checking reaches only the known upstream
ImageDatadeclaration. - Theme and text services are injected.
- No browser polyfill is required.
- Conditional exports and optional-peer isolation resolve through Expo/Metro, bare Metro, TypeScript, Jest, and a browser bundler.
Render a deliberately small set before porting all 100 cases:
- multi-line chart with gaps, rotated ticks, and a legend;
- stacked area with stable-key update;
- grouped and stacked bars;
- 10k-point scatter;
- labeled heatmap with clipping and gradients;
- faceted chart;
- pie/donut plus radar or gauge;
- geographic projection;
- grouped tooltip with touch scrub and pinning;
- zoom/pan or brush interaction;
- streaming update with interruption;
- custom scene mark.
Pass conditions:
- iOS and Android screenshots meet geometry and visual tolerances.
- Every sentinel has a usable VoiceOver and TalkBack path.
- Visible focus-indicator geometry, style, and visibility match the semantic focus state.
- Custom fonts, RTL, dark mode, high contrast, reduced motion, and orientation changes work.
- SVG and Skia consume the same scene and interaction controller.
Use release or debugOptimized builds on a low-end Android device and a
representative iPhone. Standard Android debug builds are not valid evidence.
Test 100, 1k, 10k, and 50k painted primitives, with an optional 100k stress case. Record:
- scene preparation and layout time;
- first paint;
- single-datum update;
- full data replacement;
- streaming update cadence;
- orientation and resize;
- touch scrub, pan, and zoom latency;
- nested-scroll arbitration, cancellation, and transformed hit coordinates;
- JS and UI frame time;
- memory;
- Fabric node count;
- native binary and application bundle delta;
- snapshot time and size.
The measured SVG/Skia crossover becomes documentation. Do not choose a point count from intuition.
- Run all 100 visual cases on iOS and Android for the default renderer.
- Run the performance and renderer-sensitive subset on Skia and NativeScript.
- Extract platform-neutral definition/data factories from all 100 cases.
- Translate all 16 interaction cases to semantic platform drivers, with application controllers remaining application-owned integration fixtures.
- Compare normalized domains, keys, and topology exactly; compare numeric geometry and normalized paths with the agreed tolerance before screenshot tolerances.
- Keep the DOM-free TypeScript compile and Metro import smoke alongside the current jsdom-oriented unit suite; add Hermes execution coverage.
- Test current web SSR, hydration, SVG, Canvas, and export to catch refactor regressions.
- Test stale-scene cancellation, concurrent updates, mid-flight interruption, font/theme/size invalidation, and app background/foreground rebasing.
- Use React Native Testing Library for adapter state and semantics, but require device tests for native drawing, gestures, and accessibility. Its own guidance notes that native behavior needs simulator or device validation. React Native Testing Library boundary
Do not claim supported native parity if any of these remain unresolved after the proof:
- text measurement causes repeated layout oscillation or materially wrong guides, or the measuring and painting stacks disagree on shaped text;
- the default renderer misses interaction frame budgets on ordinary charts;
- the semantic model cannot provide useful VoiceOver and TalkBack navigation;
- Metro requires DOM declarations or browser polyfills;
- SVG and Skia require divergent public chart definitions;
- native output depends on reparsing SVG strings;
- NativeScript requires project-local patches to its graphics runtime.
An engineer-week means one experienced library engineer working full time. Ranges include implementation, tests, examples, and documentation, but not unrelated chart features or store release work.
| Deliverable | Estimate | Confidence |
|---|---|---|
| Shared controller extraction, transition design, platform theme/typography contracts, and device harness | 2–4 engineer-weeks | Medium |
| React Native SVG proof productionization: gestures, complete tooltips, packaging, and release gates | 3–5 engineer-weeks | Medium |
| React Native full applicable parity: text, themes, keyed motion, gestures, tooltips, accessibility, export, API migration, 100-case hardening, Expo/bare CI | 5–8 engineer-weeks | Low–medium |
| Production React Native SVG package total | 10–16 engineer-weeks | Low–medium |
| Optional Skia execution/optimization of the shared motion contract, snapshots, and crossover benchmarks | +4–7 engineer-weeks | Low–medium |
| NativeScript Core Canvas package after shared refactor | +6–10 engineer-weeks | Low |
| Portable scene DTO plus one Compose or Flutter rendering proof | +3–6 engineer-weeks | Low |
| Production Compose, Flutter, or Swift implementation | Separate multi-month product per ecosystem | Low until the DTO proof |
Directional implementation size is 5–8k production lines plus 3–6k lines of tests and fixtures for first-class React Native parity. A NativeScript framework/view wrapper could remain near 1k lines only after a portable painter and backend exist. The renderer/backend itself is likely another 2–4k lines; device testing, accessibility feasibility, performance work, and hardening explain the 6–10 week estimate.
Two experienced engineers could overlap core/controller work, React Native rendering, and device harness work. A realistic calendar range for a production React Native package with both SVG and Skia is roughly 8–12 weeks, assuming the text and accessibility proofs pass early. NativeScript should not be scheduled as a committed package until its Canvas proof passes.
Ongoing maintenance is material:
- React Native, Expo, Metro,
react-native-svg, Skia, Reanimated, and Gesture Handler compatibility; - physical-device screenshot, performance, VoiceOver, and TalkBack coverage;
- renderer-specific bug triage;
- font and graphics differences across iOS and Android;
- conformance updates whenever the scene grammar grows.
Fund a two-week, two-engineer device and shared-behavior proof before treating the experimental package as supported.
Engineer A:
- extract the shared interaction and tooltip controller;
- define platform theme and typography ownership;
- replace the responder proof with scroll-safe gesture arbitration;
- exercise representative definitions under Hermes and on devices.
Engineer B:
- build the iOS/Android visual, performance, and binary-size harness;
- define semantic output and VoiceOver/TalkBack flows;
- test the existing SVG host across density and gesture sentinels;
- scope the later Canvas/NativeScript proof from measured bottlenecks.
At the end of the proof, decide from evidence:
- whether
react-native-svgis a viable default; - whether Skia is required for the first release or can follow;
- whether the typography contract is stable enough for public API;
- whether NativeScript meets the bar without runtime patches;
- whether to stabilize a portable scene DTO for non-JavaScript targets.
- No DOM emulation layer on native.
- No WebView marketed as native support.
- No D3 or arbitrary user callbacks in UI-thread worklets.
- No Skia dependency in the base package.
- No point-count threshold before device benchmarks.
- No thousands of invisible accessible views.
- No pixel-perfect cross-engine promise.
- No claim that a JSON subset preserves the complete live TypeScript API.
- No custom Fabric, UIKit, or Android renderer before the shared renderers are measured.