Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/bootstrap-release-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Bootstrap missing npm package

on:
workflow_dispatch:
inputs:
package:
description: Exact missing fixed-set package@version to publish
required: true
type: string

permissions:
contents: read

concurrency:
group: charts-npm-bootstrap
cancel-in-progress: false

jobs:
bootstrap:
name: Bootstrap npm package
if: github.repository == 'TanStack/charts' && github.event_name == 'workflow_dispatch' && github.ref_type == 'branch' && github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
timeout-minutes: 20
environment: npm-bootstrap
permissions:
contents: read
id-token: write

steps:
- name: Checkout exact main revision
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.sha }}

- name: Setup
uses: ./.github/actions/setup

- name: Build and identify the sole missing package
id: prepare
env:
BOOTSTRAP_PACKAGE_SPEC: ${{ inputs.package }}
RELEASE_REVISION: ${{ github.sha }}
run: node scripts/bootstrap-release-package.mjs prepare

- name: Publish the confirmed package
if: steps.prepare.outputs.publish_needed == 'true'
env:
BOOTSTRAP_PACKAGE_SPEC: ${{ inputs.package }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_BOOTSTRAP_TOKEN }}
RELEASE_REVISION: ${{ github.sha }}
run: node scripts/bootstrap-release-package.mjs publish
52 changes: 42 additions & 10 deletions API-FRICTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ Each entry records:
| F-172 | Metro skipped the fixture-owned Babel runtime | Tooling | resolved |
| F-173 | Metro retained the complete universal barrel | API/Tooling | monitoring |
| F-174 | OIDC release cannot claim a new npm package name | Tooling | monitoring |
| F-175 | Native SVG resource normalization collapsed authored IDs | Application | resolved |

## Findings

Expand Down Expand Up @@ -4065,6 +4066,10 @@ Each entry records:
overlays, tooltips, and external callbacks attached to old point objects
after responsive geometry changed. Callback prop identity could also
retrigger restoration and incorrectly change the focus source.
An equivalent definition authored inline could rebuild the same scene after
a parent callback update; restoration then treated the new point objects as
another focus change, called the parent again, and could sustain an update
loop.
Restacking onto the visual grouped-tooltip ordering change then exposed the
same drift again: the DOM host defaulted to visual order while the native
copy still used color-domain order.
Expand All @@ -4084,8 +4089,9 @@ Each entry records:
overlay; authored focus marks and inline mark states remain unsupported until
scene-state resolution is shared. It now preserves the original primary and
focus group independently from sorted tooltip rows, refreshes restored point
objects and callbacks, and keeps callback refs out of restoration effect
dependencies.
objects, re-emits callbacks when public point values or geometry change,
silently refreshes equivalent point references, and keeps callback refs out
of restoration effect dependencies.
- Verification: focused native tests cover strategy selection, grouping,
restoration, navigation, axis and custom anchors, supported default content,
placement, and extension ownership. The native type and Metro gates use the
Expand All @@ -4096,8 +4102,10 @@ Each entry records:
regression verifies inactive focus-layer paint is absent from the native
scene. Component regressions cover a primary point that sorts after another
series, restored coordinates and callbacks after resize, and stable focus
source when only callback props change. A grouped-tooltip regression now
verifies visual default order and explicit color-domain order in both hosts.
source when only callback props change. An inline-definition regression
verifies an equivalent restored scene does not re-emit focus into the
parent. A grouped-tooltip regression now verifies visual default order and
explicit color-domain order in both hosts.

### F-169 — CSS theme defaults reach the native scene compiler

Expand Down Expand Up @@ -4196,15 +4204,21 @@ Each entry records:
result included unused marks, data-transform families, and the
environment-neutral static SVG string serializer. Against the same granular
full-chart fixture, `/universal` added 119.06 KiB minified and 28.91 KiB gzip
on both iOS and Android, plus 102 modules per platform.
on both iOS and Android, plus 102 modules per platform. The esbuild boundary
policy also classified that environment-neutral serializer as browser-only,
contradicting the Metro contract whenever a native fixture retained it.
- Current decision: keep `/universal` as the ergonomic cross-runtime authoring
entry and make the full-chart Metro proof exercise it. Keep the native host's
own imports granular, publish granular entries as the bundle-sensitive path,
and do not describe the broad barrel as cost-equivalent under Metro.
and do not describe the broad barrel as cost-equivalent under Metro. Keep the
static serializer in the SVG capability group, but remove it from the
browser-only rejection group.
- Verification: the iOS and Android full-chart bundles require
`packages/charts-core/src/universal.ts`, measure 103.00 and 103.05 KiB gzip over
blank respectively, and exclude DOM hosts, browser adapters, Canvas,
reconciliation, SVG resources/surface, web tooltip code, and `react-dom`.
A native-plus-universal boundary fixture retains both the native host and the
static serializer at 11.37 KiB gzip and passes the browser-module rejection.

### F-174 — OIDC release cannot claim a new npm package name

Expand All @@ -4217,11 +4231,29 @@ Each entry records:
an existing package's settings, so the normal tokenless workflow cannot be
authorized for this package before its registry entry exists.
- Current decision: keep the package in release artifacts and the fixed
changeset, but require a maintainer-controlled direct public publish of the
checked `0.4.0` tarball. Configure the repository's release workflow as the
trusted publisher immediately afterward; the aggregate changeset can then
publish `0.5.0` through OIDC.
changeset. Bootstrap the sole missing fixed-set package from a dedicated,
protected GitHub-hosted workflow using a short-lived granular token and npm
provenance. The workflow builds and validates release artifacts, publishes
only the missing tarball, and verifies its registry integrity and
attestations. Configure `release.yml` as the trusted publisher immediately
afterward; the aggregate changeset can then publish `0.5.0` through OIDC.
- Verification: `npm view @tanstack/react-native-charts` currently returns
`E404`. Close this entry only after the public package exists, its trusted
publisher names the repository release workflow, and an aggregate release
publishes it without a long-lived write token.

### F-175 — Native SVG resource normalization collapsed authored IDs

- Status: resolved
- Severity: medium
- Owner: Application
- Observed in: pre-publication review of custom native scene gradients
- Friction: the native SVG host removed every character outside an allowlist
from authored gradient IDs. Distinct public IDs such as `a.b`, `a:b`, and
`ab` therefore addressed the same native SVG resource.
- Decision: preserve letters, digits, and hyphens, and encode every other code
point with an unambiguous SVG-safe escape. The escape marker itself is
encoded, so an authored string cannot collide with an encoded character.
- Verification: the native scene regression renders the formerly colliding
IDs plus empty and delimiter-containing IDs, and checks matching definition
IDs and paint references.
2 changes: 2 additions & 0 deletions benchmarks/entries/charts-react-native-universal-boundary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { renderChartSvg } from '@tanstack/charts/universal'
export { Chart } from '@tanstack/react-native-charts'
6 changes: 3 additions & 3 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Then add one adapter if the application needs it:
pnpm add @tanstack/react-charts react react-dom

# React Native
pnpm add @tanstack/react-native-charts react react-native react-native-svg
pnpm add @tanstack/react-native-charts react@^19.2.3 react-native@^0.86.0 react-native-svg@^15.15.4

# Preact
pnpm add @tanstack/preact-charts preact
Expand Down Expand Up @@ -80,7 +80,7 @@ pnpm exec expo install react-native-svg
Bare React Native 0.86 applications install the renderer directly:

```sh
pnpm add @tanstack/charts @tanstack/react-native-charts d3-scale react-native-svg
pnpm add @tanstack/charts @tanstack/react-native-charts d3-scale react-native-svg@^15.15.4
```

Run `bundle exec pod install` from `ios/` after adding it to a bare iOS
Expand All @@ -97,7 +97,7 @@ Packed tarballs are typechecked and bundled through default bare React Native
and Expo Metro configurations on iOS and Android. The workspace Expo 57
fixture also renders in Expo Go on an iOS simulator. Bare-native and Android
simulators, physical devices, gestures, visual parity, and screen readers are
not yet support claims.
not currently supported.

## Install the D3 modules you import

Expand Down
6 changes: 3 additions & 3 deletions packages/charts-core/docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Then add one adapter if the application needs it:
pnpm add @tanstack/react-charts react react-dom

# React Native
pnpm add @tanstack/react-native-charts react react-native react-native-svg
pnpm add @tanstack/react-native-charts react@^19.2.3 react-native@^0.86.0 react-native-svg@^15.15.4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Update the source documentation instead of this generated copy.

These changes are in packages/charts-core/docs/installation.md. Update docs/installation.md, then run pnpm docs:sync to regenerate this file. Direct edits can be overwritten and can leave the published documentation out of sync.

As per coding guidelines, author public documentation only in the root docs/ tree; do not directly edit packages/charts-core/docs because those copies are generated by pnpm docs:sync.

Also applies to: 83-83, 100-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/charts-core/docs/installation.md` at line 21, Update the
installation commands in the source documentation under the root docs/ tree
rather than the generated packages/charts-core/docs copy, then run pnpm
docs:sync to regenerate all affected documentation copies.

Source: Coding guidelines


# Preact
pnpm add @tanstack/preact-charts preact
Expand Down Expand Up @@ -80,7 +80,7 @@ pnpm exec expo install react-native-svg
Bare React Native 0.86 applications install the renderer directly:

```sh
pnpm add @tanstack/charts @tanstack/react-native-charts d3-scale react-native-svg
pnpm add @tanstack/charts @tanstack/react-native-charts d3-scale react-native-svg@^15.15.4
```

Run `bundle exec pod install` from `ios/` after adding it to a bare iOS
Expand All @@ -97,7 +97,7 @@ Packed tarballs are typechecked and bundled through default bare React Native
and Expo Metro configurations on iOS and Android. The workspace Expo 57
fixture also renders in Expo Go on an iOS simulator. Bare-native and Android
simulators, physical devices, gestures, visual parity, and screen readers are
not yet support claims.
not currently supported.

## Install the D3 modules you import

Expand Down
8 changes: 4 additions & 4 deletions packages/react-native-charts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ npx expo install react-native-svg
Bare React Native applications can install it directly:

```sh
npm install react-native-svg
npm install react-native-svg@^15.15.4
```

Run `bundle exec pod install` from `ios/` after adding it to a bare iOS
Expand Down Expand Up @@ -51,8 +51,8 @@ export function RevenueChart() {
}
```

The package is tested with Expo 57 / React Native 0.86 and
`react-native-svg` 15.15.x. The workspace fixture also renders in Expo Go on an
iOS simulator. It remains experimental: bare-native and Android simulators,
The bare fixture uses React Native 0.86.2 with `react-native-svg` 15.15.5. The
Expo 57 fixture uses `react-native-svg` 15.15.4 and renders in Expo Go on an iOS
simulator. It remains experimental: bare-native and Android simulators,
physical devices, gestures, accessibility, release builds, and performance
still need validation.
42 changes: 42 additions & 0 deletions packages/react-native-charts/src/Chart.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,48 @@ describe('React Native Chart', () => {
}
})

it('does not re-emit focus for an equivalent inline definition', async () => {
const container = document.createElement('div')
const root = createRoot(container)
let focusEvents = 0

function InlineDefinitionChart() {
const [, rerender] = React.useReducer((value) => value + 1, 0)
const inlineDefinition = defineChart({
marks: [lineY(data, { x: 'month', y: 'value' })],
x: { scale: scaleLinear().domain([1, 2]) },
y: { scale: scaleLinear().domain([8, 12]) },
})
return (
<Chart
definition={inlineDefinition}
accessibilityLabel="Revenue"
width={480}
height={260}
onFocusChange={(point) => {
if (!point) return
focusEvents += 1
if (focusEvents < 3) rerender()
}}
/>
)
}

try {
await React.act(() => root.render(<InlineDefinitionChart />))
const chart = container.firstElementChild
if (!chart) throw new Error('Expected the native chart root to render.')

await React.act(() => {
chart.dispatchEvent(new FocusEvent('focusin', { bubbles: true }))
})

expect(focusEvents).toBe(1)
} finally {
await React.act(() => root.unmount())
}
})

it('rejects browser tooltip portal extensions', () => {
const portalDefinition = defineChart({
marks: [lineY(data, { x: 'month', y: 'value' })],
Expand Down
54 changes: 53 additions & 1 deletion packages/react-native-charts/src/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { NativeChartFocusOverlay } from './FocusOverlay'
import {
adjacentFocusPoint,
createNativeChartFocusModel,
samePointIdentity,
samePointReferences,
} from './interaction'
import { resolveNativePaint, type NativePaintResolver } from './paint'
Expand Down Expand Up @@ -174,8 +175,17 @@ export function Chart<
if (!focusModel || !previous) return
const restored = focusModel.restore(previous)
if (restored) {
const next = focusModel.group(restored)
const current = focusedPointsRef.current
if (sameFocusedPointValues(next, current)) {
if (!samePointReferences(next, current)) {
focusedPointsRef.current = next
setFocusedPoints(next)
}
return
}
setFocusSource('restored')
commitFocus(focusModel.group(restored))
commitFocus(next)
} else {
setPinnedKey(null)
commitFocus([])
Expand Down Expand Up @@ -364,6 +374,48 @@ export function Chart<
)
}

function sameFocusedPointValues<
TDatum,
TXValue extends ChartValue,
TYValue extends ChartValue,
>(
left: readonly ChartPoint<TDatum, TXValue, TYValue>[],
right: readonly ChartPoint<TDatum, TXValue, TYValue>[],
) {
return (
left.length === right.length &&
left.every((point, index) => {
const current = right[index]
return (
current !== undefined &&
samePointIdentity(point, current) &&
Object.is(point.group, current.group) &&
point.groupLabel === current.groupLabel &&
sameChartValue(point.xValue, current.xValue) &&
sameChartValue(point.yValue, current.yValue) &&
sameChartValue(point.x1Value, current.x1Value) &&
sameChartValue(point.x2Value, current.x2Value) &&
sameChartValue(point.y1Value, current.y1Value) &&
sameChartValue(point.y2Value, current.y2Value) &&
point.xInterval === current.xInterval &&
point.yInterval === current.yInterval &&
Object.is(point.x, current.x) &&
Object.is(point.y, current.y) &&
point.color === current.color
)
})
)
}

function sameChartValue(
left: ChartValue | undefined,
right: ChartValue | undefined,
) {
return left instanceof Date && right instanceof Date
? left.getTime() === right.getTime()
: Object.is(left, right)
}

function resolveNativeTooltipInput<
TDatum,
TXValue extends ChartValue,
Expand Down
Loading