Docs / Build Workflow

Visualization — bar

When to use bar

Bar is the right choice when the question is "how do these categories compare?". The category axis lists discrete things — products, regions, channels, statuses — and the value axis measures one or more numeric quantities for each. The same renderer covers four common shapes:

  • Single-series — one bar per category, one measure. The default.
  • Grouped — multiple bars per category, one per measure. Reach for it when the measures are on the same scale (revenue and refunds, for example).
  • Stacked — bars stacked on top of each other; the total height is the sum of all measures. Use chart.stack: normal.
  • Percent-stacked — every category rescaled to 100%, with the bars showing the share of each part. Use chart.stack: percent.
  • Dual-axis — two measures on different scales sharing one chart, one bound to a left y-axis and the other to a right y-axis. Set axis: right on the second series.
  • Combo (bar + line) — bars for one measure plus a line over the same categories for another (for example a cumulative-percent overlay). Set type: line on the line series; usually pair it with axis: right and chart.y2_axis.

Use line instead when the x-axis is ordered by time and the question is about trend. Use pie when there are very few categories (≤ 6) and the question is purely about composition. Use grid when the audience needs the actual numbers next to category names rather than a visual comparison.

Mapping

Two mapping fields drive the chart:

  • mapping.x — required. The categorical field. Each distinct value becomes one tick on the category axis. Use a string field for clean labels; numeric or date fields are formatted with the platform default unless you override via format.
  • mapping.series — required. Array. One entry per measure to plot. Each entry must declare a field; everything else is optional.
mapping:
  x: category
  series:
    - field: revenue

Use one entry for a simple chart, two or more for grouped or stacked, and the optional axis: right on a series to put it on the secondary y-axis.

Per-series options:

  • field — required. The numeric field from the query result that drives the bar height for this series.
  • label — string. Shown in the legend and tooltip. Defaults to the field name.
  • label_from_field — string. Pull the legend label from a field in the data instead of declaring it in YAML.
  • color — hex color for this series. Defaults to the next slot in the platform palette.
  • axis"left" (default) or "right". Right places the series on the secondary y-axis.
  • type"bar" (default) or "line". line draws the measure as a line over the same categories (combo chart). Line series never stack. Not combinable with chart.orientation: horizontal — validation rejects that combination.
# two measures on the same scale (grouped)
series:
  - field: revenue
    label: Revenue
  - field: refunds
    label: Refunds

# two measures on different scales (dual-axis)
series:
  - field: revenue
    label: Revenue
  - field: order_count
    label: Orders
    axis: right

# one measure with explicit color
series:
  - field: revenue
    label: Revenue
    color: "#6c47ff"

chart shortcuts

Top-level keys on the chart block. The chart block is typed and closed — anything not listed in this page is rejected at validation time.

  • chart.orientation"vertical" (default) or "horizontal". Pick horizontal when category labels are long, or when the audience reads top-to-bottom (rankings, leaderboards).
  • chart.stack"normal" (sum stacking) or "percent" (rescale every category to 100% and switch the value-axis labels to percentages). Omit for grouped bars.
  • chart.show_value_labels — boolean. Turns the value label on for every series. The label content is the bar's value, formatted by format; style it with chart.value_label below.
  • chart.cross_filter — boolean, default true. Set to false for charts that should always show the unfiltered view (e.g. a "total revenue" headline).
  • chart.height — pixel height of the viz container. Set explicitly when the chart needs more vertical room — for example, a horizontal bar with many categories.

Value labels

chart.value_label is an object applied to every series when chart.show_value_labels is on:

  • position — placement of the label relative to the bar. Common values: "top", "inside", "insideTop", "insideBottom", "outside".
  • rotate — degrees, between -90 and 90. Useful when the label is wider than the bar.
  • color, font_size, font_weight.
  • formatter — string template (no callbacks). Useful for adding a unit prefix or suffix (e.g. "{c}M").
  • distance, align ("left" | "center" | "right"), vertical_align ("top" | "middle" | "bottom"), clip.
# compact label inside the bar with white text
chart:
  show_value_labels: true
  value_label:
    position: inside
    color: "#fff"
    font_size: 11

# label above each vertical bar with currency formatter
chart:
  show_value_labels: true
  value_label:
    position: top
    formatter: "${c}"
    distance: 4

# rotated labels for narrow bars
chart:
  show_value_labels: true
  value_label:
    rotate: -90
    position: insideBottom

Legend & tooltip

chart.legend:

  • show — boolean. Set to false when the chart has a single series and the legend is redundant.
  • position — shortcut: "top", "bottom", "left", "right", "top-left", "top-right", "bottom-left", "bottom-right".
  • orient"horizontal" | "vertical". Use to override the orient derived from position.
  • top / bottom / left / right — pixel number or percent string for direct positioning.
  • text_style — text styling: color, font_style, font_weight, font_family, font_size, line_height.
  • item_width, item_height, item_gap — pixel sizes for the colored swatches and gaps.

chart.tooltip:

  • show — boolean.
  • trigger"item" (one bar at a time, default for single-series), "axis" (group-wise; preferred when there are multiple series so hover compares them), or "none".
  • confine — boolean. Keep the tooltip inside the chart bounds.
  • formatter — string template. Use placeholders such as {a} (series), {b} (category), {c} (value).
  • background_color, border_color, border_width.
  • padding — single number or array of 2 to 4 numbers (top/right/bottom/left).
  • text_style — same shape as on the legend.
  • axis_pointer.type"line" | "shadow" | "none" | "cross". Common bar choice: "shadow".

Axes

chart.x_axis, chart.y_axis, and chart.y2_axis share the same shape for value-axis blocks (y2_axis configures the right axis when a series uses axis: right; with one extra on x_axis):

  • name — axis title.
  • name_location"start" | "middle" | "center" | "end".
  • name_gap — pixels between axis line and name.
  • min, max — numeric axis bounds. Use y2_axis.max: 1 to pin a cumulative-percent line to a 0–100% domain when values are stored as 0–1.
  • axis_label.show — boolean.
  • axis_label.rotate — degrees, -90 to 90. Use to keep long category names from overlapping.
  • axis_label.interval — integer ≥ 0 (skip every N labels) or "auto".
  • axis_label.color, axis_label.font_size, axis_label.font_weight.
  • axis_label.formatter — string template.
  • axis_label.max_chars — integer ≥ 1. Truncates labels with an ellipsis.
  • x_axis.visible_window — integer ≥ 1. Restricts the visible category count and enables a horizontal range slider; useful when there are many categories.
# long category names with rotation and ellipsis
chart:
  x_axis:
    axis_label:
      rotate: -30
      max_chars: 14

# named y-axis with a percent formatter
chart:
  y_axis:
    name: Margin
    name_gap: 28
    axis_label:
      formatter: "{value}%"

# data-zoom slider for many categories
chart:
  x_axis:
    visible_window: 12

format

Number formatting is field-keyed at the top level of the viz YAML. Per-field patterns win over the root pattern. Common patterns for a bar chart:

format:
  revenue: "$#,##0.00"     # full currency
  revenue: "$#,##0a"       # abbreviated: $1.2M, $340K
  order_count: "#,##0"     # integer with grouping
  margin_pct: "#,##0.00%"  # percentage

Format applies to value labels, tooltips, and value-axis tick labels. The full pattern grammar lives on the viz-types overview.

Cross-filter behavior

Inside a dashboard, clicking a bar adds a "pill" that narrows every other viz on the page to the clicked category. Full mechanism at Cross-filtering. Bar specifics:

  • The clicked bar's category becomes the cross-filter value.
  • If the dashboard's models do not declare a parameter named after the clicked field, the click is silently ignored. To make a column cross-filterable, declare a parameter for it in the model that powers the affected charts.
  • Disable per viz with chart.cross_filter: false. Useful for "headline" charts that should always show totals.
  • To highlight a specific bar from an external value, use the top-level emphasis block:
    emphasis:
      field: category
      value_from_param: highlight_category
      bar_color: "#6c47ff"
    The bar whose category equals the runtime value of highlight_category is colored with bar_color.

Worked examples

Simple comparison:

id: ec_revenue_by_category_bar
title: Revenue by Category
query: "models/ec_revenue.malloy::by_category"
type: bar
mapping:
  x: category
  series:
    - field: revenue
      label: Revenue
chart:
  height: 320
  show_value_labels: true
  value_label:
    position: top
  x_axis:
    axis_label:
      rotate: -30
      max_chars: 14
  y_axis:
    name: Revenue
format:
  revenue: "$#,##0.00"
published: true

Grouped (two measures, same scale):

id: ec_revenue_vs_refunds_bar
title: Revenue vs Refunds
query: "models/ec_revenue.malloy::revenue_vs_refunds"
type: bar
mapping:
  x: category
  series:
    - field: revenue
      label: Revenue
      color: "#6c47ff"
    - field: refunds
      label: Refunds
      color: "#fb7185"
chart:
  legend:
    show: true
    position: top
  tooltip:
    trigger: axis
    axis_pointer:
      type: shadow
format:
  revenue: "$#,##0.00"
  refunds: "$#,##0.00"
published: true

Percent-stacked composition:

id: ec_channel_mix_bar
title: Channel Mix per Category
query: "models/ec_revenue.malloy::channel_mix"
type: bar
mapping:
  x: category
  series:
    - field: rev_direct
      label: Direct
    - field: rev_organic
      label: Organic
    - field: rev_paid
      label: Paid
chart:
  stack: percent
  show_value_labels: true
  value_label:
    position: inside
    color: "#fff"
  legend:
    show: true
    position: top
format:
  rev_direct: "#,##0.0%"
  rev_organic: "#,##0.0%"
  rev_paid: "#,##0.0%"
published: true

Pareto (sorted bars + cumulative-percent line on the right axis). Build a Malloy query on bigquery-public-data.thelook_ecommerce that returns each category's count ordered descending plus a cumulative-percent column (values 0–1). Map the count as bars and the cumulative percent as a type: line series on axis: right:

id: ec_return_reason_pareto
title: Return reasons — Pareto
query: "models/ec_returns.malloy::by_reason_pareto"
type: bar
mapping:
  x: return_reason
  series:
    - field: return_count
      label: Returns
    - field: cumulative_pct
      label: Cumulative %
      type: line
      axis: right
      color: "#6c47ff"
chart:
  y2_axis: { max: 1 }
  legend: { show: true }
format:
  return_count: "#,##0"
  cumulative_pct: "#0%"
published: true

Push from your workspace with looky push -w <workspace_slug> after the model and viz YAML exist under content/visualizations/.

Dual-axis (revenue vs orders):

id: ec_revenue_orders_bar
title: Revenue and Orders
query: "models/ec_revenue.malloy::by_month"
type: bar
mapping:
  x: order_month
  series:
    - field: revenue
      label: Revenue
    - field: order_count
      label: Orders
      axis: right
chart:
  legend:
    show: true
    position: top
  y_axis:
    name: Revenue
format:
  revenue: "$#,##0"
  order_count: "#,##0"
published: true

Horizontal ranking with many categories:

id: ec_top_brands_bar
title: Top Brands by Revenue
query: "models/ec_revenue.malloy::by_brand_desc"
type: bar
mapping:
  x: brand
  series:
    - field: revenue
      label: Revenue
chart:
  orientation: horizontal
  height: 540
  show_value_labels: true
  value_label:
    position: right
  x_axis:
    axis_label:
      max_chars: 18
  y_axis:
    visible_window: 25
format:
  revenue: "$#,##0a"
published: true

Note: with horizontal orientation, the long category names live on the y-axis, so the y-axis's axis_label gets max_chars, and visible_window moves to y_axis.

Common pitfalls

  • Percent stack is not adding to 100%. Make sure every series in mapping.series[] represents a part of the same whole. If one of the measures is on a different scale, the chart shows what you asked for but it is not meaningful as a composition.
  • Dual-axis labels collide. Two value scales need room. Either reduce the data density (fewer categories), set explicit name_gap on both axes, or split into two charts.
  • Long category labels wrap or overlap. Add chart.x_axis.axis_label.rotate: -30 and max_chars: 14, or switch to chart.orientation: horizontal (horizontal bars cannot include type: line series — use vertical combo charts for Pareto-style overlays).
  • Cross-filter clicks have no effect. The clicked field must be declared as a parameter in at least one model used by the dashboard. Without that, clicks are silently ignored.
  • Value labels are clipped on small bars. Set chart.value_label.clip: false, or move the position to "top" / "outside", or reduce font_size.
  • Legend takes too much space. Use position: bottom or set show: false when there is only one series.
  • Too many categories blow up the layout. Use chart.x_axis.visible_window to enable a slider, or sort and limit in the Malloy query so the chart shows the top N.
  • Color is per series, not per individual bar. To highlight one specific bar, use the top-level emphasis block (see above) instead of trying to color a category directly.