// Which of a chart's series the diver last chose to see, kept across visits. // // The counterpart to `chart-period-view.ts`, and deliberately a separate entry: that // one remembers *where* on the timeline you were looking, this one remembers // *what* you had plotted. Same storage for the same reasons - `localStorage`, // because a remembered view has to survive the tab closing; not the URL, because // it has to work from the bare `/home` and `/dives/{uuid}` that the nav // links point at and people bookmark. // // Generic over the series keys because two charts want it and their keys have // nothing in common: the profile chart plots depth, the deco ceiling, // temperature, tank pressure, heart rate and the six channels a computer's own // decompression arithmetic produces; the gas chart plots dives/trend/average. // What is shared is the shape - a set of keys, re-validated on the way in // against the ones the chart actually has. // // What's stored is a view preference: a handful of series names the chart itself // defines. No dive data, nothing fetched. // // **A superseded key is left in storage rather than cleaned up**, which is the // precedent for the next bump: see "Adding a channel needs a // remembered-selection key bump; removing one does not" in DECISIONS.md. Forty // inert bytes against a list of dead key names that would have to be carried, // kept correct, and grown on every future bump - in code whose whole job is to // be forgotten. // Versioned (`-vN`). `parseSeriesVisibility` filters a stored selection down to // the keys this build plots, which is exactly right for a key that has *gone* and // exactly wrong for one that has arrived: a selection written before a channel // existed names only the older ones, all of them still available, so it would // restore cleanly and leave the new channel switched off - hiding a feature behind // a legend entry sitting right there claiming the diver had turned it off. // // Bumping the key drops those selections and opens on everything plotted, which // is what a first visit already does. The cost is one diver's hidden temperature // line coming back once; the alternative is a feature that is invisible to // precisely the people who have used the chart before. // // **The general rule: adding a key to a `parseSeriesVisibility` list needs a key // bump; removing one does not.** export const DIVE_PROFILE_SERIES_KEY = "opendiving:dive-profile-series-v5"; export const GAS_USE_SERIES_KEY = "opendiving:gas-use-series"; // The stored entry, raw and unparsed. // // Deliberately returns the string rather than a parsed array: this is the // snapshot the charts hand to `useSyncExternalStore`, which compares snapshots // with `Object.is` and loops forever if handed a freshly built array each call. // A string compares by value, so parsing happens once, downstream. export function readStoredSeries(storageKey: string): string | null { try { return window.localStorage.getItem(storageKey); } catch { // Storage can be unavailable (Safari private mode, storage disabled) - and // where it is missing entirely, reaching through it is a `TypeError` rather // than a storage error. Opening with everything plotted is a fine outcome, // not an error case. return null; } } // A stored entry as a selection, or null when there isn't one worth restoring - // which the charts read as "plot everything". // // Filtered through `allowed` rather than validated against it: a key this build // no longer plots is stale, not corrupt, and dropping it leaves the rest of a // perfectly good selection intact. export function parseSeriesVisibility( raw: string | null, allowed: readonly Key[], ): Key[] | null { if (!raw) return null; try { const parsed: unknown = JSON.parse(raw); if (!Array.isArray(parsed)) return null; const keys = allowed.filter((key) => parsed.includes(key)); // An empty selection is a legitimate thing to store - every series can be // hidden - but an entry naming *only* things this chart has never heard of // is a stale build's, and restoring it as "hide everything" would open on a // blank plot for no reason the diver could account for. return keys.length > 0 || parsed.length === 0 ? keys : null; } catch { return null; } } export function writeSeriesVisibility( storageKey: string, visible: readonly string[], ): void { try { window.localStorage.setItem(storageKey, JSON.stringify(visible)); } catch { // As above - and losing the remembered selection costs a click, not data. } } // A selection with `key` flipped, in the chart's own order rather than in the // order the toggles were clicked. That keeps the stored entry stable across // equivalent selections, and means nothing downstream has to sort it before // deciding what to draw. export function toggleSeries( visible: readonly Key[], key: Key, order: readonly Key[], ): Key[] { const next = new Set(visible); if (!next.delete(key)) next.add(key); return order.filter((candidate) => next.has(candidate)); } // The subscribe half of `useSyncExternalStore`, which deliberately never fires. // // A remembered selection is read once, when the chart mounts, and then left // alone: a second tab hiding *its* temperature line should not yank the curve // out from under whoever is reading this one. Module-level so the reference is // stable - an inline arrow would make React tear down and re-subscribe on every // render. // // Lives here rather than in `chart-period-view.ts` because the remembered-view // readers that want it are not all about periods. export function subscribeToNothing(): () => void { return () => {}; }