| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { DashHeader, DataTable, KpiRow, Section, ValidationPanel } from "../ui"; |
| import type { DrillDescriptor, OnOpen } from "../ui"; |
| import type { |
| ChartBlock, |
| DecompDescriptor, |
| KpisBlock, |
| PageBlock, |
| PageControl, |
| PageEnvelope, |
| SectionBlock, |
| TableBlock, |
| ValidationBlock, |
| } from "../ui/types"; |
| import { isRowDrillRule } from "../ui/types"; |
| import { SeriesChart } from "../viz/SeriesChart"; |
| import type { ChartSpec } from "../viz/chartData"; |
| import type { Row } from "../viz/types"; |
|
|
| |
| |
| |
| interface Group { |
| head?: SectionBlock; |
| body: PageBlock[]; |
| } |
|
|
| function group(blocks: PageBlock[]): Group[] { |
| const out: Group[] = []; |
| let cur: Group = { body: [] }; |
| for (const b of blocks) { |
| if (b.type === "section") { |
| if (cur.head || cur.body.length) out.push(cur); |
| cur = { head: b as SectionBlock, body: [] }; |
| } else { |
| cur.body.push(b); |
| } |
| } |
| if (cur.head || cur.body.length) out.push(cur); |
| return out; |
| } |
|
|
| function ChartBlockView({ block, onOpen }: { block: ChartBlock; onOpen?: OnOpen }) { |
| const rule = block.drill; |
| const rowByX = new Map<string, DecompDescriptor>(); |
| if (isRowDrillRule(rule)) { |
| const xKey = (block.spec as { x?: string }).x; |
| for (const r of block.rows) { |
| const d = r[rule.row_key]; |
| const x = xKey ? r[xKey] : undefined; |
| if (x != null && d && typeof d === "object" && (d as DecompDescriptor).kind === "decomp") { |
| rowByX.set(String(x), d as DecompDescriptor); |
| } |
| } |
| } |
| |
| |
| const deltaByX = new Map<string, number>(); |
| if (block.delta_key) { |
| const xKey = (block.spec as { x?: string }).x; |
| for (const r of block.rows) { |
| const x = xKey ? r[xKey] : undefined; |
| const v = r[block.delta_key]; |
| if (x != null && typeof v === "number" && Number.isFinite(v)) deltaByX.set(String(x), v); |
| } |
| } |
| const pickable = !!onOpen && rowByX.size > 0; |
| return ( |
| <> |
| <SeriesChart |
| spec={block.spec as unknown as ChartSpec} |
| series={block.series ?? []} |
| fields={block.fields} |
| rows={block.rows as unknown as Row[]} |
| xOrder={block.x_order} |
| deltaByKey={deltaByX.size ? deltaByX : undefined} |
| pickable={pickable} |
| onPick={(x) => { |
| const d = rowByX.get(x); |
| if (d && onOpen) onOpen(d); |
| }} |
| /> |
| {/* Y1 rule 6 — even a chart discloses its cap. A trend that quietly drew |
| 12 of 18 months is the same defect as a truncated table. */} |
| {block.shown != null && block.total != null && block.total > block.shown ? ( |
| <p className="pg-shown is-capped"> |
| Showing {block.shown} of {block.total} periods |
| </p> |
| ) : null} |
| </> |
| ); |
| } |
|
|
| function BlockView({ block, onOpen }: { block: PageBlock; onOpen?: OnOpen }) { |
| switch (block.type) { |
| case "kpis": |
| return <KpiRow items={(block as KpisBlock).items} onOpen={onOpen} />; |
| case "table": { |
| const t = block as TableBlock; |
| return ( |
| <DataTable |
| columns={t.columns} |
| rows={t.rows} |
| drill={t.drill} |
| shown={t.shown} |
| total={t.total} |
| empty={t.empty} |
| download={t.download} |
| onOpen={onOpen} |
| /> |
| ); |
| } |
| case "chart": |
| return <ChartBlockView block={block as ChartBlock} onOpen={onOpen} />; |
| case "validation": |
| return <ValidationPanel checks={(block as ValidationBlock).checks} />; |
| default: |
| |
| |
| |
| return null; |
| } |
| } |
|
|
| export function PageView({ |
| page, |
| onControl, |
| onOpen, |
| busy, |
| }: { |
| page: PageEnvelope; |
| onControl?: (key: string, value: string) => void; |
| onOpen?: (d: DrillDescriptor) => void; |
| busy?: boolean; |
| }) { |
| const groups = group(page.blocks); |
| return ( |
| <div className="pg-page"> |
| <DashHeader |
| title={page.title} |
| asOf={page.as_of} |
| subtitle={page.subtitle} |
| controls={page.controls as PageControl[] | undefined} |
| onControl={onControl} |
| busy={busy} |
| /> |
| {groups.map((g, i) => |
| g.head ? ( |
| <Section key={g.head.key ?? `s${i}`} label={g.head.label} note={g.head.note}> |
| {g.body.map((b, j) => ( |
| <BlockView key={b.key ?? `b${j}`} block={b} onOpen={onOpen} /> |
| ))} |
| </Section> |
| ) : ( |
| <div key={`g${i}`}> |
| {g.body.map((b, j) => ( |
| <BlockView key={b.key ?? `b${j}`} block={b} onOpen={onOpen} /> |
| ))} |
| </div> |
| ) |
| )} |
| </div> |
| ); |
| } |
|
|