@zephytiju/prism-stats-panel
v0.1.0
Published
Platform Prism stats-panel micro-UI (component id "stats-panel"): GeoVision v7 statistical analysis panel — KPI strip derived from the shared "query-box.entity-results" Prism channel (matches, threat-tagged count via tagPalette, fusion confidence), MOVEME
Readme
PrismStatsPanelMicroUI
Platform Prism stats-panel micro-UI. Component id: stats-panel.
Published to npm as @zephytiju/prism-stats-panel.
The stats panel renders the CONFIRMED GeoVision v7 statistical analysis panel (design record §05): the
panel title header (STATISTICAL ANALYSIS / mono subtitle / × close affordance — locale-bundled by
default, overridable through configuration), the
KPI strip, the MOVEMENT ACTIVITY bar chart, the SOURCE DISTRIBUTION donut with its legend, and the
CUSTOM ANALYSIS builder. The component is a PURE CONSUMER: it derives everything from the shared
query-box.entity-results Prism channel plus the host Lattice transport, and publishes nothing — no
channel writes, no events, no audit records. Composed applications (for example Guanlan) consume it
as-is; the component is platform-owned.
Data derivation notes
| Region | Source | Derivation |
| --- | --- | --- |
| KPI strip | shared channel query-box.entity-results (no fetch) | matches = result item count; threats = items whose tag resolves to the threat semantic token through the configured tagPalette; fusion = the percentage of result items the palette could tag at all (the fusion-confidence metric), — when there are no results |
| MOVEMENT ACTIVITY | createTimelineEventClient(useLatticeTransport()).timeline({ subjects, window }) | one request per entity-results change; events binned into 10 equal-width bins across window (out-of-window/unparseable instants dropped), bar heights normalized against the tallest bin, token ramp by relative intensity (≥0.85 threat, ≥0.65 warn, ≥0.4 ok, else accent); gridlines at 30/50/70% and the −6H/−3H/NOW axis per the design |
| SOURCE DISTRIBUTION | createEvidenceSourceClient(useLatticeTransport()).evidence({ refs }) | provenance records aggregated by source, sorted by weight (alphabetical tie-break); integer percentages normalized through the largest-remainder method so shares always sum to 100; donut arcs are hand-rolled SVG stroke-dasharray circles accumulating offsets, legend rows share the same tokens |
| CUSTOM ANALYSIS | kinds-filtered timeline({ subjects, window, kinds }) per run | see below (design decision D3) |
Both charts are hand-rolled inline SVG — no third-party chart library is used anywhere.
CUSTOM ANALYSIS and design decision D3
Per D3 the card list is
component-internal app-runtime state, deliberately NOT Prism composition: NEW MODEL starts (resets)
a builder draft; the operator picks a visualization (BAR / LINE / HEAT / NETWORK), a MEASURE (each
measure carries an interface-typed event-kind filter — movement-count → kinds: ["movement"],
event-frequency → kinds: ["movement","signal","report"]) and condition chips (toggleable, + ADD
appends further conditions); RUN ANALYSIS executes the kinds-filtered timeline query over the
channel's entities and APPENDS a rendered same-kind card to the in-panel card list. Cards are never
composition children, never variant switches, and the run emits no audit event (analysis queries are
reads). Each card shows its {viz} · {n} EVENTS meta, its hand-rolled SVG chart, its active
condition chips, and a × removal.
Configuration keys
| Prop | Meaning |
| --- | --- |
| window | Required analysis window { start, end } (ISO-8601 instants) forwarded to every ITimelineEvent query — never a hardcoded band |
| locale | UI locale for the component-fixed strings: "en" \| "zh-CN" (default "en") — see i18n |
| title | Panel header title — composition-authored override; when omitted the locale bundle's title default renders (STATISTICAL ANALYSIS / 统计分析) |
| subtitle | Panel header mono subtitle — composition-authored override; when omitted the locale bundle's subtitle default renders (CORRIDOR MODEL / SYNTHETIC DATA / 走廊模型 / 合成数据) |
| tagPalette | Entity tag → semantic color token; entries resolving to threat drive the THREATS KPI and palette-known tags drive the FUSION metric (e.g. { military: "threat" }) |
| kpiLabels | Optional { matches?, threats?, fusion? } KPI label overrides (configuration strings, localized by the composer) |
| shareTokens | Semantic-token ramp the source-distribution shares cycle through (default ["ok","accent","warn","signal","muted"]) |
| onClose | Invoked when the header × is clicked (the host dismisses the panel) |
Channel contract
| Direction | Kind | Id | Payload |
| --- | --- | --- | --- |
| consumes | state | query-box.entity-results | EntityResultsProjection \| null — the shared entity-results channel published by the query-box micro-UI |
That is the whole contract: the component consumes exactly one channel and publishes none. Channel
ids are string literals at every call-site so the build-time channel-graph scanner can derive the
graph. There are no usePrismStateSetter bindings and no emitPrismEvent calls in the component —
it never re-renders peers from its own publications because it makes none.
Lattice binding
createTimelineEventClient / createEvidenceSourceClient over useLatticeTransport() from
@zephytiju/lattice-common-interfaces — every aggregate goes through the per-method routes
interfaces/ITimelineEvent/timeline and interfaces/IEvidenceSource/evidence with full
server-side validation. No URLs, credentials, or generic invokes. The channel's result items are
mapped onto EntityReferences (ontologyInterface: "IEntitySummary") for the typed subjects /
refs arguments. Loading, empty (no results yet / no events / no provenance) and error + retry
states are first-class for both fetches; a superseding entity-results change invalidates in-flight
fetches (sequence guard).
Internationalization (i18n)
The component ships en and zh-CN locale bundles — src/locales/en.json / src/locales/zh-CN.json —
and every component-fixed UI string is resolved from them (the panel header title/subtitle defaults,
the KPI labels MATCHES/THREATS/FUSION, the
module titles MOVEMENT ACTIVITY / SOURCE DISTRIBUTION / CUSTOM ANALYSIS, the window meta
LAST {hours} HOURS interpolated from the configured window, the −6H/−3H/NOW axis, the donut center
label, NEW MODEL, SELECT VISUALIZATION, BAR/LINE/HEAT/NETWORK, the measure and condition chip labels,
RUN ANALYSIS, the state copy, the panel/chart aria labels, the non-Error fetch fallbacks, and the
close/remove aria labels). The component renders no hardcoded copy.
locale?: "en" | "zh-CN"prop (default"en") selects the string table per instance — it is a per-instance prop, so two instances in one host may render differing languages.- Every visible string — including the default header — is locale-driven.
title/subtitle/kpiLabelsare optional composition-authored overrides: a host that passes them wins per locale, and a host that omits them gets the locale-bundled defaults, so a singlelocaleswitch localizes the whole panel. - Locale bundles are namespaced under the component id (
"stats-panel") so a composer can deep-merge every component's bundle into ONE UI language bundle without collisions:
import { locales as statsPanelLocales } from "@zephytiju/prism-stats-panel";
// statsPanelLocales.en -> { "stats-panel": { … } }
const uiBundle = deepMerge(hostStrings, queryBoxLocales.en, statsPanelLocales.en);The parsed bundles are exported from the package entry (locales, en, zhCN,
stringsForLocale, formatMessage), and the raw JSONs are also served by the ./locales/* exports
subpath (e.g. @zephytiju/prism-stats-panel/locales/zh-CN.json); files ships both dist and
locales.
Theme
No palette is hardcoded. Every color resolves to SEMANTIC theme tokens (ok, accent, threat,
warn, signal, card, card-dark, input, border, line, text, muted, deep, panel)
consumed as CSS variables, plus --mantine-font-family-monospace for the mono typography — the
palette is supplied entirely by the host's MantineProvider. The local demo ships TWO themes, both
defined in src/demo.tsx: geovisionTheme (dark), mapping each semantic token onto the exact
:root variables of the CONFIRMED design (deep bg, panel black, card, mint, red, blue, amber,
purple, muted, border), and the contrasting latticeLightTheme (light), mapping the SAME semantic
token keys onto a different palette — the component is skinned purely through the surrounding
MantineProvider.
Source layout
src/ is strictly two parts:
- Component source (what the package compiles):
StatsPanel.tsx(the panel),charts.ts(pure derivation/shaping math —deriveKpis,shapeActivityBars,shapeDistributionShares,donutArcs— exported for reuse and tests),ChartViews.tsx(the hand-rolled SVG chart components),types.ts,index.ts(public entry),src/locales/(en.json,zh-CN.json,index.ts— the i18n string bundles and their resolver), andsrc/shims/node-crypto.ts(browser-build shim for the digest import — build infrastructure). - Demo: exactly ONE file,
src/demo.tsx— the two host themes (GeoVision v7 + Lattice Light), the seeded shared channel (entity results with mixed tags), the mock host action executor (ITimelineEvent/timeline + IEvidenceSource/evidence, with a failing-feed variant) and all demo test data, and the demo page rendering multipleStatsPanelinstances side by side behind a global EN | 中文 language switcher (plus per-instance switches).
The npm package ships dist (compiled component + type declarations + locale JSONs) and the
top-level locales/ directory (the raw JSON bundles, served by the ./locales/* exports subpath);
no demo code is published. scripts/copy-locales.mjs copies the JSON bundles into both locations
during npm run build.
Local development
npm install
npm run typecheck
npm test
npm run dev
npm run shot-demonpm install pulls the platform peers (@zephytiju/prism-react,
@zephytiju/lattice-common-interfaces) from the npm registry, along with the host-side peer
dependencies (react, react-dom, @mantine/core). When consuming the published package, install
it directly (npm install @zephytiju/prism-stats-panel) and provide those peer dependencies in the
host application.
The demo (npm run dev, entry src/demo.tsx) renders TWO StatsPanel instances side by side, each
inside its own MantineProvider with a different theme (GeoVision v7 dark left, Lattice Light right),
behind an EN | 中文 segmented control — the GLOBAL switch sets the locale prop of BOTH instances at
once, and each instance carries its own per-instance control so the two hosts can render DIFFERING
locales simultaneously. The demo host seeds query-box.entity-results with entity results carrying
mixed tags (the way a completed search would — the panel itself only reads the channel) and installs
a mock action executor answering interfaces/ITimelineEvent/timeline and
interfaces/IEvidenceSource/evidence with deterministic projections. RUN ANALYSIS can be exercised
on either host — appended cards stay in that instance's internal list, demonstrating the D3
same-kind-item behavior directly. The HOST DEMO AFFORDANCES re-seed the channel, seed a failing feed
(error + retry paths) and clear it (empty state). npm run shot-demo boots the vite dev server,
drives the demo in headless Chrome (LEFT instance en, RIGHT instance zh-CN; runs one NETWORK
custom analysis in HOST A) and captures both instances with the KPI strip, both charts and the
appended card to /tmp/guanlan-review/demo-stats-panel-v2.png.
Design record
https://qcnwge0wy4s0.feishu.cn/wiki/K40nwA5TZiUE7Nk5hK6cE0W8nTe — §7 (channel contracts) and §8
(Lattice transport and interface clients); decisions D3/D4 in
https://qcnwge0wy4s0.feishu.cn/wiki/DZ78w8NneizBNhkhuE2cCyp5nRb. Visual reference: CONFIRMED
GeoVision v7 design (nexus-geovision-v7-standalone.html, .stats-panel region, section 05).
