Split a trip into its two economies, so travel stops being a 60% slab
ci / lint-test (push) Successful in 48s

travel dominated every trip page and said nothing. The tempting fix is a finer
travel taxonomy, which needs a hand-maintained merchant list — the trap #19
already describes — and it is also the wrong diagnosis.

travel is the only category that spans both phases of a trip. Every other one is
100% on-the-ground: on Europe 2026, dining, transport, entertainment, groceries
and shopping are all exactly $0.00 before departure. The chart was not bad, it
was two economies stacked into one, and travel was the only thing visible in the
union.

So split on start_date and use the axis that carries information in each phase.
Booked ahead ($22,050.51, 57%) is all flights and stays, so merchant is the axis
— Agoda $4,490, Air India $3,454, Luxury Escapes $3,284. On the ground
($16,946.94) travel falls to $8,241 among dining $4,452 and transport $2,938, and
category is finally worth charting.

The hero is the ratio, not a lone total, with the on-ground daily rate beside it
— the only figure comparable between trips, since totals are not: Europe
$677.88/day against Auckland $83.39. A trip with near-zero committed spend says
so, because Sonu + Sunny's $184.84 is a filing artefact (both legs' bookings sit
on the first trip), not a cheap trip.

Two dataviz rules this page was breaking. Category bars now use one copper hue
with the name as a direct label: the per-bar rainbow double-encoded identity the
label already carries, and the trip subset fails CVD validation on this surface
(other vs shopping at delta-E 5.0 protan, below the floor of 6). And the hero
figure drops the serif and tabular-nums, which read as decoration at that size.
The phase bar is two ordinal steps of one hue, validated with --ordinal against
the card surface, with a 2px gap so the boundary is an edge.

278 passing, build clean. Data verified against the database directly; I could
not render the page in a browser to eyeball the layout.
This commit is contained in:
2026-08-02 22:20:55 +10:00
parent 2d341e24a0
commit 9e4b518f57
3 changed files with 359 additions and 89 deletions
+54
View File
@@ -252,6 +252,60 @@ trip never incurred a cost it cancelled.
Europe — Sonu + Sunny (id 3, 1228 Apr, created 2026-07-28 from tag 5),
Singapore + Bangkok 2026 (id 4).
### Why `travel` looked useless on a trip page, and the fix (2026-08-02)
`travel` was ~60% of every trip and told you nothing. The tempting fix — a finer
travel taxonomy (flights / stays / getting around) — needs a hand-maintained
merchant list, which is the trap ticket #19 already describes. It is also the
wrong diagnosis.
**Measured on Europe 2026, `travel` is the only category that spans both phases
of a trip. Every other category is 100% on-the-ground — dining, transport,
entertainment, groceries and shopping are all exactly $0.00 before departure.**
So the category chart was not a bad chart; it was two different economies stacked
into one, and travel was the only thing visible in the union.
The fix is to split on `trips.start_date` and use the axis that carries
information in each phase:
- **Booked ahead** (before `start_date`, $22,050.51 / 57% on Europe) — everything
is a flight, a stay or a rail ticket, so category is a constant and **merchant**
is the axis: Agoda $4,490.52, Air India $3,454.32, Luxury Escapes $3,283.80.
- **On the ground** (on/after `start_date`, $16,946.94) — travel falls to
$8,240.74 among dining $4,452.18, transport $2,937.70, entertainment $698.14,
groceries $589.22. **Category is finally worth charting.**
`getTripAnalytics` returns `phases`, `committed_merchants`, `on_ground_categories`
and `on_ground_daily`. A trip with a NULL `start_date` has no knowable departure,
so the SQL folds everything into on-ground rather than reporting it as committed.
**The daily rate is the only figure comparable between trips**, because totals are
not — trips differ in length. Europe $677.88/day, Sonu + Sunny $573.57, Singapore
+ Bangkok $309.76, Auckland $83.39. The page ranks the current trip against the
others from the already-loaded `useTrips()` list.
**A trip with near-zero committed spend is a filing artefact, not a cheap trip.**
Europe — Sonu + Sunny shows $184.84 committed against Europe 2026's $22,050.51
because both legs' flights and stays were filed on the first trip. The page says
so rather than letting the ratio read as missing data.
Two chart rules this page now follows, both from the `dataviz` skill and both
previously broken here:
- **One series → one colour.** The category bars use a single copper hue with the
category as a direct label. `CATEGORY_COLORS` was a per-bar rainbow, which
double-encodes identity the label already carries — and the trip subset **fails**
CVD validation on this surface (`other``shopping` ΔE 5.0 protan, below the
floor of 6). Do not reintroduce per-category colour on a labelled bar chart.
- **No serif and no `tabular-nums` on the hero figure.** Fraunces is for section
headings; a display face on a large number reads as decoration, and equal-width
digits make it look loose.
The phase split bar is two ordinal steps of one hue (`#7c4820``#d28a47`),
validated with `--ordinal` against the `#171410` card surface, with a 2px gap so
the boundary is an edge rather than a colour change. Both segments are
direct-labelled, so it needs no legend.
### Trip participation is derived, and a trip is shared
Rebuilt 2026-08-02. Trips were scoped to `trips.owner_id`, so Sonu saw **no