loopable / web /src /viz /seriesData.ts
fsanyoto's picture
Deploy AIOS web (React glide grid + FastAPI slice)
d349fee verified
Raw
History Blame Contribute Delete
4.91 kB
// ---------------------------------------------------------------------------
// 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)
);
}