Skip to content
Native Animation
Radar Profile Morph animation preview

This preview uses fictional demo content. See the props below for the customization options available in this component.

npx nativeanimation add radar-profile-morph
Skia

Radar Profile Morph

A radar chart with morphing profile vertices and an inspection bubble, with configurable axes, profiles, palette, and fonts.

$9 for this animation

Add to your project

Copy the component and its assets into your app with the Native Animation CLI, or paste the agent prompt into Claude Code, Codex or Cursor once the MCP server is connected.

About this animation

A radar chart with morphing profile vertices and an inspection bubble, with configurable axes, profiles, palette, and fonts.

Why this one

Each dimension has its own explicit minimum, maximum, and formatter. Five rings represent 20%, 40%, 60%, 80%, and 100% of that dimension's range. Named axis buttons expose both profile values to screen readers and open a value bubble below the chart. The chart compares actual data rather than deforming a decorative shape. A mint inspection beam and an editorial wellness layout give the demo its own identity.

How it works

  1. Validate three to eight axes and one or two complete profiles before plotting anything.
  2. Normalize each value using its axis range. All rings share normalized fractions, not raw units.
  3. Animate each normalized vertex with an interruptible spring (damping 30, stiffness 300, mass 1), staggered by 40 ms. The polygon and its points read the same live vertex values. Pending stagger delays are canceled when a new profile arrives, so rapid changes start from the live pose. Color blends over 350 ms; the chart enters from −8° to 0° over 950 ms.
  4. Trace around the chart to select an axis, or tap its named icon button. Axis buttons sit outside the chart gesture detector so native taps do not compete with the pan recognizer. Scores never change during a gesture.
  5. Run a 240 ms beam along the selected axis, then reveal its signed difference over 180 ms. The difference is active minus comparison, in original units.
  6. Keep autoplay in the demo: switch profiles at 1100/1800 ms, inspect Focus at 2450 ms, reset at 4200 ms, repeat at 5000 ms. Touch cancels all timers.

Install

npx expo install @shopify/react-native-skia react-native-gesture-handler react-native-reanimated react-native-worklets

Use a GestureHandlerRootView at the app root. Configure Reanimated/worklets for the host SDK. On web, initialize CanvasKit before mounting this component, following Expo's Skia setup. No images or network requests are needed. Demo-only expo-linear-gradient is not required by the component.

Usage

const axes = [
  { id: 'rest', label: 'Rest', min: 0, max: 100 },
  { id: 'focus', label: 'Focus', min: 0, max: 10 },
  { id: 'movement', label: 'Movement', min: 0, max: 60 },
];
const profiles = [
  { id: 'now', label: 'This week', values: [78, 8, 42] },
  { id: 'before', label: 'Last week', values: [64, 6, 36] },
];
const labels = {
  title: 'Weekly balance', subtitle: 'Compare your dimensions',
  hint: 'Tap or trace to inspect', table: 'Values by dimension',
  scale: 'Axis range', invalidData: 'Check your axes and profile data.',
  solid: '●', dashed: '┄', difference: '{value} units',
  selectAxis: 'Choose a dimension', profileChanged: 'Showing {label}',
};
<RadarProfileMorph
  axes={axes} profiles={profiles} labels={labels}
  formatValue={(value, id) => `${value}${id === 'movement' ? ' min' : ''}`}
  onAxisSelect={(id) => setInspectedAxis(id)}
  accessibilityLabel="Weekly balance comparison"
/>

For controlled use, pass selectedProfileId={profileId} and onProfileSelect={setProfileId}. Add ref={radar}, colorScheme="light", palette={customPalette}, font props, and style={{ flex: 1 }} as needed. radar.current?.selectAxis('focus'), .selectProfile('now'), and .reset() drive the same behavior as the controls. Controlled requests invoke the callback once and wait for the host to update the prop. Unknown method IDs are ignored.

Props

PropTypeDefaultPurpose
axesreadonly RadarAxis[]requiredUnique IDs, labels, finite min/max with max > min
profilesreadonly RadarProfile[]requiredOne or two unique IDs, labels, complete value arrays
selectedProfileIdstringuncontrolled first profileControlled active profile
onProfileSelect(id: string) => voidundefinedValid changed selection request
onAxisSelect(id: string) => voidrequiredChanged axis selection
formatValue(value, axisId) => stringrequiredValues, scale endpoints, signed differences; supply units here
labelsRadarLabelsrequiredAll structural copy; fields shown in the example
colorSchemelight / dark / autoautoFull theme follows system when automatic
paletteRadarPalettethemebackground, surface, text, muted, border, accent, success, error
fontFamilystringsystemBody font
titleFontFamilystringbody fontHeading font
monoFontFamilystringsystemValues and scale font
onInteraction() => voidundefinedReal touch input, for canceling host autoplay
styleStyleProp<ViewStyle>undefinedFlexible root, no safe-area insets
testIDstringradar-profile-morphRoot; suffixes -chart, -profile-ID, -axis-ID
accessibilityLabelstringrequiredChart's spoken name

All labels fields are required: title, subtitle, hint, table, scale, invalidData, solid, dashed, difference, selectAxis, profileChanged. difference replaces {value}; profileChanged replaces {label}. Legend patterns supplement color. No internal demo brand or copy defaults.

Empty data, unknown controlled IDs, duplicate IDs, missing dimensions, invalid ranges, nonfinite values, and values outside the declared range show labels.invalidData as an accessible alert. No missing dimension is silently replaced with zero. Formatting callbacks must return strings and should not throw. When profile data changes, keep uncontrolled IDs stable or use controlled selection.

Compatibility

Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Worklets 0.13.0, and Skia 2.14.0. Expo Go includes the installed stack.

Icons: Lucide (ISC), copied from the project's kit. Tidewell is the demo brand; the droplet uses a palette-matched tile. No reference assets are shipped.

Accessibility

The root measures its own slot. The radar canvas fills the available width up to 460 pt. The outer ring spans 88% of the canvas (over 85% of the component width on standard phones). Short slots scroll the complete flow instead of shrinking the radar into a thumbnail. Content is capped at 480 pt. The demo fits standard portrait screens without scrolling; short embedded slots, landscape, and enlarged text can use the single outer ScrollView with its indicator hidden. The component applies no safe area. The host owns outer screen insets. Profile buttons and named axis targets are at least 48 pt high. The inspection bubble occupies its own layout space below the chart, so it never covers the data.

Reduced motion sets vertex geometry and colors immediately, removes entry rotation, traveling glow, and press scale, and fades the inspection detail. Screen readers have named axis buttons with both profile scores, selected states, and selection announcements. All public props and ref methods are preserved; labels.table remains accepted for compatibility. The built-in mint/violet theme has a complete light counterpart; a custom palette uses its muted color as the secondary gradient and comparison color.