Bar Charts¶
Bar charts encode values as lengths measured from a common baseline, which makes them one of the most fundamental tools for comparison; this family includes horizontal bar charts and vertical column charts. Horizontal bars are especially good for ranking categories with long labels, while vertical columns are especially good for showing change over simple time steps.
dbt Charts bar charts use a small set of top-level shorthand fields together with style. By default, style.orientation is auto: dbt Charts picks horizontal for categorical x (nominal/string labels) and vertical for continuous x (temporal, quantitative, date-like ordinal, or time-unit bucketed). The rule is type-driven, not viewport-driven: orientation reflects what the data is, not how wide the chart happens to be. Set style.orientation: vertical or style.orientation: horizontal to lock in a specific orientation. See Axis Labels: Smart Layout for how the engine picks orientation and label tilt automatically when style.orientation is left at its default.
A chart's control surface is the full set of authored properties available on a single chart. In dbt Charts, that surface is primarily top-level chart fields plus a typed style object.
This page is intentionally family-oriented rather than exhaustive. For the implementation-backed source of truth, including default ownership and lower-level property coverage, see the YAML Schema Reference.
Minimum Required for a Bar Chart¶
These are the minimum fields required to render a basic bar chart in dbt Charts. With a nominal x-axis (category strings), the default result is a horizontal bar chart; with a continuous x-axis (temporal, quantitative, or date-like ordinal), the default is a vertical column chart. See Axis, Scale, and Orientation for the rule, or set style.orientation explicitly to override.
| dbt Charts field | Maps to Vega-Lite | Allowed values | Notes |
|---|---|---|---|
query |
data.values |
Query name or query reference | Supplies the dataset. |
type: bar |
mark.type: "bar" |
Literal bar |
Selects the bar mark. |
x |
encoding.x.field |
Field name | Category, discrete field, or temporal field. |
y |
encoding.y.field |
Field name | Numeric measure to compare. |
Minimum Example¶
source: examples_db
charts:
product_revenue_columns:
query:
sql: |
SELECT product, category, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY product, category ORDER BY revenue DESC
type: bar
title: Revenue by Product
x: product
y: revenue
rows:
- product_revenue_columns
Top-Level Chart Fields¶
These are the top-level chart properties you set directly on a bar chart before you get into nested properties under style.
| dbt Charts field | Maps to Vega-Lite | Allowed values | Notes |
|---|---|---|---|
query |
data.values |
Query name or query reference | Query results become the plotted dataset. Use pre-aggregated data with one row per plotted key. |
type: bar |
mark.type: "bar" |
Literal bar |
Standard bar or column chart. |
x |
encoding.x.field |
Field name | Usually the category field, or a temporal field for time-unit column charts. |
y |
encoding.y.field |
Field name or list of field names | Usually the numeric measure, even when the final chart is horizontal. A list creates layered multi-metric bars. |
title |
title.text |
String | Chart title. |
notes |
metadata | String | AI search/context metadata. Never rendered. |
color |
encoding.color.field |
Field name | Adds series grouping or composition. |
x_label |
encoding.x.title |
String | Custom x-axis title. |
y_label |
encoding.y.title |
String | Custom y-axis title. |
field_labels |
legend, tooltip and axis titles | Map of column to text | Display text for a column, painted as written. See Field Labels. |
format |
quantitative value-axis format | Format string | Numeric formatting for the value axis. In horizontal bars, that value axis is rendered on x. |
sort |
categorical axis sort | Sort object | Most useful for ranked bar charts. |
Vertical Columns and Horizontal Bars¶
This example shows the family's two main forms side-by-side: a vertical bar chart (also called a column chart), and the same data rendered horizontally. Both pin style.orientation explicitly so the contrast is what's on the page; left at auto, this nominal x would render horizontal by default.
source: examples_db
queries:
sales_by_product:
sql: |
SELECT product, category, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY product, category ORDER BY revenue DESC
charts:
product_revenue_columns:
query: sales_by_product
type: bar
title: Revenue by Product
x: product
y: revenue
style:
orientation: vertical
product_revenue_bars:
query: sales_by_product
type: bar
title: Revenue by Product
x: product
y: revenue
style:
orientation: horizontal
rows:
- product_revenue_columns
- product_revenue_bars
Labels and Ranking¶
This example keeps the chart definition compact while showing the top-level fields most readers use in practice: labels, formatting, and sorting.
source: examples_db
charts:
ranked_products:
query:
sql: |
SELECT product, category, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY product, category ORDER BY revenue DESC
type: bar
title: Top Products by Revenue
notes: A sorted product ranking shown as horizontal bars.
x: product
y: revenue
x_label: Product
y_label: Revenue
sort:
by: revenue
order: desc
style:
number_format: currency_whole
orientation: horizontal
rows:
- ranked_products
What sort: Does When a Category Holds Several Rows¶
sort: orders categories, and a category with a color series or a y: [...] list holds more than one row, so the sort column is folded to a single value per category before the categories can be ranked. Two rules cover it:
- A stacked bar sorted by its own measure ranks by the stacked total. That is the only reading of a
sort:namingrevenueon astack: zerochart whoseyisrevenue: the bar's length is the total. Ay: [...]list does not qualify, since no single column is the measure there. - Every other sort ranks by the column's own value. A sort by an ordering key (
release_seq,stage_rank) ranks by that key whatever the row count; a category holding one series does not sort ahead of a category holding three. The same holds for a grouped bar sorted by its measure: the fold follows whether the chart stacks, not how the bars are drawn, so withstack: nonethe categories rank by each one's smallest series rather than by the group's total. Sort by a pre-summed column in the query when you want the group total.
sort.by must name a column on every row of the query result; a missing column fails with ERR-SORT-FIELD-NOT-FOUND rather than rendering in natural order.
The rule is the same in both orientations, and the key column does not need to be a measure: a text or date column is an ordering too. On a multi-series bar it does cost the endpoint-label rail, which steps aside to a legend for a non-numeric sort: (see When a bar falls back to a legend instead).
Layered Multi-Metric Bars¶
When y is a list, dbt Charts creates a layered bar chart. This is most useful when the metrics share a similar scale and the comparison is more important than stacking.
queries:
product_metrics:
columns: [product, revenue, profit]
values:
- ["Gadget X", 18350, 4200]
- ["Gadget Y", 30490, 7600]
- ["Tool Z", 34900, 9100]
- ["Widget A", 66300, 18500]
- ["Widget B", 85680, 24100]
charts:
layered_product_metrics:
query: product_metrics
type: bar
title: Revenue and Profit by Product
x: product
y: [revenue, profit]
rows:
- layered_product_metrics
Style Fields¶
Use style for dbt Charts shorthand properties that affect presentational defaults such as legend visibility and grid lines.
| dbt Charts field | Maps to Vega-Lite | Allowed values | Notes |
|---|---|---|---|
style.legend.visible |
encoding.color.legend |
true or false |
Typed legend control. false hides the legend. |
style.axis.grid.visible |
axis grid visibility | true or false |
false hides grid lines. Use style.axis.grid.visible: false (nested under grid:). |
style.background |
chart SVG background wrapper | Color value | Applies a chart-level background fill behind the rendered SVG. |
Legend and Grid Lines¶
This example shows the small style surface for bar charts without changing the underlying data bindings.
source: examples_db
charts:
category_columns_styled:
query:
sql: |
SELECT category, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC
type: bar
title: Revenue by Category
x: category
y: revenue
color: category
style:
stack: zero
legend:
visible: false
axis:
grid:
visible: false
rows:
- category_columns_styled
Axis, Scale, and Orientation¶
All presentation config lives under style. This is where the family distinguishes default vertical columns from horizontal bars.
| dbt Charts field | Maps to Vega-Lite | Allowed values | Notes |
|---|---|---|---|
style.orientation |
bar orientation transform | auto (default), vertical, or horizontal |
auto picks horizontal for categorical x, vertical for continuous x (temporal/quantitative/date-like ordinal); vertical and horizontal lock the orientation. |
style.axis_x |
config.axisX / encoding.x.axis |
AxisStyle object |
Per-axis styling (format, ticks, labels, grid, title). |
style.axis_y |
config.axisY / encoding.y.axis |
AxisStyle object |
Per-axis styling. |
style.axis_y.scale |
config.axisY.scale |
ScaleStyle object |
Y-axis scale config (zero, nice, domain, clamp). Use axis_x.scale for categorical band padding. |
style.stack |
encoding.y.stack |
"none", "zero", "normalize", "center" |
Stacking behavior. Omit stack when a color field is present to get the default side-by-side grouped layout. Set "none" for grouped (side-by-side), "zero" for stacked-from-baseline, "normalize" for 100% stacked, "center" for diverging stacks. (Within-group spacing of grouped bars is controlled by style.overlap, not stack.) |
style.stack_order |
encoding.order |
"value" (default), "data", "alphabetical" |
Z-order of stacked segments. "value" puts the largest aggregate at the baseline. "data" stacks series in first-appearance order: SQL row order for color, the listed order for y: [a, b, ...] (a measure null in the first rows appears later). "alphabetical" sorts by series name. Ignored when stacking is off or there is only one series. |
style.overlap |
bar xOffset/yOffset spacing |
"auto" (default), "none", "flush", "partial", "full", or a number |
Within-group spacing for grouped bars (a color field, not stacked). "auto" adapts to the series count: partial (25% overlap) at 2 series, none (small gap) at 3+. "none" gaps, "flush" touches, "partial" overlaps 25%, "full" coincides. A number is a fraction of bar width: >0 overlaps, 0 touches, <0 gaps. Positive overlap is only valid for 2 series: overlapping 3+ series buries the middle one, so it raises an error (use none/flush for 3+, or rely on auto). Setting any overlap with an active stack mode ("zero"/"normalize"/"center") is also an error. |
style.marks.bar.band_width |
mark.width.band |
float (fraction of the band, 0–1) |
Fraction of the available band each bar fills. Lower it to thin bars out and open up whitespace between categories without changing overlap, which only governs spacing within a grouped-color band. |
Bar Width¶
This example thins the bars to 40% of their band, leaving visible whitespace between categories.
source: examples_db
charts:
product_revenue_thin:
query:
sql: |
SELECT product, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY product ORDER BY revenue DESC
type: bar
title: Product Revenue
x: product
y: revenue
style:
marks:
bar:
band_width: 0.4
rows:
- product_revenue_thin
Orientation and Axis Controls¶
This example uses style to turn a column chart into a horizontal ranking chart while also adjusting axis formatting and label space.
source: examples_db
charts:
product_revenue_ranked:
query:
sql: |
SELECT product, category, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY product, category ORDER BY revenue DESC
type: bar
title: Product Revenue Ranking
x: product
y: revenue
sort:
by: revenue
order: desc
style:
orientation: horizontal
number_format: currency_whole
axis_x:
labels:
max_width: 140
axis_y:
scale:
nice: true
rows:
- product_revenue_ranked
Endpoint Labels and the Top-Row Series Rail¶
A multi-series bar names its series on the chart, anchored to each segment, rather than in a side legend. This is the default on every built-in theme; you don't switch it on. Multi-series bars come from either a color channel or a y: [field_a, field_b] list; both layouts render the endpoint rail. Where the labels go depends on the orientation:
- Vertical stacked column charts get one label per segment in a side pane, anchored to each segment's vertical midpoint in the last column that series appears in (the rightmost, for a series that runs the full width). The chart pane and label pane sit side-by-side in an
hconcat. - Horizontal stacked bar charts get a single rail of labels above the top row of the chart: one label per series, centered horizontally on its segment's midpoint within the top categorical row. The chart pane and rail sit stacked in a
vconcat. The bottom row's value axis still carries the numeric measure labels.
The asymmetry follows from the reading direction: vertical stacks read top-to-bottom along the measure axis, so per-segment side labels feel natural; horizontal stacks read left-to-right inside one row, so a single label rail above the top row is the direct equivalent.
| dbt Charts field | Allowed values | Notes |
|---|---|---|
style.endpoint_labels.visible |
true or false |
Defaults to true on every built-in theme. Set false per-chart to send the series names back to a legend; set true to force the rail onto a shape it steps aside from (below). |
When the label pane or rail is on, the categorical legend turns off automatically: the two would encode the same series→color mapping twice. On the vertical layout, the y-axis also auto-flips to the left so it doesn't collide with the right-edge label pane.
On narrow cards, vertical endpoint-label text compacts to fit the available width while its automatic vertical spacing stays readable for closely ending series.
The horizontal rail has no per-label collision resolver: it measures the real, rendered position and width of every label (the actual font, at the actual pixel scale the chart draws at), and if any two adjacent labels would touch, the whole chart falls back to a legend instead of drawing a rail with overlapping text. See "When a bar falls back to a legend instead" below.
When a bar falls back to a legend instead¶
The rail has to be able to name every series, or it names some of them and quietly misleads. An authored sort: is fine when it sorts by a measure: each rail anchors on the rendered domain, so it follows the sort rather than refusing it. Sorting by a non-numeric column is the exception: the rail places its labels from the order the sorted axis draws, dbt Charts confirms that order only for a numeric sort column, and labels placed from the wrong order name series in the wrong place, so the chart takes a legend instead. Where it can't, the chart keeps its legend, placed above the plot, untitled, reading in the same order as the bars. A bar steps aside from direct labeling when:
- It's grouped: explicit
stack: none, or the grouped-by-default whenstackis omitted and acolorfield is present. Every series' last bar rises from the same baseline to a similar height, so the labels collide into one another. - It has no
colorencoding and noy: [...]list: a single series has nothing to name. (y: [...]and acolor:column compose: one series per column value per measure, named<value> - <measure>; see Multiple Measures by a Dimension.layers:still cannot combine withy: [...].) color:names the same field asx:, or a series is missing from the column the rail anchors on (a trailing null, say). Either way some series would go unnamed.- It carries
multiples:: see Small Multiples. - The measure goes negative: a stacked segment crossing zero has no meaningful cumulative midpoint to anchor to.
- It's horizontal and
stack: center: that rail anchors on the cumulative0..Σaxis, which the diverging center-stack domain doesn't have. - It has
layers:: the rail walks the base chart's own series, so an overlay layer would be left anonymous. - It's horizontal and the rail's own labels would collide: narrow segments, long series names, or too many series for the card width can make adjacent labels overlap. dbt Charts measures the real rendered text rather than guessing, and steps aside to a legend rather than ship a colliding rail.
Writing style.endpoint_labels.visible: true on the chart itself overrides all of these: an author who asks for the rail on one of these shapes gets it, or gets a rendering error naming the conflict where the shape genuinely can't carry it.
Vertical Stacked Columns¶
source: examples_db
charts:
region_revenue_by_category_vertical:
query:
sql: |
SELECT region, category, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY region, category ORDER BY region, category
type: bar
title: Revenue by Region (vertical stack)
x: region
y: revenue
color: category
style:
stack: zero
orientation: vertical
endpoint_labels:
visible: true
rows:
- region_revenue_by_category_vertical
Horizontal Stacked Bars¶
source: examples_db
charts:
region_revenue_by_category:
query:
sql: |
SELECT region, category, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY region, category ORDER BY region, category
type: bar
title: Revenue by Region (stacked by Category)
x: region
y: revenue
color: category
style:
stack: zero
orientation: horizontal
endpoint_labels:
visible: true
rows:
- region_revenue_by_category
Value Labels¶
Show the numeric value above (or inside) each bar by enabling style.marks.bar.labels.visible.
source: examples_db
charts:
product_revenue_labeled:
query:
sql: |
SELECT product, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY product ORDER BY revenue DESC
type: bar
title: Product Revenue
x: product
y: revenue
style:
marks:
bar:
labels:
visible: true
position: top # above | top | middle | middle_aligned | bottom
format: ",.0f" # omit to inherit axis_quantitative.format (the measure-axis format, for example, ".3~s")
font:
color: "#6b7280"
size: 11
rows:
- product_revenue_labeled
Position values:
| Value | Placement |
|---|---|
above |
Outside, past the bar end |
top |
Just inside the bar's end, the theme default |
middle |
Centered in the bar |
middle_aligned |
Every label at one shared height, so a row of them reads as a line |
bottom |
Just inside the baseline end |
On a stacked bar, middle centers each label in its own segment rather than in the whole stack; use it when every segment carries a value. above has no room above an individual segment, so it renders as top. middle_aligned is not meaningful per-segment and raises an error; use middle.
On vertical bars, a label whose segment is too short to hold it is dropped rather than drawn across its neighbors, so a stack with one thin band shows the labels that fit and omits the one that does not.
dx/dy (pixel offsets) are also available under labels to nudge a label off its default position. To enable labels for all bar charts at theme level, set marks.bar.labels.visible: true in your theme YAML.
Overlays (layers:)¶
Add a layers: list to a bar chart to overlay additional marks, a line trend, a
scatter reference, or a second bar series on a separate y-axis. The base bar chart owns
the x-axis, frame, title, legend, and sort. Each layer adds its own mark and legend
entry.
charts: revenue_vs_target: type: bar x: month y: revenue query: monthly layers: - type: line y: target label: Target axis_y: position: right style: marks: line: stroke: width: 2
A layer on its own axis gets an independent scale; see Dual y-axis for why dbt Charts warns about it and what to use instead.
Layer fields:
| Field | Description |
|---|---|
type |
Mark type: bar, line, area, or scatter |
y |
Measure column for this layer |
label |
Legend label (defaults to the column name) |
color |
Data field for color encoding on this layer |
query |
Override the base query for this layer's data |
x |
X field for this layer (values extend the base x-scale) |
axis_y |
{position: left|right} plus per-side scale/format |
style |
Marks-only style patch, nested under the layer's mark family (for example, marks.line.stroke.width) |
sort: and x-axis chrome are base-only; layers inherit x ordering.