Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions docs/spec-driven-countries.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,16 +209,19 @@ their IDs and raw values are humanized. When an age-band value lacks an
artifact label, range values such as `0_17` and `65_plus` become `0–17` and
`65+`.

A geography-role dimension sets `row.geography` and uses its declared `level`
or `"region"`. Other dimensions become `target_dimensions` with `key`,
A geography-role dimension sets `row.geography`, `row.geography_id`,
`row.geography_dimension_id`, and `row.geography_rank`, and uses its declared
`level` or `"region"`. A target may contain at most one populated
geography-role dimension. Other dimensions become `target_dimensions` with `key`,
`label`, `value`, `source_key`, `raw_value`, and an optional zero-based `rank`.
The published dimension ID, not its display label, determines `key`. Simple
lowercase IDs retain keys such as `bd_age_band`; IDs containing other
characters receive a lossless query-safe encoding. Consequently, two distinct
dimensions may share a display label without merging into one facet.
Facet values use rank order only when every displayed value has a rank;
otherwise the legacy facet sorter remains in force. Structured rows are also
excluded from whole-population estimate-scope inference.
Facet values, including geography values, use rank order only when every
displayed value has a rank; otherwise the legacy facet sorter remains in force.
Structured rows are also excluded from whole-population estimate-scope
inference.

### Target representation classification

Expand Down
2 changes: 1 addition & 1 deletion frontend/components/microcosm/calibration-explorer-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ export function CalibrationExplorerMap({
const upLabel = expandedView
? `Up to all ${data.currentLevel.label.toLowerCase()}`
: explorerUpLabel(state);
const breadcrumbs = explorerBreadcrumbs(state);
const breadcrumbs = explorerBreadcrumbs(state, data.pathLabels);
const selectedTarget = data.groups
.flatMap((group) => group.nodes)
.find((item) => item.kind === "target" && item.id === state.path.target)?.target;
Expand Down
56 changes: 53 additions & 3 deletions frontend/components/microcosm/calibration-explorer-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,18 @@ describe("calibration explorer presentation model", () => {
);
});

test("resolves program labels at the final presentation boundary", () => {
test("uses the program label resolved by the tree", () => {
expect(
explorerNodeLabel({
id: "taxable interest income",
label: "taxable interest income",
label: "Taxable interest income",
kind: "program",
}),
).toBe("Taxable interest income");
expect(
explorerNodeLabel({
id: "refundable ctc",
label: "refundable ctc",
label: "Refundable CTC",
kind: "program",
}),
).toBe("Refundable CTC");
Expand Down Expand Up @@ -246,6 +246,56 @@ describe("calibration explorer presentation model", () => {
});
});

test("prefers artifact source and category labels in breadcrumbs", () => {
expect(
explorerBreadcrumbs(
state({
source: "obr",
program: "efo_receipts",
geography: "United Kingdom",
dimensions: [],
}),
{
source: "Office for Budget Responsibility",
program: "EFO receipts",
},
),
).toEqual([
{ label: "All targets", path: { dimensions: [] } },
{
label: "Office for Budget Responsibility",
path: { dimensions: [] },
},
{
label: "EFO receipts",
path: {
source: "obr",
program: "efo_receipts",
dimensions: [],
},
},
{
label: "United Kingdom",
path: {
source: "obr",
program: "efo_receipts",
geography: "United Kingdom",
dimensions: [],
},
},
]);
});

test("preserves an artifact category label on a program tile", () => {
expect(
explorerNodeLabel({
id: "efo_receipts",
kind: "program",
label: "EFO receipts",
}),
).toBe("EFO receipts");
});

test("distinguishes an empty filtered result from an invalid hierarchy scope", () => {
expect(explorerEmptyMessage(state({ dimensions: [] }, true))).toContain(
"filters",
Expand Down
11 changes: 7 additions & 4 deletions frontend/components/microcosm/calibration-explorer-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ export interface ExplorerBreadcrumb {
path: ExplorerPath;
}

export function explorerBreadcrumbs(state: ExplorerState): ExplorerBreadcrumb[] {
export function explorerBreadcrumbs(
state: ExplorerState,
pathLabels: { source?: string; program?: string } = {},
): ExplorerBreadcrumb[] {
const crumbs: ExplorerBreadcrumb[] = [
{ label: "All targets", path: { dimensions: [] } },
];
Expand All @@ -65,14 +68,14 @@ export function explorerBreadcrumbs(state: ExplorerState): ExplorerBreadcrumb[]
}
if (state.path.source && state.path.program) {
crumbs.push({
label: sourceLabel(state.path.source),
label: pathLabels.source ?? sourceLabel(state.path.source),
path:
state.breakdown === "geography" && state.path.geography
? { geography: state.path.geography, dimensions: [] }
: { dimensions: [] },
});
crumbs.push({
label: programLabel(state.path.program),
label: pathLabels.program ?? programLabel(state.path.program),
path: {
source: state.path.source,
program: state.path.program,
Expand Down Expand Up @@ -183,7 +186,7 @@ export function explorerMapHeight(pageIntroHeight: number): string {
export function explorerNodeLabel(
node: Pick<CalibrationTreeNode, "id" | "kind" | "label">,
): string {
if (node.kind === "program") return programLabel(node.id);
if (node.kind === "program") return node.label || programLabel(node.id);
if (node.kind === "dimension_value") return canonicalLabel(node.label);
return node.label;
}
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ export function StagingTargetChangeMap({
}

const directions = layoutDirections(data, size.width, size.height, expanded);
const breadcrumbs = explorerBreadcrumbs(state);
const breadcrumbs = explorerBreadcrumbs(state, data.pathLabels);
const upLabel = expanded ? `Up to ${data.currentLevel.label.toLowerCase()}` : explorerUpLabel(state);

return (
Expand Down
1 change: 1 addition & 0 deletions frontend/lib/microcosm/calibration-prefetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function response(
return {
lossAttributionAvailable: false,
path: state.path,
pathLabels: {},
currentLevel: { kind: "overview", label: "Test" },
groups: [{ id: "test", label: "Test", nodes, metrics: EMPTY_METRICS }],
dimensionOrder: [],
Expand Down
25 changes: 25 additions & 0 deletions frontend/lib/microcosm/calibration-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,31 @@ describe("source, geography, and declared-dimension hierarchy", () => {
expect(tree.groups[0].label).toBe("Nova Statistics Agency");
});

test("retains artifact source and category labels throughout the selected path", () => {
const tree = buildCalibrationTree(
[
target("obr-income-tax", {
source: "obr",
source_label: "Office for Budget Responsibility",
variable: "efo_receipts",
variable_label: "EFO receipts",
geography: "United Kingdom",
}),
],
state({
source: "obr",
program: "efo_receipts",
dimensions: [],
}),
);

expect(tree.pathLabels).toEqual({
source: "Office for Budget Responsibility",
program: "EFO receipts",
});
expect(tree.groups[0].label).toBe("EFO receipts");
});

test("renders geography first, then programs grouped by their source", () => {
const overview = buildCalibrationTree(
rows,
Expand Down
42 changes: 41 additions & 1 deletion frontend/lib/microcosm/calibration-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ export interface CalibrationTreeResponse {
releaseId?: string;
lossAttributionAvailable: boolean;
path: ExplorerState["path"];
pathLabels: {
source?: string;
program?: string;
};
currentLevel:
| { kind: "overview"; label: string }
| { kind: "geography"; label: string }
Expand Down Expand Up @@ -484,6 +488,36 @@ function programGroups(rows: CalibrationTreeTarget[]): CalibrationTreeGroup[] {
);
}

function pathLabels(
rows: CalibrationTreeTarget[],
path: ExplorerState["path"],
): CalibrationTreeResponse["pathLabels"] {
if (!path.source) return {};
const sourceRows = rows.filter(
(row) => String(row.source ?? "other") === path.source,
);
const artifactSourceLabel = sourceRows
.map((row) => row.source_label?.trim())
.find((label): label is string => Boolean(label));
const labels: CalibrationTreeResponse["pathLabels"] = {
source: artifactSourceLabel ?? sourceLabel(path.source),
};
if (!path.program) return labels;
const programRows = sourceRows.filter((row) => programId(row) === path.program);
const artifactProgramLabels = [
...new Set(
programRows
.map((row) => row.variable_label?.trim())
.filter((label): label is string => Boolean(label)),
),
];
labels.program = programLabel(
path.program,
artifactProgramLabels.length === 1 ? artifactProgramLabels[0] : null,
);
return labels;
}

function geographyNodes(rows: CalibrationTreeTarget[]): CalibrationTreeNode[] {
const byGeography = groupRows(rows, geographyId);
return sortNodes(
Expand All @@ -508,6 +542,7 @@ export function buildCalibrationTree(
): CalibrationTreeResponse {
const chartRows = allRows.map(normalizeChartCalibrationStatus);
const { path } = state;
const selectedPathLabels = pathLabels(chartRows, path);
const options = filterOptions(chartRows);
const filteredRows = applyExplorerFilters(chartRows, state.filters);

Expand All @@ -517,6 +552,7 @@ export function buildCalibrationTree(
releaseId,
lossAttributionAvailable,
path,
pathLabels: selectedPathLabels,
currentLevel: { kind: "geography", label: "Geography" },
groups: [{
id: "geography",
Expand All @@ -542,6 +578,7 @@ export function buildCalibrationTree(
releaseId,
lossAttributionAvailable,
path,
pathLabels: selectedPathLabels,
currentLevel: { kind: "overview", label: "Programs" },
groups: programGroups(filteredGeographyRows),
dimensionOrder: [],
Expand All @@ -555,6 +592,7 @@ export function buildCalibrationTree(
releaseId,
lossAttributionAvailable,
path,
pathLabels: selectedPathLabels,
currentLevel: { kind: "overview", label: "Programs" },
groups: programGroups(filteredRows),
dimensionOrder: [],
Expand All @@ -575,10 +613,11 @@ export function buildCalibrationTree(
releaseId,
lossAttributionAvailable,
path,
pathLabels: selectedPathLabels,
currentLevel: { kind: "geography", label: "Geography" },
groups: [{
id: path.program,
label: path.program,
label: selectedPathLabels.program ?? programLabel(path.program),
nodes,
metrics: calibrationTreeMetrics(filteredProgramRows),
}],
Expand Down Expand Up @@ -677,6 +716,7 @@ export function buildCalibrationTree(
releaseId,
lossAttributionAvailable,
path,
pathLabels: selectedPathLabels,
currentLevel,
groups,
dimensionOrder,
Expand Down
Loading
Loading