Skip to content

[feat]: Add --stack for 2D grouped bar/line charts #185

Description

@fahimfaisaal

Problem / use case

Today, 2D grouped charts render Y-series as side-by-side bars or lines within each X category. There is no way to show part-to-whole composition (stacked totals per X) in 2D mode.

Meanwhile, 3D charts already stack Z values on each (X, Y) cell via ECharts stack: 'z' in useBar3DChartOptions.ts. The 2D path has no equivalent — users who want stacked composition must leave vizb, misuse 3D, or use a heatmap (which answers a different question).

Common use case: sales.csv with -g region,category -p x,y — compare total revenue per region broken down by category, not just side-by-side category bars.

# Today: grouped side-by-side only
vizb bar sales.csv -g region,category -p x,y -o out.html

# Desired: one bar per region, categories stacked inside
vizb bar sales.csv -g region,category -p x,y --stack -o stacked.html

Proposed solution

Add an opt-in --stack boolean flag for 2D grouped bar and line charts (categorical x + y, no z). Default stays grouped (backward compatible).

Scope (v1)

In scope Out of scope
2D grouped mode: categorical x + y, no z 1D (no y), value mode, mixed mode
Bar + line Scatter, pie, heatmap, radar
CLI --stack, --chart bar:stack, --chart line:stack Changing default from grouped to stacked
UI toggle in settings panel Percent/normalized (100%) stack — future
3D path unchanged

Flag semantics

  • CLI: --stack (bool, default off)
  • JSON key: stack on BarConfig and LineConfig
  • Rules:
    • RequiresAxes("x", "y")
    • New ExcludesAxes("z") — skip flag with warning when z is present (3D owns stacking)
    • When scale=log and stack=true: skip stack with CLI warning (ECharts stacked bars/lines do not work on log axes)

Rendering (core change ~6 lines in two composables)

No transform/builder changes — data shape is identical; only ECharts series config changes.

Barui/src/composables/charts/useBarChartOptions.ts, when stack is true and hasYAxis:

stack: 'total',  // on each transposed Y series

Lineui/src/composables/charts/useCategorySeriesChartOptions.ts (line grouped path):

stack: 'total',
areaStyle: {},  // standard ECharts stacked-area look

Labels, tooltip, sort

  • Tooltip: createTooltipConfig already shows per-series values and Σ <xName> — works for stacks as-is.
  • Sort: existing X-axis sort by total across Y still makes sense.
  • Labels (--show-labels): v1 = per-segment labels (ECharts default). Follow-up: total label on top segment only (mirror create3DCellLabel for 3D).

Implementation checklist

Go

  • Add StackFlag in internal/charts/flag.go
  • Add ExcludesAxes("z") rule in internal/charts/rules.go
  • Add Stack *bool to internal/charts/bar/bar.go and internal/charts/line/line.go
  • Register flag on bar + line in cmd/charts/bar/bar.go and cmd/charts/line/line.go

UI

  • stack?: boolean on BarConfig / LineConfig in ui/src/types/index.ts
  • stack?: Ref<boolean> on BaseChartConfig; thread through useActiveChartShape, useChartOptions, ChartCard.vue
  • Apply stack: 'total' in useBarChartOptions.ts
  • Apply stack: 'total' + areaStyle: {} in useCategorySeriesChartOptions.ts (line only)
  • New StackControl.vue; entry in fieldRegistry.ts (appliesTo: ['bar', 'line'], appliesOn: ['2D'], hidden when rendering3D)
  • setStack in useSettingsStore.ts + handler in SettingsPanel.vue

Tests

  • Go: ApplyRules — stack kept for x+y, skipped for x+y+z and log+stack
  • UI vitest: useBarChartOptions / useCategorySeriesChartOptions emit stack: 'total' when enabled
  • Pipeline: --chart bar:stack round-trips through materialise (cmd/cli/pipeline_test.go)

Docs

  • docs/src/content/docs/charts/bar.mdx — 2D stacked example + settings table row
  • docs/src/content/docs/charts/line.mdx — same for stacked area

Example commands for docs:

vizb bar sales.csv -g region,category -p x,y --stack -o stacked.html
vizb line sales.csv -g region,category -p x,y --stack -o stacked-area.html

Architecture (today vs proposed)

flowchart LR
  subgraph today [Today 2D]
    X1["X category"] --> G1["Bar A"]
    X1 --> G2["Bar B"]
    X1 --> G3["Bar C"]
  end
  subgraph stacked [With --stack]
    X2["X category"] --> S1["Segment A"]
    S1 --> S2["Segment B"]
    S2 --> S3["Segment C"]
  end
Loading

Key files

Area Path
Bar config internal/charts/bar/bar.go
Line config internal/charts/line/line.go
Flag descriptors internal/charts/flag.go
Flag rules internal/charts/rules.go
2D bar render ui/src/composables/charts/useBarChartOptions.ts
2D line render ui/src/composables/charts/useCategorySeriesChartOptions.ts
3D stack (reference) ui/src/composables/charts/useBar3DChartOptions.ts
Settings registry ui/src/composables/settings/fieldRegistry.ts

Edge cases (document, don't over-engineer)

  • Negative values: ECharts stacks positives and negatives separately — acceptable default.
  • Single Y value: stack is a no-op visually; flag is harmless.
  • Many Y series: stacked bars can get hard to read — legend toggle still works.

Estimated diff

~15 files, ~200 lines — mostly plumbing; core render change is ~6 lines in two composables.

Alternatives considered

  • Keep grouped only — forces users to external tools or 3D for composition views.
  • Use heatmap — shows a grid of values, not stacked part-to-whole bars per category.
  • Default to stacked — changes visual semantics for all existing 2D charts; rejected in favor of opt-in.
  • 100% normalized stack — useful but a separate feature (--stack-percent or similar); defer to v2.
  • Scatter stacking — ECharts does not support stacked scatter; out of scope.

Metadata

Metadata

Assignees

Labels

cliCommand line interface related tasksenhancementNew feature or requestuiui-related tasks

Fields

Priority

Low

Start date

None yet

Target date

None yet

Projects

Status
Done

Relationships

None yet

Development

No branches or pull requests

Issue actions