npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gnome-ui/react-native-charts

v1.1.0

Published

Data visualisation components for React Native, styled with Adwaita design tokens, rendered on Skia via Victory Native

Readme

@gnome-ui/react-native-charts

Data visualisation components for @gnome-ui/react-native, styled with GNOME Adwaita design tokens and rendered on Skia via Victory Native.

npm CI License: MIT

Status: all 23 components shipped. LineChart, BarChart, AreaChart, PieChart, RadarChart, RadialBarChart, CloudChart, SparkLineChart, SparkAreaChart, SparkBarChart, ScatterChart, FunnelChart, ComposedChart, GaugeChart, TreeMap, SankeyChart, BulletChart, WaterfallChart, Heatmap, SparkGaugeChart, SparkPieChart, SparkBulletChart, and BoxPlot — this package now fully mirrors @gnome-ui/charts's 23-component roadmap for React Native, one chart at a time, on top of Victory Native (Skia + Reanimated) rather than Recharts (SVG-over-DOM), since RN has no DOM/SVG renderer to reuse directly. See ROADMAP.md for the full per-component history.

Installation

npm install @gnome-ui/react-native-charts victory-native @shopify/react-native-skia react-native-reanimated react-native-gesture-handler react-native-worklets

Or, in an Expo app, let expo install resolve SDK-compatible versions of the native peers:

npx expo install victory-native @shopify/react-native-skia react-native-reanimated react-native-gesture-handler react-native-worklets
npm install @gnome-ui/react-native-charts

Requires @gnome-ui/react-native (for theme tokens and useNumberFormatter) and react ≥ 19 as peer dependencies.

Setup

  1. Wrap your app's root in GestureHandlerRootView — Victory Native's CartesianChart uses react-native-gesture-handler internally, even for a chart with no pan/zoom interaction:

    import { GestureHandlerRootView } from 'react-native-gesture-handler';
    
    export default function App() {
      return (
        <GestureHandlerRootView style={{ flex: 1 }}>
          {/* ... */}
        </GestureHandlerRootView>
      );
    }
  2. In an Expo app on SDK 50+, babel-preset-expo auto-detects react-native-reanimated / react-native-worklets in node_modules and wires their Babel plugin for you — no babel.config.js changes needed. Outside Expo, add react-native-worklets/plugin to your Babel config's plugins (must be listed last).

Components

| Component | Description | |-----------|-------------| | LineChart | Multi-series line chart with dots, grid, axis labels, and legend | | BarChart | Grouped/clustered bar chart for categorical comparisons | | AreaChart | Filled area chart — flat tint or gradient fill, overlapping or stacked series | | PieChart | Pie or donut chart with optional in-slice labels and legend | | RadarChart | Spider/radar chart for multi-attribute comparisons across subjects | | RadialBarChart | Concentric arc bars for multiple circular progress metrics | | CloudChart | Word/tag cloud with value-proportional font sizing | | SparkLineChart | Minimal inline line sparkline for embedding in cards and tables | | SparkAreaChart | Minimal inline area sparkline with optional gradient fill | | SparkBarChart | Minimal inline bar sparkline for compact trend display | | ScatterChart | Scatter/bubble chart for correlation between two numeric variables; zKey encodes a third dimension as bubble size | | FunnelChart | Funnel visualization for conversion rates and sales pipelines | | ComposedChart | Mixed bar/line/area series sharing one x-axis | | GaugeChart | Radial gauge for a single value against a min/max range, with optional color thresholds | | TreeMap | Proportional-area rectangles for hierarchical/part-of-whole data, laid out with a squarified treemap algorithm | | SankeyChart | Flow diagram for multi-stage funnels/allocations, laid out with a d3-sankey-style algorithm | | BulletChart | Compact single-measure KPI indicator — qualitative range bands, a performance bar, and an optional target tick | | WaterfallChart | Bridge chart showing how a sequence of increases/decreases moves a value from a starting point to an ending point | | Heatmap | Grid of colored cells for a value across two categorical dimensions, with intensity-based coloring and an optional legend | | SparkGaugeChart | Minimal inline circular progress ring for a single value against a min/max range, with optional color thresholds | | SparkPieChart | Minimal inline pie or donut chart for a small breakdown of values | | SparkBulletChart | Minimal inline bullet-chart track for a single measure against qualitative bands and an optional target | | BoxPlot | Box-and-whisker plot for comparing distributions across groups — median, interquartile range, whiskers, and outliers |

Usage

import { LineChart } from '@gnome-ui/react-native-charts';

<LineChart
  data={[
    { day: 'Mon', cpu: 42, memory: 68 },
    { day: 'Tue', cpu: 58, memory: 72 },
  ]}
  series={[
    { dataKey: 'cpu', name: 'CPU %' },
    { dataKey: 'memory', name: 'Memory %' },
  ]}
  xAxisKey="day"
  showLegend
/>;

See src/components/LineChart/README.md, src/components/BarChart/README.md, src/components/AreaChart/README.md, src/components/PieChart/README.md, src/components/RadarChart/README.md, src/components/RadialBarChart/README.md, src/components/CloudChart/README.md, src/components/SparkLineChart/README.md, src/components/SparkAreaChart/README.md, src/components/SparkBarChart/README.md, src/components/ScatterChart/README.md, src/components/FunnelChart/README.md, src/components/ComposedChart/README.md, src/components/GaugeChart/README.md, src/components/TreeMap/README.md, src/components/SankeyChart/README.md, src/components/BulletChart/README.md, src/components/WaterfallChart/README.md, src/components/Heatmap/README.md, src/components/SparkGaugeChart/README.md, src/components/SparkPieChart/README.md, src/components/SparkBulletChart/README.md, and src/components/BoxPlot/README.md for the full prop reference of each.

Design notes

  • Shared chart internals live in src/internal/ (ChartContainer, ChartLegend, chartKeys, useChartFont) — extracted once BarChart became a second consumer of the exact same axis-font/ palette/legend/generic-typing code LineChart already had. Not part of the public API; every new chart should compose these rather than re-deriving them.

  • Series colors default to the GNOME Adwaita chart palette (blue3, green4, orange3, purple3, red3, yellow5), read from the active GnomeProvider theme so they track color scheme and high-contrast switches — see getChartPalette.

  • Axis/tick labels use Skia's matchFont({ fontSize: 12 }) with the default "System" family, not theme.fontFamily ("Adwaita Sans") — confirmed on-device that Skia's font matcher renders no glyphs at all for a family name that isn't actually registered on the device, unlike RN's own <Text>, which silently falls back to the OS default font for an unknown fontFamily.

  • No interactive (tap/press) tooltip yet. Victory Native's useChartPressState requires the series' data keys to be known as TypeScript literal types at the call site, which doesn't fit this package's (and @gnome-ui/charts') deliberately dynamic series: { dataKey: string }[] prop shape without a larger API redesign. Revisit once a second chart's needs clarify the right shape for it.

  • AreaChart's stacked mode passes an explicit domain={{ y: [0, stackedMax] }} to CartesianChart — its own auto-domain is the extent of each series' raw values, not aware a stacked chart sums them, so without this the topmost stacked band renders visibly flat-clipped at the un-stacked ceiling (confirmed on-device). Gradient fills use a withAlpha() helper (src/internal/colorAlpha.ts) to turn a palette hex color into a #RRGGBBAA fade-to-transparent pair for Skia's <LinearGradient> — the same alpha-suffix trick Chip/Highlight use in @gnome-ui/react-native.

  • PieChart is polar, not Cartesian — it composes PolarChart/Pie.Chart instead of CartesianChart, has no axes/grid, and its data prop is a flat { label, value, color? }[] rather than the data+series split the other charts use. Slice labels render inside each slice (Victory Native's Pie.Label) rather than the web version's external leader-line labels — there's no leader-line primitive to build on, and a label-position swap is a reasonable platform-idiom adaptation, not a data/behavior change.

  • RadarChart has no Victory Native primitive at all and is hand-built directly on @shopify/react-native-skia's own low-level primitives (Canvas, Path, Line, Text, Skia.PathBuilder) — flagged to the user before starting rather than assumed, since it's a real architecture decision (same category as the original Victory Native choice for this whole package). Manages its own Canvas sizing via a plain View's onLayout, since there's no CartesianChart/PolarChart wrapper to do it. Uses the modern Skia.PathBuilder.Make() API, not the older mutable Skia.Path.Make() (deprecated in this Skia version — confirmed via a real runtime warning, not assumed from docs).

  • RadialBarChart also has no Victory Native primitive — same situation as RadarChart, confirmed again with the user before starting rather than assumed. Hand-built on SkPathBuilder.addArc (a thick stroked arc per ring, not a filled annular sector). A real sweep-direction bug shipped and was caught by the on-device screenshot, not by any automated check — Skia's addArc treats positive sweep as clockwise in its y-down coordinate space, so a negative sweep from startAngle: 180 traced the bottom half instead of the intended top-half gauge, rendering as a barely-visible sliver clipped against the canvas edge. Any future arc-based chart in this package should double-check sweep direction algebraically against Skia's own doc comment before trusting a screenshot alone to catch a sign error like this.

  • CloudChart needed no Skia canvas at all, unlike RadarChart/RadialBarChart — its web source has no real word-cloud packing algorithm, just flex-wrapped <span>s with font-size scaled linearly by value, laid out by the browser's own text flow. Ports directly to a plain flexWrap: 'wrap' RN View of Texts. No hover-only hint (web's :hover { opacity: 0.7 }, no touch equivalent and no onPress in the source either).

  • SparkLineChart still reuses CartesianChart (unlike CloudChart) purely for its scaling math, with every decorative axis/grid/frame part hidden (axisOptions={{ lineColor: 'transparent' }}, no font). It fully controls its own normalized data shape internally (always a plain Record<string, number> via a synthetic __x index field), so it passes explicit generic type arguments to CartesianChart instead of needing the InputKeys/NumericalKeys replica pattern the bigger, consumer-shape-generic charts use. The web's hover-triggered highlighted mode is dropped — a sparkline's usual embedding size (e.g. inside a table cell) has no natural touch affordance, unlike this whole port's usual hover-to-long-press swap. resolveSparkSeries/ normalizeSparkData/sparkAccessibilityProps (src/internal/sparkTypes.ts) are shared by the whole spark-chart family, extracted once SparkAreaChart became a second consumer — same "second occurrence" call as the bigger charts' own src/internal/ split.

  • SparkAreaChart layers Area (fill) + a separate Line (stroke), same as AreaChart — Victory Native's Area hardcodes style: "fill" and its props don't accept a style/ strokeWidth override at all (confirmed via a real tsc error, not assumed), so a second <Area style="stroke"> doesn't typecheck.

  • SparkBarChart has no multi-series mode at all (single dataKey+color only), matching the web version exactly — unlike its two spark siblings. Uses Victory Native's standalone Bar, not BarGroup/BarGroup.Bar (which BarChart needs for grouping multiple series side by side), since there's only ever one series here.

  • ScatterChart is the only chart here where each series owns its own independent point list (its own xKey/yKey/zKey field names), not rows shared across series — every series' points are merged into one combined row array before handing it to CartesianChart, with each row only populating its own series' namespaced y (and z) field; Scatter itself skips any point whose y isn't a number, so other series' rows render as gaps for free. CartesianChart sorts its data by xKey internally (confirmed from transformInputData's source) — any per-point data matched back in after the fact (this chart's bubble-size zKey lookup) must match by each point's own raw xValue/yValue, never by array index into the array you originally built, since the sort reorders points relative to your own insertion order. xLabel/yLabel from the web version are dropped — they only ever fed Recharts' tooltip text, which this package has none of yet.

  • FunnelChart has no Victory Native primitive either, same category as RadarChart/ RadialBarChart, and is hand-built directly on Skia.PathBuilder — one closed trapezoid Path per segment (moveTo/lineTo around the four corners, .close()), width tapering linearly by each item's share of the largest value. Uses ChartContainer with legend={null} since the web source renders no legend for this chart. Shipped with zero bugs on the first on-device screenshot — the first hand-rolled-geometry chart in this package to do so, credited to already having the Skia.PathBuilder.Make() API and Skia's angle/coordinate conventions worked out from the two prior hand-rolled charts before writing a line of this one.

  • ComposedChart is the first chart mixing multiple render primitives (bar/line/area) within one series array — every prior chart's series all shared one rendering primitive. series is split by type and rendered as: all bar-type series together inside one BarGroup (it needs every bar as a direct child to compute width/offset), then area-type (Area fill + a separate Line stroke, same pattern AreaChart uses), then line-type last — a deliberate, documented deviation from the array's own literal order, since bars can't be interleaved with other types without breaking BarGroup's internal width math. The first on-device screenshot surfaced a demo-data mistake, not a component bug: a growth field (values 8–30) sharing one y-axis with revenue (4000–5600) rendered as an invisible hairline — CartesianChart shares one y-domain across every yKey, with no per-series secondary axis, the same limitation the web ComposedChart has with its own single YAxis. Any future composed-chart data needs every series kept within the same order of magnitude, or a series will visually vanish.

  • GaugeChart is a single-ring simplification of RadialBarChart's already-validated semicircle-gauge geometry — fourth chart in this package with no Victory Native primitive (same category as RadarChart/RadialBarChart/FunnelChart), reusing RadialBarChart's exact SkPathBuilder.addArc sweep-direction convention and strokeCap="round" track/value pairing rather than re-deriving them. Adds thresholds (ascending value/color status bands) and a centered value+caption drawn as Skia Text nodes directly on the Canvas, rather than an absolutely-positioned RN <Text> overlay. No RN theme token exists for the web's --gnome-dim-label-color CSS var (confirmed by grepping the generated theme — zero hits) — approximated the caption's dimmed look with theme.windowFgColor plus a Skia opacity={0.55} node prop instead of inventing a new theme token for one component's caption text. Shipped with zero bugs on the on-device screenshot, continuing the pattern FunnelChart started: once a hand-rolled Skia chart's underlying primitive (arc math, here) has been paid down by an earlier chart, a later chart reusing it in a simpler shape can reasonably ship clean.

  • TreeMap is the first chart needing an actual layout algorithm, not just a hand-rolled Skia primitive — flagged to the user before starting, since it's a different category of decision than "no Victory Native primitive" (already settled for RadarChart/RadialBarChart/ FunnelChart/GaugeChart). Ships a hand-rolled squarified treemap (Bruls/Huizing/van Wijk — the same algorithm Recharts' own Treemap uses internally) as pure squarify/layoutRow/ worstRatio number-only helpers, decoupled from Skia entirely; TreeMap itself only turns the resulting rects into RoundedRect/Text nodes. Sorts data descending by value before layout — a deliberate deviation from array order, since squarify's aspect ratios degrade noticeably on unsorted input. Reuses GaugeChart's opacity node-prop trick for the dimmed secondary value line. Real bug found and fixed via the on-device screenshot: the show-label gate only checked the tile's own width/height against a fixed size threshold, never whether the label's own measured text width actually fit inside that tile — a tall-but-narrow tile (an ordinary outcome of squarify packing several small values into one row) could pass the size gate while still being narrower than its own text, visibly bleeding the label into the neighboring tile. Fixed by measuring font.measureText(label).width before deciding to draw it, hiding the label entirely once it doesn't fit rather than letting it overflow.

  • SankeyChart is the second chart needing a real layout algorithm — flagged to the user before starting (same category as TreeMap, distinct from the "no Victory Native primitive" question already settled for the arc-based hand-rolled charts), who chose a full d3-sankey-style layout over a simplified no-relaxation version. Column assignment by longest-path depth (bounded relaxation over the DAG, not an explicit topological sort), node height proportional to throughput, then several passes alternating relaxRightToLeft/relaxLeftToRight — each pass pulls every node toward the weighted-center of the links tugging on it and resolves the resulting overlap — to straighten links and reduce crossings, the same technique d3-sankey (and the web version's underlying Recharts Sankey) uses. Deliberately skips d3-sankey's own link-crossing-minimizing sort at each node (links stack in the input links array's own order) as a smaller, documented scope trim distinct from the relaxation algorithm itself. Links are cubic-Bezier Paths colored by their source node via the withAlpha() helper (AreaChart/SparkAreaChart's translucent-fill trick), standing in for the web version's CSS color-mix (no Skia equivalent).

  • BulletChart needs no Canvas/Skia at all, the second chart here after CloudChart to skip it entirely — its web source is already just absolutely-positioned percentage-width <div>s in a flex row (qualitative bands, a performance bar, an optional target tick), which RN's own View percentage left/width/top/bottom styles port directly with no canvas or layout algorithm needed. Reuses GaugeChart's documented gap (no RN theme token for the web's --gnome-dim-label-color) for the dim target-value text, approximated with opacity: 0.55. Verification lesson, not a component bug: a demo where the performance bar's color matches one of its own ranges bands (a realistic case, e.g. a red "critical" zone with a red bar) looked broken in a screenshot — the bar's thin (40%-height-inset) fill seemingly absent over the differently-colored earlier bands. It wasn't; sampling actual pixel RGB values at the bar's true vertical center (not the row's visual midpoint) confirmed correct rendering the whole time — a same-colored thin center stripe is genuinely hard for a human eye to resolve in a compressed screenshot preview. Sample real pixel values for any future thin/inset overlay verification against a same- or similar-colored background, rather than trusting a screenshot by eye alone.

  • WaterfallChart is the first chart in this package to compose Victory Native's StackedBar — a real primitive match found by reading its source (points/chartBounds/barOptions), not the "no Victory Native primitive, hand-roll on Skia" situation RadarChart/RadialBarChart/ FunnelChart/GaugeChart hit. Each bar is two stacked series per category (an invisible base and a colored value, the same floating-bar recipe the web version's <Bar dataKey="base" fill="transparent"> + <Bar dataKey="value"> pair uses), colored per-bar (not per-series) via barOptions's datumIndex closing back over the original row array — safe here because transformInputData only sorts by xKey when every value is numeric, and this chart's xKey is always a string label. An explicit domain={{ y: [yMin, yMax] }} covering both series' combined extent is required for the same reason AreaChart's stacked mode needs one: the auto-domain only look at each series' own raw values, with no idea the real bar top is their sum. Two real on-device bugs, neither in the component's own math: (1) Skia's color parser can't be assumed to support the "transparent" CSS keyword the way a browser does — used an explicit #00000000 hex value for the invisible base segment instead, matching this whole package's existing "prefer alpha-suffixed hex over named colors for Skia" convention. (2) Victory Native's categorical x-axis defaults to tickCount: 5 and silently downsamples ticks past that — with 6 bridge steps, it didn't crowd the 6th label, it dropped one category's label entirely (confirmed via downsampleTicks' even-spaced-index selection, not assumed). Harmless for a chart where an occasional skipped tick is fine (a long time series), but every step in a bridge is a named, meaningful data point — fixed with an explicit tickCount: { x: data.length, y: 5 } override so every bar always gets its own label, regardless of category count.

  • Heatmap needed no chart primitive at all, the third chart here after CloudChart/ BulletChart to skip both Victory Native and a hand-rolled Skia canvas — its web source is already just a CSS grid of plain <div>s with no chart-drawing involved, which RN's own View/ Text port directly as a row-of-rows layout instead of display: grid (RN has no grid layout mode at all). Cell color intensity reuses the withAlpha() helper (AreaChart/SparkAreaChart's translucent-fill trick) instead of the web's CSS color-mix. The only Skia usage in the whole component is a tiny Canvas for the optional legend ramp's gradient fill, since RN View styles have no linear-gradient equivalent — this package already depends on Skia for exactly that gradient-fill need elsewhere, so it added no new dependency. A genuine RN-vs-web layout gap: the web version gets its row-label column's width for free from CSS Grid's auto track sizing (as wide as the longest row label); RN has no such per-track auto-sizing at all. Solved by measuring every row label's width up front with Skia's synchronous font.measureText — the same technique TreeMap/GaugeChart already use for layout decisions in this package — and using the widest one as a fixed View width, rather than a two-pass onLayout measure-then-reflow. Real on-device bug in that measurement's first version: the row-label box was sized to exactly the measured text width and given its own paddingRight — RN's width is border-box, so the padding silently ate into the text's own available space, ellipsizing capital-letter-heavy labels first (Mon/Tue/Wed/ThuM…/T…, Fri only surviving by coincidence). A first, wrong hypothesis (a Skia-vs-RN font-metric mismatch from setting theme.fontFamily on the label Text) was tested and ruled out before finding the real cause — reverting that font change alone fixed nothing, which is what isolated the padding math as the actual bug. Fixed by adding the padding to the measured width instead of trusting the box to absorb it — a lesson for any future fixed-width View that also carries its own padding, in this package or otherwise. Reused GaugeChart's/BulletChart's documented gap (no RN theme token for the web's --gnome-dim-label-color) for row/column/legend label text, same opacity: 0.55 approximation.

  • SparkGaugeChart is a full-circle simplification of GaugeChart's already-validated semicircle-gauge geometry — same SkPathBuilder.addArc primitive, strokeCap="round", and 0deg=east/positive-sweep-is-clockwise angle convention GaugeChart/RadialBarChart already paid down on-device, just sweeping the full 360deg from the top (START_ANGLE = 270) instead of 180deg from the side. Unlike GaugeChart, it takes an explicit square size (matching the web version's own width={size} height={size}) instead of filling a container height, so it needs no onLayout/canvas-size state at all — geometry is fully known from props up front. Extracted GaugeChart's threshold-color-resolution algorithm (walk sorted thresholds, keep the last one <= value) into src/internal/resolveThresholdColor.ts on this, its second real consumer — same "extract on second occurrence" call this package's other src/internal/ helpers were built on. Track color reuses theme.light3 directly (a real token match for the web's --gnome-light-3 var, unlike the --gnome-dim-label-color gap several other charts hit — no approximation needed here). Shipped with zero bugs on the first on-device screenshot, the arc geometry and sweep direction already fully de-risked by GaugeChart/RadialBarChart before this component existed.

  • SparkPieChart reuses PieChart's exact PolarChart/Pie.Chart primitive, but had to solve a real gap PieChart never needed: the web version supports paddingAngle (a gap in degrees between slices), and Pie.Chart has no built-in equivalent at all. Victory Native does ship a trick for this (PieSliceAngularInset, a stroke drawn over each slice's radial edge in a given color) but it only looks like a gap when that color happens to match whatever surface the chart sits on — fragile for a small inline chart meant to be dropped into cards, list rows, or any arbitrary background. Used a different technique instead: interleave a synthetic, fully transparent "spacer" slice between every pair of real slices, with its own value solved algebraically (spacer = paddingAngle * total / (360 - gaps * paddingAngle)) so its rendered sweep comes out to exactly paddingAngle degrees once mixed into Pie.Chart's shared value total — a real, transparent gap rather than a background-color illusion. Reused WaterfallChart's own documented lesson (an explicit #00000000 hex, never the "transparent" CSS keyword, for anything Skia needs to render as invisible) for the spacer's color. Confirmed correct on-device: the "no padding" demo's slices visibly touch with no seam, while every other demo shows a real gap — shipped with zero bugs on the first on-device screenshot.

  • SparkBulletChart needed no new geometry at all — it's the "spark" (compact, no label, no value text) member of the bullet-chart family, and its web source is close enough to BulletChart's own (the same absolutely-positioned percentage-width bands/bar/target-tick recipe, just without the label/value text around it) that the actual bands/bar/target rendering moved into a new shared src/internal/BulletTrack.tsx on this, BulletChart's second real consumer — same "extract on second occurrence" call this package's other shared helpers were built on. BulletChart itself was refactored to compose BulletTrack rather than duplicating the logic, with no behavior change (its own test suite still passes unchanged). BulletTrack deliberately takes sizing (flex: 1 for BulletChart, sitting next to a label in a row; width: '100%' for SparkBulletChart, standing alone) as a caller-supplied style prop rather than assuming one, since the two consumers need genuinely different layout contexts. Shipped with zero bugs on the first on-device screenshot — the underlying track rendering was already fully validated by BulletChart before this component existed.

  • BoxPlot is the twenty-third and final component in this package's roadmap — same "no layout algorithm, no Victory Native/Skia primitive" situation BulletChart/Heatmap already found, just with vertical instead of horizontal percentage positioning for the whiskers/box/ median/outliers, the same top/height percentage technique BulletTrack already validated on-device for left/width. Ported percentile/computeStats/resolveStats (the quartile and 1.5×IQR outlier-fence math) verbatim — pure number-crunching, no rendering concerns. A real layout gap, not from the percentage geometry itself: the web version's tick-label column and each column's own track get their vertical alignment "for free" from ordinary CSS box flow; RN has no equivalent, so the axis column's tick-track area is given its own bottom spacer with the exact same height as each data column's label row (LABEL_ROW_HEIGHT), guaranteeing by construction — not by relying on how percentage top resolves against a padded containing block — that the axis's 0%/100% line up with the real track's max/min. Two real on-device bugs, both in the tick-label sizing, not the alignment fix above: a Text pinned with both left: 0 and right: 8 (mirroring the web's fixed 44px column too literally) forced a 36dp box that ellipsized longer formatted numbers ("334.56" rendered as "334...."); removing left fixed that case but a still-wider formatted string ("331.28ms", from a custom valueFormatter) truncated again — RN clamped even an unconstrained absolutely-positioned Text's measured width to its containing block's size in practice, unlike the web <span>, which has no width constraint at all and simply overflows left when it needs to. Fixed with a generous explicit width (120, well beyond the 44dp column) plus textAlign: 'right', so the digits stay anchored on their tick position regardless of how many characters any formatter produces. General lesson: don't assume an absolutely-positioned RN Text with only one offset set (right, no left/width) sizes itself to its own content the way a web <span> would — give it an explicit generous width instead of relying on that assumption, especially for anything driven by a caller-supplied formatter.