| // --------------------------------------------------------------------------- | |
| // viz / seriesData.ts β EXIT wave 2 (W2-7/W2-9). The Y1 chart block's data | |
| // preparation, pure and React-free so `verify_ui.py` can run it under node. | |
| // | |
| // Both functions here guard a MEASURED trap rather than a hypothetical one: | |
| // Β· `inServerOrder` β the engine sorts a non-date category axis by VALUE | |
| // DESCENDING, so a trend renders as a revenue-ordered sawtooth unless the | |
| // server's chronology is reapplied (Y1 amendment 7). | |
| // Β· `asFields` β a chart field carries the viz `FieldType` vocabulary, NOT | |
| // Y1's `fmt` enum, and mixing them typechecks nowhere and renders as raw | |
| // text (Y1 rule 1). | |
| // --------------------------------------------------------------------------- | |
| import type { Bucket } from "./chartData"; | |
| import type { Field, FieldType } from "./types"; | |
| /** The Y1 `fields[]` entry β the server's minimal field descriptor. */ | |
| export interface WireField { | |
| key: string; | |
| label: string; | |
| type: string; | |
| source?: string; | |
| } | |
| /** The viz field-type vocabulary as a runtime set. β `money` is deliberately | |
| * absent: it belongs to Y1's `fmt` enum, and a chart field carrying it is the | |
| * mix-up rule 1 warns about. It lands on `text`, which is the visible symptom. */ | |
| export const VIZ_FIELD_TYPES: ReadonlySet<string> = new Set([ | |
| "text", "status", "currency", "int", "date", "pct", "select", "user", | |
| "multiselect", "checkbox", "phone", "email", "url", "rating", "created_time", "formula", | |
| // Wave 18 C5 β£b β the RUNTIME copy of the vocabulary. A `Set` is not a `Record`: the | |
| // compiler stays green when this goes stale and `asFields` silently downgrades the column | |
| // to text. When a `FieldType` joins the union, this line moves WITH the batch. | |
| "automation", | |
| // Wave 22 C7 β `metric` is numeric (a measure over a snapshot window) and PLOTS. | |
| "metric", | |
| // Wave 19 R7 β `image`. Listed for the reason the comment above gives, not because a chart | |
| // plots pictures: it does not. `isNumericFieldType` leaves it categorical, so it groups like | |
| // any other reference string. What listing it BUYS is honesty β without it `asFields` would | |
| // report the column as `text`, and the one thing a viz field type decides is what the surface | |
| // is allowed to claim about the column. | |
| "image", | |
| // Wave 23 C7 β `json`. THE BATCH LINE the union's own comment names: a `Set` is not a | |
| // `Record`, so the compiler stays green when this goes stale and `asFields` silently reports | |
| // a json column as `text`. Same reasoning as `image` β the type is categorical either way, | |
| // and what listing it buys is that the surface stops mis-naming the column. | |
| "json", | |
| // 2026-08-07 β the relational pair. THE BATCH LINE again: a `Set` is not a `Record`, so | |
| // without this `asFields` would report a rollup column as `text` and the chart builder would | |
| // refuse to plot the one column the feature exists to produce. | |
| "link", "rollup", | |
| // β Wave-27 item 13 (R13) β `code`. THE BATCH LINE a fourth time, and this one was caught by | |
| // `verify_ui` rather than by the compiler, which is the whole point of that gate existing: the | |
| // union in `viz/types.ts` went red under `tsc` the moment `code` joined the grid's, and this | |
| // Set stayed green while `asFields` quietly reported a code column as `text`. Categorical | |
| // either way (`isNumericFieldType` does not name it); what listing it buys is that the surface | |
| // stops mis-naming the column. | |
| "code", | |
| ]); | |
| /** | |
| * Y1 `fields[]` β viz `Field[]`. | |
| * | |
| * β An UNRECOGNISED `type` falls back to `text` rather than throwing β the | |
| * server may learn a field type before this client does (Y1 rule 7's reasoning, | |
| * one level down), and one unknown type must not blank a whole chart. `text` is | |
| * the safe landing: it groups, and it never claims to be measurable. | |
| */ | |
| export function asFields(wire: WireField[]): Field[] { | |
| return wire.map((f) => ({ | |
| key: f.key, | |
| label: f.label, | |
| type: (VIZ_FIELD_TYPES.has(f.type) ? f.type : "text") as FieldType, | |
| source: f.source === "overlay" ? "overlay" : "odoo", | |
| })); | |
| } | |
| /** | |
| * Re-order buckets to the server's declared chronology. | |
| * | |
| * β ANYTHING THE LIST DOES NOT NAME IS APPENDED, NEVER DROPPED. Filtering to | |
| * `x_order` would silently delete a period the server plotted but forgot to | |
| * order β a bar disappearing from a revenue trend, with nothing anywhere going | |
| * red. Sorting unnamed keys to the end keeps every value on screen and makes | |
| * the omission visible instead. | |
| */ | |
| export function inServerOrder(buckets: Bucket[], xOrder?: string[]): Bucket[] { | |
| if (!xOrder || xOrder.length === 0) return buckets; | |
| const rank = new Map(xOrder.map((k, i) => [k, i])); | |
| return [...buckets].sort( | |
| (a, b) => | |
| (rank.get(a.key) ?? Number.MAX_SAFE_INTEGER) - | |
| (rank.get(b.key) ?? Number.MAX_SAFE_INTEGER) | |
| ); | |
| } | |