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: righton 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: lineon the line series; usually pair it withaxis: rightandchart.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 viaformat.mapping.series— required. Array. One entry per measure to plot. Each entry must declare afield; 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".linedraws the measure as a line over the same categories (combo chart). Line series never stack. Not combinable withchart.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 byformat; style it withchart.value_labelbelow.chart.cross_filter— boolean, defaulttrue. Set tofalsefor 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 tofalsewhen 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 fromposition.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. Usey2_axis.max: 1to 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
emphasisblock:
The bar whoseemphasis: field: category value_from_param: highlight_category bar_color: "#6c47ff"categoryequals the runtime value ofhighlight_categoryis colored withbar_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_gapon both axes, or split into two charts. - Long category labels wrap or overlap. Add
chart.x_axis.axis_label.rotate: -30andmax_chars: 14, or switch tochart.orientation: horizontal(horizontal bars cannot includetype: lineseries — 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 reducefont_size. - Legend takes too much space. Use
position: bottomor setshow: falsewhen there is only one series. - Too many categories blow up the layout. Use
chart.x_axis.visible_windowto 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
emphasisblock (see above) instead of trying to color a category directly.