Tide Analysis
A tide explainer fitting eight Indonesian ports from raw readings, which refuses when a record is too short

- Indonesian ports covering all four tide types
- 8
- hourly records per port, January to August 2026
- 212 days
- amplitude error recovering constants under 15 cm noise
- < 1 cm
- tide, linear-algebra or charting libraries
- 0
On this page
The problem
Students and curious readers who want to understand tides mostly find tools that look up answers. Most tide predictors take published constants, numbers that describe how the Moon and Sun pull the water at one coastline, and add them up into a curve. That's fast, but it proves nothing about the physics and can't work for a place with no published constants.
Tide Analysis starts from raw hourly sea-level readings and works out those constants itself, in the browser. That exposes something lookup tools hide. Some tidal cycles are so close that telling them apart takes about six months of data. Ask a two-week record to separate them, and the right answer is "I can't". The site is not for navigation, and it names Indonesia's official hydrographic office, Pushidrosal, as the source to use.
What I built
The maths is tested against records with known answers
I wrote the astronomy, the curve fitting and the prediction engine myself, as plain functions I can test without a browser. Before the solver existed, I built a generator that makes fake tide records from constants I choose, with noise I choose. The solver has to recover those constants. With 15 cm of noise on every reading, it still recovers every amplitude within a centimetre. When one of these tests fails, the maths is wrong, never the tolerance.
It says when a record is too short
Before fitting, the app checks whether the record is long enough to separate each pair of cycles. If not, it refuses and names the two that conflict and how many days would resolve them. Every fit also reports a stability figure, and the chart always shows the gap between prediction and observation, where weather and storm surges appear. The main interactive lets you drag the record length from 15 days to about six months and watch cycles become resolvable.
I pick the sensor that is actually recording
A tide station can report several sensors, and the most complete one can be broken. One radar sensor at Benoa sat flat at −0.281 m for seven months while the gauge beside it recorded 2.3 m tides. Another sensor, at Semarang, had 9-metre spikes that made it look the most active. I settled on picking the sensor with the largest typical range and treating spikes as gaps. Each of these cases is now a test.
Surprising results stay in, as tests
Losing 70% of a record in scattered pieces barely hurts the fit. Losing the same 70% in one block ruins it. I kept that as a test. A stability panel fits each record in four separate stretches to show whether a constant belongs to the harbour or to those months. At Benoa, the main lunar tide moves 9% between stretches and the main solar tide moves 56%.
Data is only shipped when its licence is clear
A check runs before every build and fails it if any bundled data source has an unresolved licence. Two of the three sources I wired up, UHSLC and BIG, ship switched off, because their terms for Indonesian stations were never confirmed.
Result
Tide Analysis is live, in Indonesian and English, and runs entirely in your browser with no server. It bundles eight Indonesian ports from the IOC sea-level network, each a 212-day hourly record from 1 January to 1 August 2026. Together they cover all four tide types, each classified from the station's own fit. You can compare modern fitting with the classical Admiralty method, and toggle cycles on and off to see the two-week spring-neap rhythm appear. One worked example walks a single constant from timestamp to answer. I built it over three days, 12 to 14 August 2026.
Under the hoodTechnical detail for engineers
lib/astroholds the Doodson argument calculator and nodal corrections;lib/tideholds the least-squares fit, prediction and the Admiralty method. Plain functions over typed arrays, with no React, DOM, clock or network.- The Rayleigh criterion gates each fit. Refusals are structured and tested in both directions. K1/P1 and S2/K2 need about 183 days; 15 days resolves six of the ten standard constituents and 29 days resolves eight.
- Every solve returns its condition number (κ) in the result type. Constituent correlation, leakage at refused frequencies and coverage masks are reported alongside.
- Sensor rule: largest robust scale, with spikes rejected into declared gaps. Two stack overflows on real-sized inputs were fixed after synthetic tests passed.
records:validatechecks every source against a licence manifest before the build. IOC, UHSLC and BIG each have an adapter to one internalRecordshape. UHSLC was planned as enabled, but its Indonesian data is attributed through BIG.- The constituent explorer re-sums fitted constants and doesn't refit. A live refit would let M2 absorb its neighbours and hide the spring-neap beat between M2 and S2.
- The ± on each constant is one standard error assuming independent hourly noise, which tidal residuals violate. At Benoa, S2's ± is about 1% while its movement between stretches is 56%. M4 moves 81%.
- Tide type uses the Formzahl ratio, from 0.204 at Sabang to 3.486 at Kolinamil in Jakarta's port. Kolinamil has 740 missing hours and covers 202.5 of the 212 days, but its gaps are scattered and it fits cleanly. The chart is hand-built SVG, with no charting, linear-algebra or tide library. Two runtime dependencies (Next.js, React).
- Static export to GitHub Pages, gated on tests and the licence check. 377 tests in 24 files. About 12,500 lines of TypeScript, 88 commits.
Screenshots
Hiring, or building something similar?
I'm open to full-time roles and select freelance work.