RORK LABJP
EVENT — Apple holds its Surprise and Shine event today, September 9, starting at 10:00 Pacific. That lands in the small hours of September 10 in JapanEXPECT — Expected are the iPhone 18 Pro and Pro Max, a foldable, the 2nm A20 Pro chip, and release dates for iOS 27 and its sibling updatesWAIT — As this is written the event has not happened yet. Rumor-stage writing and post-announcement writing look identical once they are mixed togetherMAX — Since Rork Max generates native Swift, Apple news is not somebody else's problem. Worth repeating that the standard product still writes React NativeSIMULATOR — Rork Max compiles on cloud Macs and lets you check the result in a streaming iOS simulator inside the browser, with no Xcode and no Mac hardwareSEASON — A new OS is when automated build pipelines wobble most. An article selling convenience owes its readers a word about that wobbleEVENT — Apple holds its Surprise and Shine event today, September 9, starting at 10:00 Pacific. That lands in the small hours of September 10 in JapanEXPECT — Expected are the iPhone 18 Pro and Pro Max, a foldable, the 2nm A20 Pro chip, and release dates for iOS 27 and its sibling updatesWAIT — As this is written the event has not happened yet. Rumor-stage writing and post-announcement writing look identical once they are mixed togetherMAX — Since Rork Max generates native Swift, Apple news is not somebody else's problem. Worth repeating that the standard product still writes React NativeSIMULATOR — Rork Max compiles on cloud Macs and lets you check the result in a streaming iOS simulator inside the browser, with no Xcode and no Mac hardwareSEASON — A new OS is when automated build pipelines wobble most. An article selling convenience owes its readers a word about that wobble
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.

Rork558Expo203React Native236Data VisualizationIndie Development26

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 $15 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