RORK LABJP
BUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the outputNATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other buildersPLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screenCOMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to endPRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that backDEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verifyBUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the outputNATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other buildersPLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screenCOMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to endPRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that backDEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verify
Articles/App Dev
App Dev/2026-08-16Intermediate

My chart broke on day one, not at scale

A line chart that vanished for anyone with only a few days of data. The cause was a zero-height Y axis turning coordinates into NaN. Here is the measured behavior and the small normalization layer that fixed it.

Rork539Expo175React Native227Data VisualizationIndie Development23

I shipped a screen that plots your records over time. It worked perfectly on my phone.

Then a message arrived: "The chart area is just blank."

I could not reproduce it, because my own account had weeks of data behind it. The person writing to me had installed the app that morning. Working as an indie developer, my test device is almost always the least representative one I own.

The moment I loaded a dataset with a single entry, the line disappeared. The problem was not the charting library. It was four lines of axis math I had written myself.

When the values do not move, the axis height becomes zero

To draw a line chart you usually derive the vertical range from the minimum and maximum. That is exactly what I did.

const H = 200; // drawing area height in px
 
const naive = (values) => {
  const min = Math.min(...values);
  const max = Math.max(...values);
  return { min, max, span: max - min, pxPerUnit: H / (max - min) };
};

Here is what happens when you feed that function the inputs a real user can produce. These are actual results from running it in Node.

single point   span= 0 pxPerUnit= Infinity isFinite= false
same value x3  span= 0 pxPerUnit= Infinity isFinite= false
all zeroes     span= 0 pxPerUnit= Infinity isFinite= false
normal 7 days  span= 6 pxPerUnit= 33.333333333333336

With seven days of varied data you get a sensible 33.3px per unit.

With one data point, with a value that repeats, or with everything at zero, the span collapses to 0. JavaScript does not throw on division by zero. It returns Infinity and moves on.

No error. The calculation simply continues.

The result of that division flows straight into your coordinates

An infinite scale factor becomes something worse on the next multiplication. Same run, same script:

4 * Infinity = Infinity
0 * Infinity = NaN

When the value itself is 0, y = (value - min) * pxPerUnit evaluates to NaN.

Both SVG and Canvas treat a NaN coordinate as "do not draw this." No line, no warning. The component renders successfully and the user sees an empty frame.

My first instinct was to blame a library version. It cost me roughly half a day before I looked at my own helper. Numeric code that fails silently is the hardest kind to find, because nothing in the logs suggests anything went wrong.

Input statespanScaleWhat the user sees
Seven days of records633.3px/unitRenders as expected
One record only0InfinityNo point, no line
Same value three days0InfinityNot even a flat line
Padded with zeroes0InfinityCoordinates become NaN

Filling gaps with zero makes the chart say something else

Part of the collapse was my own doing. I was padding missing days with 0 so every series had exactly seven entries.

That was convenient for the array. It was dishonest on screen. "No record that day" and "the value was zero that day" are different facts to the person reading the chart.

For weight or sleep duration, a zero carves a spike straight down through the graph. For a counter, zero is often the truth.

I now write one line per metric describing which rule applies, mostly so that future me does not have to rethink it.

Metric typeMissing dayReason
Weight, temperature, sleepBreak the lineZero is not a possible reading
Steps, counts, saved itemsTreat as 0Zero occurrences is a real fact
Mood or subjective ratingsBreak the lineBlank should not read as neutral

One small layer in front of the chart

The fix lives in front of the charting library rather than inside its options, so that it survives a library swap.

It does two things: it enforces a minimum span, and it refuses input that cannot be drawn.

type Point = { x: number; y: number | null };
 
type Axis = { lo: number; hi: number; span: number; pxPerUnit: number };
 
export const buildAxis = (
  values: number[],
  height = 200,
  minSpan = 1,
): Axis | null => {
  const finite = values.filter((v) => Number.isFinite(v));
  if (finite.length === 0) return null;
 
  let lo = Math.min(...finite);
  let hi = Math.max(...finite);
 
  if (hi - lo < minSpan) {
    const center = (hi + lo) / 2;
    lo = center - minSpan / 2;
    hi = center + minSpan / 2;
  }
 
  return { lo, hi, span: hi - lo, pxPerUnit: height / (hi - lo) };
};

Measured output after the padding is applied. All three collapsed cases now produce a finite scale.

padded single    {"lo":3.5,"hi":4.5,"span":1,"pxPerUnit":200}
padded same x3   {"lo":4.5,"hi":5.5,"span":1,"pxPerUnit":200}
padded all zero  {"lo":-0.5,"hi":0.5,"span":1,"pxPerUnit":200}
padded normal    {"lo":3,"hi":9,"span":6,"pxPerUnit":33.333333333333336}

A single value now draws a mark centered in the plot. An all-zero series draws its line near the bottom with a little breathing room.

Pick minSpan to match the unit of the metric: 100 for step counts, 1 for hours of sleep. I reuse this helper across the small apps I maintain, and the unit is the one argument I have never been able to hard-code. A hard-coded constant will look wrong the moment you reuse the component for a different unit.

The null return covers the case where no finite value exists at all. The caller then shows something other than a chart.

I keep this in a plain module with no rendering imports, so it is testable without mounting a component. Three assertions covering a single point, a repeated value, and an empty array take a minute to write, and they fail loudly the next time someone changes the padding rule.

One more habit that helped: log a warning whenever buildAxis returns null in a release build. Silent numeric failures do not show up in crash reports, so unless you emit the signal yourself, the only way you learn about a blank chart is when someone takes the trouble to write to you.

Sometimes the right answer is not to draw the chart

Being able to render something is not the same as that something being worth showing.

A line chart built from one record is a single dot with no line. It tells the reader nothing about their own progress, and it looks like a defect.

I set my floor at three points. Below that I show a short message instead: "Two more days and your chart appears." That gives someone a reason to come back.

const MIN_POINTS = 3;
 
export const TrendSection = ({ points }: { points: Point[] }) => {
  const values = points.map((p) => p.y).filter((v): v is number => v !== null);
  const axis = buildAxis(values, 200, 1);
 
  if (values.length < MIN_POINTS || axis === null) {
    return <TrendPlaceholder remaining={MIN_POINTS - values.length} />;
  }
 
  return <TrendChart points={points} axis={axis} />;
};

This branch belongs to the same problem space as empty state design. Loading, no records, and not-enough-records look alike but mean different things, and merging them confuses people. I covered how to separate them in designing empty states for Rork apps. If you need grouped series in a bar chart, rendering multiple datasets side by side with react-native-chart-kit covers that implementation.

If you only do one thing after reading this, build three fixtures: one point, three identical values, and all zeroes. Open your chart screen with each. Three taps will save you the half day it cost me.

For the layer underneath, where the records themselves are defined and typed across several apps, I wrote up that structure in a typed analytics event layer across six Rork apps.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-08-06
Deciding overlay text legibility at ingest time instead of on device — four metrics measured side by side
Moving the question of whether text stays readable over a wallpaper out of the device and into the content pipeline. Four candidate metrics measured across 240 images, including what downscaled judging actually computes.
App Dev2026-07-14
Long-Press Context Menus for a Gallery Item in a Rork Expo App
Long-pressing a wallpaper card does nothing, yet iOS users expect a preview and a menu. From why Pressable alone falls short, to a native context menu with zeego, resolving the scroll-vs-long-press conflict, wiring up save and share, and a custom overlay fallback for Android — all with working code.
App Dev2026-07-07
Laying Out Variable-Height Images in Two Columns: A Masonry Wallpaper Gallery in a Rork Expo App
From why numColumns cannot pack variable-aspect images cleanly, to a dependency-free column-balancing algorithm, to keeping virtualization with FlashList masonry and a pragmatic no-dependency fallback, building a wallpaper gallery with real code.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →