Skip to content
Merged
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
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/charts/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/charts/src/core/definition/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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 } : {}),
}
Expand Down
22 changes: 22 additions & 0 deletions packages/charts/src/core/layout/charts/stackedDiscreteBar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down
17 changes: 16 additions & 1 deletion packages/charts/src/core/layout/charts/stackedDiscreteBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions packages/charts/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading