From 7e118bf0466c6d9edef55bbd8038c0f138c8b97e Mon Sep 17 00:00:00 2001 From: xrendan Date: Mon, 7 Sep 2026 20:09:19 -0600 Subject: [PATCH 1/2] Add grouped row spacing to stacked discrete bars --- packages/charts/src/core/definition/schema.ts | 4 ++++ .../layout/charts/stackedDiscreteBar.test.ts | 22 +++++++++++++++++++ .../core/layout/charts/stackedDiscreteBar.ts | 17 +++++++++++++- packages/charts/src/core/types.ts | 5 +++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/charts/src/core/definition/schema.ts b/packages/charts/src/core/definition/schema.ts index fa34e51bfcb..38d4c557e7c 100644 --- a/packages/charts/src/core/definition/schema.ts +++ b/packages/charts/src/core/definition/schema.ts @@ -154,6 +154,8 @@ const definitionSchema = z.object({ missingData: z.enum(["auto", "hide", "show"]).default("auto"), comparisonLines: z.array(comparisonLineSchema).optional(), seriesStrategy: z.enum(["entity", "metric"]).optional(), + rowGroupBreaks: z.array(z.string()).optional(), + rowGroupGap: z.number().min(0).max(4).optional(), hideLegend: z.boolean().default(false), hideSeriesLabels: z.boolean().default(false), @@ -322,6 +324,8 @@ export function parseDefinition(raw: unknown): ParseDefinitionResult { ...(parsed.sort !== undefined ? { sort: parsed.sort } : {}), ...(parsed.comparisonLines !== undefined ? { comparisonLines: parsed.comparisonLines } : {}), ...(parsed.seriesStrategy !== undefined ? { seriesStrategy: parsed.seriesStrategy } : {}), + ...(parsed.rowGroupBreaks !== undefined ? { rowGroupBreaks: parsed.rowGroupBreaks } : {}), + ...(parsed.rowGroupGap !== undefined ? { rowGroupGap: parsed.rowGroupGap } : {}), ...(parsed.theme !== undefined ? { theme: parsed.theme } : {}), ...(parsed.locale !== undefined ? { locale: parsed.locale } : {}), } diff --git a/packages/charts/src/core/layout/charts/stackedDiscreteBar.test.ts b/packages/charts/src/core/layout/charts/stackedDiscreteBar.test.ts index 310281b3a5c..73055f32b70 100644 --- a/packages/charts/src/core/layout/charts/stackedDiscreteBar.test.ts +++ b/packages/charts/src/core/layout/charts/stackedDiscreteBar.test.ts @@ -126,6 +126,28 @@ describe("stacked discrete bar sorting and totals", () => { }) }) +describe("stacked discrete bar row grouping", () => { + it("inserts extra space only after configured entity rows", () => { + const layer = layoutStackedDiscreteBar( + ctxFor("provincial-budgets", { + y: ["program_spending", "debt_charges"], + selectedEntities: ["Ontario", "Quebec", "Nova Scotia"], + time: "2023-24", + types: ["stacked-discrete-bar"], + sort: { by: "custom", order: "asc" }, + rowGroupBreaks: ["Quebec"], + rowGroupGap: 1, + }), + AREA, + OPTS, + ) + const labels = layer.nodes + .filter((node) => node.kind === "text" && node.key.startsWith("label/")) + .map((node) => (node.kind === "text" ? node.position.y : 0)) + expect(labels[2] - labels[1]).toBeCloseTo(2 * (labels[1] - labels[0])) + }) +}) + describe("stacked bar both-direction stacking (spec 15)", () => { it("stacks mixed-sign series without negatives offsetting positives", () => { const ctx = ctxFor("pathological", { diff --git a/packages/charts/src/core/layout/charts/stackedDiscreteBar.ts b/packages/charts/src/core/layout/charts/stackedDiscreteBar.ts index 1b43ae3b925..3bd1797fc6d 100644 --- a/packages/charts/src/core/layout/charts/stackedDiscreteBar.ts +++ b/packages/charts/src/core/layout/charts/stackedDiscreteBar.ts @@ -224,7 +224,22 @@ export function layoutStackedDiscreteBar(ctx: LayoutContext, area: Rect, opts: C }) // --- Rows ---------------------------------------------------------------------------- - const rows = bandPositions(bars.length, [plotArea.y, plotArea.y + plotArea.height], 1) + const breaks = new Set(ctx.definition.rowGroupBreaks ?? []) + const groupGap = ctx.definition.rowGroupGap ?? 0.75 + const breakCount = bars.filter((bar, index) => index < bars.length - 1 && breaks.has(bar.entity)).length + const rows = + breakCount === 0 || groupGap === 0 + ? bandPositions(bars.length, [plotArea.y, plotArea.y + plotArea.height], 1) + : (() => { + const slot = plotArea.height / (bars.length + breakCount * groupGap) + let cursor = 0 + return bars.map((bar, index) => { + const center = plotArea.y + (cursor + 0.5) * slot + cursor += 1 + if (index < bars.length - 1 && breaks.has(bar.entity)) cursor += groupGap + return { start: center - slot / 2, center, width: slot } + }) + })() const barHeight = Math.min(Math.max((rows[0]?.width ?? plotArea.height) * 0.7, BAR_HEIGHT_FLOOR), BAR_HEIGHT_MAX) const targets: HitTarget[] = [] const t = strings(locale) diff --git a/packages/charts/src/core/types.ts b/packages/charts/src/core/types.ts index 75881a7b14d..8c95805e129 100644 --- a/packages/charts/src/core/types.ts +++ b/packages/charts/src/core/types.ts @@ -305,6 +305,11 @@ export interface ChartDefinition { comparisonLines?: ComparisonLine[] seriesStrategy?: SeriesStrategy + /** Stacked discrete bars: insert extra vertical space after these entity rows. */ + rowGroupBreaks?: string[] + /** Extra space at each row-group break, measured in normal row slots. Default 0.75. */ + rowGroupGap?: number + hideLegend: boolean hideSeriesLabels: boolean hideRelativeToggle: boolean From c7efe333c81a6768dba56a22c369e714cb8e9d74 Mon Sep 17 00:00:00 2001 From: xrendan Date: Mon, 7 Sep 2026 20:19:43 -0600 Subject: [PATCH 2/2] Bump charts package to 1.1.0 --- bun.lock | 2 +- packages/charts/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index 29fa171a4ab..1bdc5cf3dc4 100644 --- a/bun.lock +++ b/bun.lock @@ -346,7 +346,7 @@ }, "packages/charts": { "name": "@buildcanada/charts", - "version": "1.0.0", + "version": "1.1.0", "bin": { "charts": "./dist/cli/index.js", }, diff --git a/packages/charts/package.json b/packages/charts/package.json index 96e29487f22..691b89c003c 100644 --- a/packages/charts/package.json +++ b/packages/charts/package.json @@ -1,6 +1,6 @@ { "name": "@buildcanada/charts", - "version": "1.0.0", + "version": "1.1.0", "description": "Build Canada charts: pure layout core with a single React SVG renderer, deterministic headless rendering, and a CLI.", "type": "module", "main": "./dist/index.js",