DEV Community

Sammii
Sammii

Posted on

How I built Lunary's birth chart reader

The birth chart feature is the core of Lunary. Every other feature either feeds from it or refers back to it. Building it properly took longer than I expected, mostly because "properly" turned out to have more requirements than I'd initially scoped.

This is the story of how that feature was built: the calculation layer, the visual representation, the AI interpretation, and the iterations that got it from a rough prototype to something I'm proud of.

What a birth chart actually contains

A complete birth chart has more elements than most apps show. Lunary calculates and displays:

Planets: Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto, and Chiron. Each has an ecliptic longitude (position in the zodiac), a latitude (angular distance from the ecliptic plane), a declination (angular distance from the celestial equator), and a speed (degrees per day, which indicates retrograde when negative).

Points: The North Node (also called the True Node, computed from the Moon's orbital ascending node), the South Node (opposite the North Node), and the Part of Fortune (calculated from Ascendant + Moon - Sun, or the reversed formula for night charts).

House cusps: All 12 Placidus house cusps, computed from the local sidereal time at the birth location and time. The Ascendant is the 1st house cusp and the Midheaven (MC) is the 10th house cusp.

Aspects: All angular relationships between planets and points that fall within orb for the major aspect types (conjunction, sextile, square, trine, quincunx, opposition). Displayed as a grid showing each planet pair.

That's the data layer. The visual layer has to present all of this in a way that's readable.

The chart visual

The traditional birth chart wheel is a circle divided into 12 houses, with the zodiac signs running around the outside and planets placed at their positions within the wheel. This is the standard visual that anyone who has studied astrology will recognise, and departing from it creates friction.

Lunary's chart visual is an SVG rendered server-side. The zodiac outer ring, house lines, and planet glyphs are all drawn as SVG paths and text elements. The positions are computed from the ecliptic longitudes and the Ascendant angle.

The main challenge in chart wheel layout is planet clustering. When two or more planets are close together in the zodiac, their glyphs overlap. Real chart software handles this with a "displacement" algorithm that adjusts the angle of crowded glyphs while keeping a line connecting them to their actual position. Implementing this correctly took several iterations. The naive approach was to offset clustered planets by a fixed amount, which worked for pairs but failed for clusters of three or more. The final algorithm sorts planets in a cluster by longitude, then distributes them evenly within the cluster space while preserving the order.

The data panel

Alongside the wheel, Lunary shows a structured data panel with:

  • A table of all planetary positions (sign, degree, minute, retrograde status)
  • A table of house cusps
  • An aspect grid (a triangular matrix where each cell shows the aspect between two planets)
  • A section for dignity and debility (planets in their rulership, exaltation, fall, or detriment)

The dignity section took research to implement correctly. Every planet has rulership over certain signs (Mercury rules Gemini and Virgo, Venus rules Taurus and Libra, etc.), exaltation in one sign (the Sun is exalted in Aries, the Moon in Taurus, etc.), fall in the sign opposite its exaltation, and detriment in the sign opposite its rulership. These are classical astrological concepts that Lunary surfaces automatically.

The AI interpretation layer

The AI interpretation is where the most interesting engineering happened. The problem with using a language model for birth chart interpretation is straightforward: models have been trained on a lot of astrological content and will happily produce natal chart readings that sound plausible but aren't grounded in the user's actual positions.

A model given "interpret this Scorpio rising" will produce a reading about Scorpio rising in general, drawing on all the Scorpio rising content it was trained on. That's not what we want. We want a reading that references the specific degree of the Ascendant, the Ascendant ruler (Mars or Pluto for Scorpio), where that ruler is placed, and how that placement interacts with other chart factors.

The solution was to not ask the model to interpret the chart from scratch. Instead, we describe the chart to the model in astronomical terms:

Natal chart data:
- Sun: 14°32' Gemini, 3rd house, applying trine to Saturn (orb: 2°15')
- Moon: 28°47' Pisces, 12th house, separating conjunction with Neptune (orb: 3°22')
- Mercury: 2°19' Cancer, 3rd house, retrograde, applying square to Mars (orb: 4°07')
[... all planets]

House cusps:
- 1st (Ascendant): 3°41' Scorpio
- 10th (Midheaven): 14°22' Leo
[... all cusps]

Active aspects:
[... list with orbs]

Task: Write a narrative birth chart interpretation based on the above positions...
Enter fullscreen mode Exit fullscreen mode

This framing forces the model to reason about the specific data provided rather than recall general astrological content. The chart is described as a set of facts and the model is asked to synthesise those facts into a narrative.

The improvement in output quality from this approach was significant. Interpretations that use this structured prompt consistently reference the actual positions, mention specific aspects with their orbs, and integrate the house placements correctly. Generic interpretations that don't mention the specific data are much rarer.

What the user sees

The finished birth chart experience walks through:

  1. A visual wheel with all planets and house divisions
  2. A "chart summary" that highlights the three most significant placements (usually Ascendant, Sun, Moon) in natural language
  3. A full planetary positions table
  4. An AI narrative covering the major themes of the chart
  5. The aspect grid with tappable aspects that link to grimoire articles about those specific aspect combinations
  6. A dignity and debility section

The tappable aspects are probably the feature I'm most proud of. You see that your Sun squares Saturn, you tap it, and you get the grimoire article specifically about Sun square Saturn in a natal chart. That connection between the live chart data and the educational content is exactly what I wanted to build when I started.

The iteration process

The first version of the chart reader was much simpler: positions table, no visual, basic AI summary. The visual wheel came in a later iteration after I established that users found the raw data table confusing without the visual context.

The AI interpretation went through the most iterations. The first version used a basic system prompt asking the model to "interpret this birth chart" with the chart positions appended. The output was superficially plausible but not grounded in the data. The second version described the chart in structured astronomical terms, which improved grounding significantly. The current version also includes a passage from the grimoire for each major placement, which grounds the model's interpretive vocabulary in Lunary's own educational content.

Each iteration was motivated by actual user feedback. The things that seemed obvious from a developer perspective (of course the positions table is useful) were not always obvious to users, and the things I thought were secondary (the aspect grid) turned out to be some of the most used features.


I'm Sammii, founder of Lunary and indie developer building tools I actually want to use. I write about shipping products solo, the technical decisions behind them, and figuring it all out in public.

Top comments (0)