Building HabitChart: A Technical Deep Dive

A behind-the-scenes look at how we built the projection engine, implemented real-time calculations, and created a PWA that works offline.

Posted by

The Challenge

I wanted to build something that would make the compound effect visceral and immediate. Not a generic habit tracker, but a projection tool that shows you exactly what your daily choices become over time.

The technical requirements were clear:

  • Real-time calculations (no lag, even for 50-year horizons)
  • Mobile-first performance (under 2 seconds to interactive)
  • Offline capability (PWA with service workers)
  • Guest mode (try before you commit)

The Tech Stack

I chose Next.js 14 with TypeScript for the foundation. Here's why:

  • Server-side rendering for fast initial loads
  • API routes for backend logic without a separate server
  • TypeScript for type safety in projection calculations
  • Built-in optimization for mobile performance

For the chart visualization, I chose Victory.js—it's responsive, customizable, and handles large datasets smoothly. The projection engine renders up to 100 data points for each horizon without frame drops.

The Projection Engine

The core of HabitChart is the projection calculation. Here's the actual implementation:

export function calculateProjection(
  habit: Habit,
  horizonType: HorizonType,
  config: ProjectionConfig = DEFAULT_CONFIG
): ProjectionData {
  const horizonDays = HORIZON_CONFIG[horizonType].days;
  const baseValue = habit.dailyAmount;

  const adopter: Point[] = [];
  const nonAdopter: Point[] = [];

  for (let day = 0; day <= horizonDays; day += Math.max(1, Math.floor(horizonDays / 100))) {
    const adopterValue = baseValue * Math.pow(config.compoundingRate, day);
    const nonAdopterValue = baseValue * Math.pow(config.decayRate, day * 0.1);

    adopter.push({ x: day, y: adopterValue });
    nonAdopter.push({ x: day, y: nonAdopterValue });
  }

  return { adopter, nonAdopter, horizon: horizonDays, unit: habit.unit };
}

The key insight: sample every Nth day instead of calculating all 18,250 days for a 50-year projection. This keeps the calculation instant while maintaining visual smoothness.

Guest Mode Architecture

One of my favorite features is guest mode. You can try HabitChart without creating an account—no friction, no commitment. Here's how it works:

  • Habits stored in localStorage (client-side)
  • Projection calculations run entirely in the browser
  • No backend calls until you decide to save
  • Auto-migration to MongoDB when you sign up

This architecture means you can create a habit and see its projection within 15 seconds of landing on the site. No signup forms, no email verification—just immediate value.

PWA Implementation

HabitChart works offline using next-pwa. The service worker caches:

  • All habit calculation logic
  • The projection chart component
  • Today's logging interface

This means you can log your habits and view projections even without internet. Changes sync automatically when you're back online.

Testing the Formulas

Projection calculations need to be bulletproof. I used Jest to test every edge case:

describe('calculateProjection', () => {
  it('should generate compound growth for adopter', () => {
    const result = calculateProjection(mockHabit, '1mo');
    const firstDay = result.adopter[0].y;
    const lastDay = result.adopter[result.adopter.length - 1].y;
    expect(lastDay).toBeGreaterThan(firstDay);
  });
});

All projection logic, time budget calculations, and plateau detection are covered by unit tests. No shipping broken math.

What I Learned

Building HabitChart taught me that performance and user experience go hand-in-hand. The projection needs to be instant because the magic is in that immediate visualization of your future self.

It also reinforced the value of starting simple: guest mode, one-time payments, offline-first. These constraints made the app better, not worse.