# Overview (https://docs.dbtcharts.com/) # dbt Charts Overview *dbt Charts* makes it easy to build any *Interface* for your *Database*. You (or an AI assistant) write straightforward YAML declaring what you want to see, and dbt Charts renders it as an interactive dashboard, in a variety of formats. Built on top of dbt, it turns a text file and a SQL query into a live, shareable dashboard: no JavaScript, no drag-and-drop BI tool, no proprietary format. ## What is it for? dbt Charts is a visualization and dashboarding layer for data teams. It's good at two things: ### Reporting & dashboards Build polished, interactive dashboards and share them in multiple formats. Rich layouts, filters, and styling, all declared in YAML that lives in Git alongside your dbt models. ### Data exploration Go from a query to a chart instantly. A fast, iterative loop for exploring data and finding insights without the overhead of building a full report. ## How It Works 1. **You write a YAML file** describing what data to show and how to visualize it 2. **dbt Charts compiles it** into an interactive board 3. **The board queries your database** via SQL to fetch data 4. **Users interact** with filters, click through charts, and explore the data ## Quick Start Example Here's a compact dashboard that runs the moment you install dbt Charts, no database required. Each query embeds its rows inline with `type: values`, and the charts map query columns to visual encodings. `cols` places the two charts side by side. ```dbt-charts-example title: "Document activity" queries: by_status: type: values columns: [status, document_count] values: - [Signed, 412] - [Sent, 168] - [Draft, 97] - [Voided, 24] monthly: type: values columns: [month, documents] values: - [Jan, 120] - [Feb, 145] - [Mar, 138] - [Apr, 171] - [May, 189] - [Jun, 203] charts: status_chart: query: by_status type: bar title: "Documents by status" x: status y: document_count trend_chart: query: monthly type: line title: "New documents per month" x: month y: documents rows: - cols: - status_chart - trend_chart ``` That's it: inline data, two charts, a side-by-side layout, and a live board. When you're ready for real data, swap `type: values` for **SQL against your warehouse**; the charts stay the same (see the [Quick Guide](quick-guide.md)). Querying the dbt Semantic Layer by metric name is planned; that work is tracked in [dbt-labs/dbt-charts#1](https://github.com/dbt-labs/dbt-charts/issues/1). ## Why dbt Charts - **YAML is the source of truth**: Dashboards are text files. Version-controlled, diffable, reviewable in a pull request. - **SQL today, dbt metrics next**: Write raw SQL against your dbt models now. Asking for a governed metric by name through the dbt Semantic Layer is planned, so the definition stays in the dbt project. - **Dashboards that track your models**: Dashboards live alongside your dbt models in Git. When your models change on a branch, the dashboards on that branch change with them; no dangling references to fix after a migration. - **AI-friendly by design**: YAML is far easier for an AI assistant to write correctly than JavaScript, a proprietary BI config, or hand-drawn SQL. "Show me sales by region" becomes a valid dashboard an agent can generate, review, and refine. - **Open source**: The engine, CLI, and YAML language are free and open source; everything on this site describes them unless marked otherwise. [dbt Charts Cloud](cloud/index.md) is the optional hosted product that adds team features (access control, git-backed editing, and a shared warehouse connection) on top of the same YAML. ## What You Need to Know To use dbt Charts effectively, it helps to be comfortable with: - **SQL or dbt**: a basic grasp of your data models (metrics and dimensions) - **YAML**: basic syntax; it's simple - **Git**: basic version control You don't need JavaScript, a frontend framework, or web-deployment know-how; dbt Charts handles rendering and query generation. ## Get Started - **[Install dbt Charts](guides/installation.md)**: get set up in your environment - **[Quick Guide](quick-guide.md)**: learn boards, queries, charts, and variables, and build a dashboard in a few minutes - **[FAQ](faq.md)**: quick answers to "how do I move a chart?" and a hundred other questions, each linking to the full page - **[Examples](examples/quickstart.md)**: complete, working dashboards to copy from - **[VS Code & Cursor Extension](editor-extension.md)**: highlighting, snippets, and a live dashboard preview while you edit Once you're building, the [Charts](charts/index.md), [Variables](variables/index.md), [Themes](themes.md), [Styling](styling.md), and [YAML Reference](reference/yaml-reference.md) sections cover everything the language can do. # Quick Guide (https://docs.dbtcharts.com/quick-guide/) # Quick Guide dbt Charts boards are YAML files that render as dashboards. The fastest way to your first board is to hand the tool to a coding agent. The rest of this page is the setup by hand, and the syntax you will read in whatever the agent writes. --- ## Start with your agent In the project you want charts for, paste this into Claude Code, Codex, or any coding agent: ```text I want to make charts of this data with dbt Charts. Run `uv tool install dbt-charts`, then `dct skills intro`, and follow what it prints. ``` The agent installs `dct`, reads the intro skill (which teaches it the language and which skill to read next), looks at your data, writes boards into `charts/`, and renders them for you to look at. Ask for changes in plain language; every edit lands in the same YAML file, which you can read, diff, and review. That prompt installs with [uv](https://docs.astral.sh/uv/getting-started/installation/); install it first if you don't already have it. This works in any agent that has a shell and your files: Claude Code, Codex, Cursor, GitHub Copilot's agent mode, and the like. In claude.ai or ChatGPT there is nothing to install, so start on [dbtCharts.com](https://dbtcharts.com/) instead (see [dbt Charts Cloud](cloud/index.md)). Already ran it? Skip to [Syntax](#syntax) to read what your agent wrote. --- ## Or by hand Install dbt Charts with [uv](https://docs.astral.sh/uv/getting-started/installation/) or [pip](https://pip.pypa.io/en/stable/installation/); both of those pages cover Windows, macOS, and Linux. Then scaffold a project with `dct init`: ```bash uv tool install dbt-charts # or: pip install dbt-charts dct init ``` [Installation & Setup](guides/installation.md) has the full walkthrough, including warehouse adapters. `dct init` writes a `dbt_charts.yml` project anchor and a `charts/` directory with a starter dashboard (`charts/guide.yml`) that renders without a database. Preview it right away with `dct serve`, then edit the starter or add your own board alongside it. `dct init` can also set up editor and AI integrations. If you are starting in an existing dbt project, run `dct init` in the project root: `dct serve` discovers the root from `dbt_project.yml`, and SQL queries run against the connection your dbt `profiles.yml` names for the project. You do not need to declare a source. See [Getting Started](guides/getting-started.md) for the full setup path. --- ## Syntax The board is the base element: a layout tree that can hold text, variables, queries, charts, and other boards. Start with board layout, then add the pieces that make a board interactive. ### Boards A board can be as small as a title and Markdown text: ```dbt-charts-example title: "Simple board" text: | This is Markdown text. - **bold** for important points - *italics* for nuance - `code` for field names ``` Boards become powerful because they nest. When you need structure, `rows` stacks items vertically, `cols` places them side by side, and each item is itself a board: ```dbt-charts-example title: "This is the parent board" width: 600 rows: - text: | With just **some text** on it. It's Markdown, so we can put things like [links](https://example.com/), *emphasis*, and lists: - keep prose close to the data - call out important details - add structure without adding a chart style: border: width: 1 color: "#cbd5e1" - title: "Nested board" text: | This is a nested board with its own border. style: border: width: 1 color: "#cbd5e1" - title: "Two nested boards" cols: - text: | And here's another with just text, no title. - **Bold text** works inside nested boards. - *Italic text* does too. - Bullets make short notes easy to scan. style: border: width: 1 color: "#cbd5e1" - title: "And another with just a title" style: border: width: 1 color: "#cbd5e1" ``` When a board defines charts but no explicit layout, dbt Charts renders those charts as implicit rows. Use `rows`, `cols`, `tabs`, or `grid` when the arrangement matters. See [Boards](boards/index.md) for layout patterns. --- ### Variables Variables are the controls readers use to change a board. This one creates a segment selector with three static options: ```dbt-charts-example variables: segment: input: select options: static: - Enterprise - SMB - Mid-Market text: | # Segment: {{ segment or 'All segments' }} Change the selector above. The text reads **{{ segment or 'All segments' }}** from the current variable value. ``` The `{{ segment }}` expression can appear in text, titles, links, SQL, and query filters. A variable does not filter data by itself; you wire it into a query explicitly. Variables are usually not static like this first example. In production, you often populate them from a query, a source column, or a dbt model column; they also support many UI element and styling options. Read more in [Variables](variables/index.md). --- ### Queries Queries define the data a board can use. A board can mix database-backed SQL queries with static values for targets, labels, or small reference tables: ```yaml {contract=board-root} queries: revenue: # sql to a database sql: | SELECT month, segment, revenue FROM mart_revenue ORDER BY month source: analytics targets: # static values type: values columns: [segment, target] values: - [Enterprise, 438000] - [SMB, 228000] - [Mid-Market, 285000] ``` Queries can use the same `{{ segment }}` variable from above. The `filter` helper writes the SQL predicate for the selected value, or a no-op predicate when no segment is selected: ```yaml {contract=board-fragment} queries: revenue: sql: | SELECT month, segment, revenue FROM mart_revenue WHERE {{ filter('segment', segment) }} ORDER BY month source: analytics ``` Each query has a name (`revenue`, `targets`) and returns named columns. Queries can reference other queries, read previous query results, tune cache behavior, and combine SQL with CSV, dbt model data, HTTP, and static values. Read more in [Queries](queries.md). --- ### Charts Charts reference queries and map result columns to visual channels. Start with a query, then bind its columns to a chart: ```dbt-charts-example queries: monthly_revenue: sql: | SELECT date, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY date ORDER BY date source: examples_db charts: revenue_trend: query: monthly_revenue type: line title: "Revenue trend" x: date y: revenue ``` The chart does not aggregate or infer meaning. The query owns the rows and columns; the chart owns the visual encoding. Here, the SQL groups orders by date, and the line chart maps `date` to the x-axis and `revenue` to the y-axis. Variables can then feed the same query layer. This example populates a category selector from SQL and uses the selected value in the revenue query: ```dbt-charts-example variables: category: input: select options: query: category_options queries: category_options: sql: | SELECT DISTINCT category AS value FROM ecommerce_orders ORDER BY category source: examples_db revenue: sql: | SELECT category, SUM(revenue) AS revenue FROM ecommerce_orders WHERE {{ filter('category', category) }} GROUP BY category ORDER BY revenue DESC source: examples_db charts: revenue_by_category: query: revenue type: bar title: "Revenue by category" x: category y: revenue ``` Here, the variable changes the SQL predicate, while the chart still only maps the category and revenue columns to the axes. Charts come in many families, and extensible charts can render custom SVG or HTML when the built-in families are not enough. Dive into [Charts](charts/index.md). --- ### Styling dbt Charts keeps styling declarative too. Choose a theme for the whole board, then use typed chart options for labels, axes, formats, colors, marks, and scale behavior. Built-in themes are `clarity` (the default), `paper`, `vivid`, `neon`, and `stark` (see [Themes](themes.md)); chart-local style only needs to override what should differ from the theme: ```dbt-charts-example title: "Styled revenue trend" theme: paper width: 600 queries: daily_revenue: sql: | SELECT date, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY date ORDER BY date source: examples_db charts: revenue_trend: query: daily_revenue type: line x: date y: revenue x_label: "Order date" y_label: "Daily revenue" style: marks: line: curve: monotone axis_y: labels: format: currency_whole ``` Use styles when the default rendering needs more polish, but keep data meaning in the query and visual mapping in the chart definition. See [Styling](styling.md) and the chart family pages for the full option set. --- ### Bringing It All Together Cheat Sheet Start with the same pattern in a small board: one variable, one query, two KPIs above one single-value chart. ```dbt-charts-example title: "Category snapshot" width: 600 variables: category: label: "Category" input: select options: query: category_options queries: category_options: sql: | SELECT DISTINCT category AS value FROM ecommerce_orders ORDER BY category source: examples_db summary: sql: | SELECT SUM(revenue) AS revenue, COUNT(*) AS orders, AVG(revenue) AS avg_order FROM ecommerce_orders WHERE {{ filter('category', category) }} source: examples_db charts: revenue: query: summary type: kpi label: "Revenue" value: revenue style: value: format: currency_whole orders: query: summary type: kpi label: "Orders" value: orders avg_order: query: summary type: kpi label: "Avg order" value: avg_order style: value: format: currency_whole rows: - cols: - revenue - orders - avg_order ``` dbt Charts is flexible enough that you could use it to write the quick guide itself. The fuller cheat sheet below uses the same pieces, then nests the examples so the board reads like a compact version of this page. The parent board sets the default `clarity` theme explicitly, and most of the embedded example boards add their own border, so they read as cards nested inside the guide. Notice the chart definitions live at the top, then nested boards list chart IDs like `revenue_by_month` and `target_by_month` where those charts should appear. ```dbt-charts-example title: "dbt Charts quick start cheat sheet" theme: clarity variables: segment: input: select options: static: [Enterprise, SMB, Mid-Market] queries: revenue: type: values columns: [month, segment, revenue, target] values: - [Jan, Enterprise, 142000, 130000] - [Feb, Enterprise, 151000, 138000] - [Mar, Enterprise, 164000, 146000] - [Jan, SMB, 68000, 72000] - [Feb, SMB, 74000, 76000] - [Mar, SMB, 81000, 80000] - [Jan, Mid-Market, 97000, 91000] - [Feb, Mid-Market, 101000, 95000] - [Mar, Mid-Market, 109000, 99000] charts: revenue_by_month: query: revenue type: bar title: "Revenue by month" x: month y: revenue color: segment style: axis_y: labels: format: currency_whole target_by_month: query: revenue type: line title: "Target by month" x: month y: target color: segment style: axis_y: labels: format: currency_whole rows: - title: "1. Boards" cols: - text: | A board starts with a `title`, then stacks `rows` or places `cols` beside each other. Text cells support **markdown**, including links, lists, emphasis, and inline `code`. ```yaml title: "Basic board" rows: - text: | This is markdown text. - **bold** for important points - *italics* for nuance - `code` for field names - cols: - title: "Left panel" text: "Boards can sit side by side." - title: "Right panel" text: "Each item is itself a board." ``` - title: "Basic board" rows: - text: | This is markdown text. - **bold** for important points - *italics* for nuance - `code` for field names - cols: - title: "Left panel" text: "Boards can sit side by side." - title: "Right panel" text: "Each item is itself a board." - title: "2. Variables" cols: - text: | Variables define controls. This board has a `segment` selector, and both the text and charts read the current value. The board on the right is an actual nested board with its own variable definition. ```yaml variables: example_segment: label: "Segment" input: select options: static: [Enterprise, SMB, Mid-Market] rows: - text: | Current segment: **{{ example_segment or 'All segments' }}** ``` - style: border: width: 1 radius: 8 title: "Rendered variable" variables: example_segment: label: "Segment" input: select options: static: [Enterprise, SMB, Mid-Market] text: | Current segment: **{{ example_segment or 'All segments' }}**. Change this nested board's selector and the text updates with it. - title: "3. Queries" cols: - text: | Queries own the rows and columns. They can come from SQL, CSV, dbt models, HTTP data, or inline values. ```yaml queries: revenue: type: values columns: [month, segment, revenue, target] values: - [Jan, Enterprise, 142000, 130000] - [Feb, Enterprise, 151000, 138000] - [Mar, Enterprise, 164000, 146000] ``` - style: border: width: 1 radius: 8 title: "Query result" text: | The `revenue` query in this cheat sheet returns monthly revenue, segment, and target columns. The chart definitions below decide how to encode those columns visually. - title: "4. Charts" rows: - text: | Charts reference a query and bind result columns to visual channels. The chart definitions are declared once above, then this nested board lists the chart IDs in `cols` to place them side by side. ```yaml charts: revenue_by_month: query: revenue type: bar title: "Revenue by month" x: month y: revenue color: segment ``` - style: border: width: 1 radius: 8 title: "Rendered charts" cols: - revenue_by_month - target_by_month - title: "5. Nested boards" cols: - text: | Nested boards are how larger dashboards stay organized. This nested board has its own variable, query, charts, and layout. Inside it, chart IDs are placed where they belong. ```yaml title: "Segment review" variables: review_segment: label: "Segment" input: select options: static: [Enterprise, SMB, Mid-Market] queries: review_revenue: type: values columns: [month, segment, revenue, target] values: - [Jan, Enterprise, 142000, 130000] - [Feb, Enterprise, 151000, 138000] - [Mar, Enterprise, 164000, 146000] charts: review_revenue_by_month: query: review_revenue type: bar x: month y: revenue cols: - text: | Current segment: **{{ review_segment or 'All segments' }}** - review_revenue_by_month ``` - style: border: width: 1 radius: 8 title: "Segment review" variables: review_segment: label: "Segment" input: select options: static: [Enterprise, SMB, Mid-Market] queries: review_revenue: type: values columns: [month, segment, revenue, target] values: - [Jan, Enterprise, 142000, 130000] - [Feb, Enterprise, 151000, 138000] - [Mar, Enterprise, 164000, 146000] - [Jan, SMB, 68000, 72000] - [Feb, SMB, 74000, 76000] - [Mar, SMB, 81000, 80000] - [Jan, Mid-Market, 97000, 91000] - [Feb, Mid-Market, 101000, 95000] - [Mar, Mid-Market, 109000, 99000] charts: review_revenue_by_month: query: review_revenue type: bar title: "Revenue" x: month y: revenue color: segment style: axis_y: labels: format: currency_whole review_target_by_month: query: review_revenue type: line title: "Target" x: month y: target color: segment style: axis_y: labels: format: currency_whole cols: - text: | Current segment: **{{ review_segment or 'All segments' }}** - compare revenue against target - keep the explanation beside the visuals - rows: - review_revenue_by_month - review_target_by_month ``` Small boards compose into larger reports, and those reports can compose again. When a nested board gets large, move it to its own board YAML file and import it from the layout. --- ### Quick Reference ```yaml {contract=board-root} title: "My Board" variables: segment: input: select options: static: [Enterprise, SMB, Mid-Market] queries: revenue: type: values columns: [month, segment, revenue] values: - [Jan, Enterprise, 142000] charts: revenue_by_month: query: revenue type: bar x: month y: revenue rows: - title: "Revenue" text: "Monthly revenue by selected segment." - revenue_by_month ``` Start with the data and interaction you need, then add the board shape that makes it readable. --- ## Tools Use `dct render` when you want a static artifact from one board file: ```bash dct render charts/revenue.yml dct render charts/revenue.yml --format html dct render charts/revenue.yml --format png --output revenue.png ``` Use `--var` to render a specific variable state: ```bash dct render charts/revenue.yml --var segment=Enterprise ``` See [`dct render`](cli/render.md) for every output format. Use `dct serve` when you want live browser previews while editing YAML: ```bash dct serve dct serve --port 3000 ``` Board paths map to URLs. For example, `charts/revenue.yml` renders at `/revenue/`, and query parameters set variables: ```text http://localhost:3000/revenue/?segment=Enterprise ``` See [`dct serve`](cli/serve.md) for routing, ports, and database options. # FAQ (https://docs.dbtcharts.com/faq/) # How do I...? Quick answers to the questions people ask most, written the way they ask them. Each answer is the short version and links to the page that has the full one. If you came from another BI tool, start here: dbt Charts has no drag-and-drop editor, so "move", "resize", and "hide" all mean editing a few lines of YAML. --- ## Getting started ### How do I install dbt Charts? `uv tool install dbt-charts` (or `pip install dbt-charts`) on Python 3.11 or newer, then add the adapter extra for your warehouse. See [Installation](guides/installation.md). ### What is the smallest thing I need to make a dashboard? A `dbt_charts.yml` naming a data source, and one YAML file under `charts/` with a query, a chart, and a `rows:` list. The [Quick Start example](examples/quickstart.md) is complete in about twenty lines. ### What is a board? A board is the YAML file that defines a dashboard: its queries, charts, variables, and layout. Boards can nest inside other boards. See [Boards](boards/index.md). ### Is there a drag-and-drop editor? No. Every dashboard is a YAML file in git, and layout is expressed as `rows:`, `cols:`, `grid:`, or `tabs:`. The [Playground](https://play.dbtcharts.com) and the [VS Code extension](editor-extension.md) give you a live preview while you edit. ### How do I preview a dashboard while I work on it? Run `dct serve` and open the board in your browser, or press Cmd+Shift+V in VS Code with the [extension](editor-extension.md#live-preview) installed. Both re-render on save. ### How do I see all my dashboards in one place? `dct serve` lists every board under `charts/` at `/`, or serves `charts/index.yml` there if you create one. See [URL routing](cli/serve.md#url-routing). ### Can an AI assistant build dashboards for me? Yes. `dct init mcp` wires the [MCP server](cli/mcp.md) into Cursor, Claude Code, or ChatGPT, and `dct init skills` installs authoring skills. Cloud users can also use the [Claude and ChatGPT connector](cloud/claude-chatgpt-connector.md). ### Where do I find the full list of YAML fields? The generated [YAML Schema Reference](reference/yaml-reference.md) lists every field on every chart type. The [Quick Reference](reference/quick-reference.md) is the one-page cheat sheet. --- ## Arranging a dashboard ### How do I move a chart? Reorder it in the layout list. Charts render in the order they appear under `rows:` or `cols:`; cut the line and paste it where you want it. In a `grid:`, set `col` and `row` on the item to pin it. See [Board Layouts](boards/layouts.md). ### How do I put two charts side by side? Wrap them in a `cols:` list. Three items get a third of the width each. See [Cols](boards/layouts.md#cols-horizontal-stack). ### How do I make a chart full width? Give it its own entry in `rows:` rather than sharing a `cols:` row. In a `grid:` with `columns: 24`, set the item's `width: 24`. See [Grid Layout](boards/layouts.md#grid-layout). ### How do I resize a chart? Width comes from the layout: set `width: "70%"` on a `cols:` item, or a column span on a `grid:` item. Height is set on the chart with `height: 400`, or as a shape with `style.aspect_ratio`. See [Board Sizing](boards/sizing.md). ### How do I make one column wider than the others? Set `width:` on the `cols:` items (`"70%"` and `"30%"`), or nest a `cols:` inside a `cols:` for splits like 50/25/25. See [Unequal Widths with Nesting](boards/sizing.md#unequal-widths-with-nesting). ### How do I make a row of KPIs shorter? Set `height: "120px"` on the row item that wraps them. KPI cards, tables, and callouts size themselves; `height:` on those chart types is rejected. See [Height on Rows](boards/sizing.md#height-on-rows). ### How do I change the overall width of the dashboard? Set `style.frame.width` for an exact width, or `style.frame.max_width` to cap how wide the content may grow. See [Width Rule](boards/sizing.md#width-rule). ### How do I add a section heading? Give a nested board a `title:`. Titled sections render as headings, and heading levels follow nesting depth automatically. See [Board Content](boards/content.md#title-heading-levels). ### How do I add text, notes, or a paragraph of explanation? Add a `text:` block at the board root or as a layout item. It is Markdown: headings, lists, tables, links, and images all work. See [Board Content](boards/content.md). ### How do I add a logo or image? Use a Markdown image in a `text:` block: `![Logo](https://example.com/logo.svg)`. See [Images](boards/content.md#images). ### How do I split a dashboard into tabs? Use `tabs:` with an `items:` list; each item is a nested board with its own `title:` and layout. See [Tab Layout](boards/layouts.md#tab-layout). ### How do I collapse a section by default? Add `details:` with a `summary:` label to a nested board; it renders as a click-to-expand section with no JavaScript. See [Details](boards/layouts.md#details-collapsible-sections). ### How do I hide a chart without deleting it? Set `visible: false` on the layout item, or point `visible:` at a checkbox variable so readers can toggle it. See [Layout Visibility](variables/advanced.md#layout-visibility-visible). ### How do I reuse the same header or KPI row on several dashboards? Put it in a partial file (`_header.yml`) and list the path as a layout item. See [Board Imports](boards/imports.md). ### How do I change the spacing between charts? Gap sizing lives under `style.layout` (`rows`, `cols`, and `grid.gap`). Set it on the board, or in `charts/meta.yml` for the whole project. See [Board Styling](boards/index.md#board-styling). ### What happens on a phone? `cols:` layouts wrap to a vertical stack on narrow screens. See [Cols Layout](reference/layout-types.md#cols-layout). ### How do I show just one chart from a dashboard? Append `chart_focus: ` to the board file, or pass `--chart ` to `dct render`. See [Focusing a Single Chart](boards/index.md#focusing-a-single-chart). --- ## Charts ### Can I add a chart type the language doesn't have? Yes: `type: custom` draws a shape from your own SVG template. Many shapes also compose from the built-in ones, and text blocks can carry raw HTML on boards you host yourself. See [Chart types and extensibility](charts/extensibility.md). ### How do I change the chart type? Change `type:` (`bar`, `line`, `area`, `scatter`, `pie`, `donut`, `heatmap`, `histogram`, `table`, `kpi`, `map`, and more). Each type has its own required fields. See [Supported Built-In Types](charts/more-types.md#supported-built-in-types). ### How do I plot several series on one chart? Add `color:` naming the column that splits the series. See [Multi-Series Line Graph](charts/line-graphs.md#multi-series-line-graph). ### How do I plot two measures on one chart? Give `y:` a list: `y: [revenue, units_sold]`. Add `color:` as well when the result is already grouped by a dimension. See [Multiple Measures by a Dimension](charts/line-graphs.md#multiple-measures-by-a-dimension). ### How do I make a stacked bar chart? Add `color:` and set `style.stack: zero`. Use `normalize` for 100% stacked and `none` for grouped side-by-side bars. See [Axis, Scale, and Orientation](charts/bar-charts.md#axis-scale-and-orientation). ### How do I make bars horizontal? Set `style.orientation: horizontal`. Left on `auto`, categorical x-axes already render horizontally. See [Vertical Columns and Horizontal Bars](charts/bar-charts.md#vertical-columns-and-horizontal-bars). ### How do I sort bars by value? Add `sort:` with `by:` a column and `order: desc`. See [Labels and Ranking](charts/bar-charts.md#labels-and-ranking). ### How do I make the bars thinner? Set `style.marks.bar.band_width` to a fraction of the band, such as `0.4`. See [Bar Width](charts/bar-charts.md#bar-width). ### How do I show the value on each bar or point? Set `style.marks.bar.labels.visible: true` (or `marks.line`, `marks.point`). See [Value Labels](charts/bar-charts.md#value-labels). ### How do I label the end of each line instead of using a legend? Set `style.endpoint_labels.visible: true`. See [Endpoint Labels](charts/line-graphs.md#endpoint-labels). ### How do I hide the legend? Set `style.legend.visible: false`. See [Chart Styling](charts/index.md#chart-styling). ### How do I move the legend? Set `style.legend.position.edge` to `top`, `bottom`, `left` or `right`. `align` (`start`, `center`, `end`) places it along that edge, and `overlay: true` floats it over the plot instead of reserving a strip. See [LegendStyle](reference/yaml-reference.md#legendstyle). ### How do I limit how many legend entries show? Set `style.legend.symbol_limit`, or pin an explicit list with `style.legend.values`. See [Legend](charts/color-channels.md#legend). ### How do I rename the axis titles? Set `x_label:` and `y_label:` on the chart. See [Axis Labels, Formatting, and Styling](charts/line-graphs.md#axis-labels-formatting-and-styling). ### How do I rename a column in the legend or tooltip? Set `field_labels:` on the chart, mapping the column to its display text. See [Field Labels](charts/line-graphs.md#field-labels). ### How do I rotate or skip crowded axis labels? Overlapping labels are skipped and tilted automatically. Force an angle with `style.axis_x.labels.angle: -45`, or turn the automatic handling off. See [Axis Labels](charts/axis-labels.md). ### How do I hide axis labels or grid lines? `style.axis_y.labels.visible: false` hides labels; `style.axis.grid.visible: false` hides grid lines. See [Other label controls](charts/axis-labels.md#other-label-controls). ### How do I set the axis minimum and maximum? Set `style.axis_y.scale.continuous.domain: [0, 100]`. On a date axis, also set `scale.continuous.type: temporal`. See [Extending the visible range](charts/time-axes.md#extending-the-visible-range-past-the-data-scalecontinuoustype-scalecontinuousdomain). ### How do I stop the y-axis from starting at zero? Set `style.axis_y.scale.continuous.zero: false` on a line or scatter chart. A bar chart rejects it, because a bar's length is measured from zero, and an area chart warns, because its fill is too; use a line for a zoomed range. See [Target and Reference Lines](charts/layered-charts.md#target-reference-lines). ### How do I use a log scale? Set `style.axis_y.scale.continuous.type: log`. Values must be strictly positive, and bars do not support it. See [Axis Scales](charts/axis-scales.md). ### How do I format numbers as currency or percent? Name the format on the query with `formats:` (`currency`, `currency_whole`, `percent`, `integer`, and more) and every chart that shows the column uses it (see [Column formats](queries.md#column-formats)). A chart's own exception goes in its slot: `style.axis_y.labels.format` on a measure axis, or `format:` on a KPI or table column. Percent formats expect a 0 to 1 ratio. See [Format names and aliases](styling.md#format-names-and-aliases). ### How do I change the default currency? Set `regional.currency` to an ISO code. In `charts/meta.yml` it changes every board in the folder; in a board it overrides it: ```yaml {contract=board-root} regional: currency: EUR ``` `currency`, `currency_whole`, `currency_full` and every `$` spec then print `€`, at the locale's placement. A board has one currency; to show a second one, define a `style.formats` alias with a `prefix:` or `suffix:`. See [Regional settings](styling.md#regional-settings). ### How do I use European number separators? Set `regional.locale.number` to a locale name, e.g. `fr-FR` for `1 234,5`, or to a d3 locale definition. The locale's own currency comes with it unless `regional.currency` says otherwise. `regional.locale.time` does the same for month and weekday names (`de-DE` prints `Mrz`). See [Regional settings](styling.md#regional-settings). ### How do I define a number format once and reuse it? Add an alias under `style.formats` (for example `revenue: "$,.0f"`) and use the alias name in any `format:` field. See [Global Styling](styling.md#global-styling). ### How do I change how dates show on the axis? Set `style.axis_x.time_unit` for the grain and `style.axis_x.labels.format` for the label text. Date columns are detected automatically. See [Time Axes](charts/time-axes.md). ### Why does my line chart skip months with no data? Missing calendar buckets are synthesized automatically; `style.axis_x.fill` decides whether the gap breaks the line (`"null"`), reads as `zero`, or is interpolated. See [Gap handling](charts/time-axes.md#gap-handling-synthesized-rows-for-missing-buckets). ### How do I smooth a line? Set `style.marks.line.curve: monotone`. Use `step` for a step line. See [Smoothing and Step Lines](charts/line-graphs.md#smoothing-step-lines). ### How do I put bars and a line on the same chart? Add a `layers:` list to the chart; each layer has its own `type:` and `y:`. See [Layered Charts](charts/layered-charts.md). ### How do I add a second y-axis? On a layer, set `axis_y.position: right`. The layer gets its own scale on the right. See [Dual Y-Axis](charts/layered-charts.md#dual-y-axis). ### How do I add a trend line? Compute it in SQL (most warehouses have `REGR_SLOPE` and `REGR_INTERCEPT`) and overlay it as a `line` layer with its own `query:`. See [Trend Lines](charts/layered-charts.md#trend-lines). ### How do I add a target or goal line? There is no field for an arbitrary threshold line yet. Zero baselines and 100% lines are drawn automatically when relevant; for a fixed target, emit the target as a column and overlay it as a `line` layer. See [Target and Reference Lines](charts/layered-charts.md#target-reference-lines). ### How do I make a pie or donut chart? `type: pie` (or `donut`) with `theta:` for the size column and `color:` for the slice column. `total:` puts a grand total in the center of a donut. See [Sector Charts](charts/sector-charts.md). ### How do I repeat one chart per region (small multiples)? Add `multiples:` with `rows:` or `columns:` naming the field to split on. See [Small Multiples](charts/small-multiples.md). ### How do I make a heatmap? `type: heatmap` with `x:`, `y:`, and `color:` for the value. See [Heatmap](charts/more-types.md#heatmap). ### How do I show a distribution? `type: histogram` with `x:`; bins are computed for you. See [Histogram](charts/more-types.md#histogram). ### How do I make a map? `type: map` for filled regions, `point_map` for markers, `bubble_map` for sized markers. See [Maps](charts/maps.md). ### How do I show a single big number? `type: kpi` with `value:` and `label:`. The query must return exactly one row. See [KPI Charts](charts/tables.md#kpi-charts). ### How do I show a change versus last period on a KPI? Compute the delta in the query and put it in the KPI's `support:` block with a `format:` like `percent_delta`, a `glyph:`, and a `tone:`. See [KpiSupportConfig](reference/yaml-reference.md#kpisupportconfig). ### How do I add a note or warning card? `type: callout` with `message:` and an optional `style.tone` of `info`, `positive`, `negative`, or `warning`. See [Callout Charts](charts/tables.md#callout-charts). ### How do I change chart colors? Pick a palette with `style.color.categorical.palette` (for example `vivid-10`), or fix one color with `style.color.static`. See [Chart Color Channels](charts/color-channels.md) and [Palettes](guides/palettes.md). ### How do I give one category the same color on every chart? Pin it under `style.charts.category_colors` at the board root, so the value gets the same swatch everywhere. See [One value, one color](guides/palettes.md#one-value-one-color-across-the-whole-board). ### How do I color by value (a gradient)? Set `color:` to a numeric column and add `style.color.gradient` with a palette and optional `min`/`max`. See [Gradient scale](charts/color-channels.md#gradient-scale). ### How do I highlight values above a threshold? Add a `conditional_formatting:` block keyed by column, with `when:` rules like `gte: 50000` that set `background` or `font`. Works on tables and KPIs. See [Conditional (threshold)](charts/color-channels.md#conditional-threshold). ### How do I customize the tooltip? Tooltips show the hovered row's values automatically and follow the measure's format (the query's `formats:` or the chart's `axis_y.labels.format`). Their paint (background, font, value format) is set board-wide under `style.charts.tooltip`. See [Hover Tooltips](charts/interactions.md#hover-tooltips) and [TooltipStyle](reference/yaml-reference.md#tooltipstyle). ### How do I make a chart clickable (drill down)? Set `link:` to a URL template; `{{ x }}`, `{{ y }}`, and `{{ color }}` are replaced with the clicked value. See [Drill-Down Links](charts/interactions.md#drill-down-links-link). ### How do I add a title, subtitle, or description to a chart? `title:` and `subtitle:` render above the chart. `notes:` never renders; it is metadata for search and AI tools. See [Field Reference](charts/index.md#field-reference). ### How do I hide a chart's title? Leave `title:` out. A chart with no title renders without one. ### How do I set a chart's height? Set `height: 400` at the chart root, or `style.aspect_ratio` for a shape. `min_height` and `max_height` under `style:` clamp the automatic height. See [Chart Sizing](charts/index.md#chart-sizing). ### Why is my chart blank? Usually the query returned no rows or a named column is missing. Run `dct render board.yml --format json --allow-chart-errors` to see what the chart received, and `dct validate` for schema errors. See [Chart Not Displaying](guides/troubleshooting.md#chart-not-displaying). ### One chart shows an error but the others are fine. Is that expected? Yes. A failing query or data-shape error renders as a red callout in that chart's tile while the rest of the board renders. See [Error Handling](guides/error-handling.md). --- ## Tables ### How do I reorder table columns? Reorder the `SELECT` list in the query. Column order always comes from the query, never from `style.columns`. See [Table Column Config](charts/tables.md#table-column-config). ### How do I hide a table column? Set `visible: false` on that column under `style.columns`. Its values stay available to `link:` templates. See [Table Column Config](charts/tables.md#table-column-config). ### How do I rename a table column? Set `label:` on the column under `style.columns`, or alias it in SQL. See [Table Column Config](charts/tables.md#table-column-config). ### How do I set a column's width or alignment? `width:` (pixels or a percentage) and `align:` under `style.columns.`. `style.column_defaults` applies to every column at once. See [Table Column Config](charts/tables.md#table-column-config). ### How do I format numbers or dates in a table? Set `format:` on the column: a d3 spec like `"$,.0f"`, a preset like `number_full`, or a strftime spec like `"%B %d, %Y"` for dates. See [Number Formatting](charts/tables.md#number-formatting). ### How do I paginate a long table? Set `style.pagination.page_rows: 25`. Use the query's `limit:` when you want fewer rows fetched, not just fewer shown. See [Table Pagination](charts/tables.md#table-pagination). ### How do I make a link in a table cell? Set `link:` on the chart (every cell) or on a column under `style.columns`, with `{{ column_id }}` placeholders. See [Linking Between Boards](boards/linking.md#drill-down-from-charts-and-tables). ### How do I add a sparkline inside a table? Set `spark:` on a column whose cells hold a list of values. See [Inline Spark Columns](charts/tables.md#inline-spark-columns). ### How do I make a pivot (cross-tab) table? On a `table` chart, add `rows:`, `columns:`, and `values:` naming the fields. The query stays long-form. See [Pivot](queries.md#pivot-cross-tab-tables). ### How do I add a totals row or column? Emit the totals from the query (a literal `"Total"` value in the pivot field). The renderer never sums for you. See [Row and Column Totals](queries.md#row-and-column-totals). ### How do I show the data behind a chart as a table? Add `support_table:` to the chart to attach a mini data grid, or add a second `type: table` chart on the same query. See [Support Table](reference/chart-support-table.md). --- ## Queries and data ### How do I query a dbt model? Write SQL against a `dbt_profile` source and use `{{ ref('model_name') }}`. Run `dbt parse` first so `target/manifest.json` exists. See [Queries](queries.md). ### How do I connect to my warehouse? In a dbt project there is nothing to add: with no `sources:` declared, dbt Charts uses your dbt profile. Otherwise (or to pick a profile target) add a `sources:` entry to `dbt_charts.yml`: `type: dbt_profile` to reuse your dbt profile, or a direct `postgres`, `snowflake`, `bigquery`, `duckdb`, or `sqlite` block. See [Sources](sources/index.md). ### Which warehouses are supported? BigQuery, Snowflake, Postgres, Redshift, DuckDB, SQLite, SQL Server, Trino, Athena, ClickHouse, Databricks, and Spark, plus CSV, Parquet, and JSON files. See [Source Types](sources/index.md#source-types). ### How do I use a CSV file as a data source? Add a `type: csv` source pointing at the file (or folder) in `dbt_charts.yml`, then query it with SQL. See [Files and APIs](sources/index.md#files-and-apis). ### How do I put a few rows of data straight into the YAML? Use a values query: `rows:` with a list of mappings, or `columns:` plus `values:`. No database needed. See [Values Queries](queries.md#2-values-queries-inline-data). ### How do I chart data from an API? Use `type: http` with a `url:`, optional `method`, `headers`, and `body`. See [HTTP / API Queries](queries.md#3-http-api-queries). Runs under local `dct` only; Not supported on dbtcharts.com. ### Can I use one query for several charts? Yes. Name the query once under `queries:` and reference it from each chart with `query: `. It runs once. See [Reuse Queries Across Charts](queries.md#reuse-queries-across-charts). ### Can one query build on another? Yes. Reference it as `{{ queries.other_query }}` inside your SQL and it is inlined as a subquery. See [Query References](queries.md#query-references). ### How do I limit the number of rows? Set `limit:` on the query. Chart-level row limiting does not exist. See [Raw SQL Queries](queries.md#raw-sql-queries). ### How do I group by week, month, or quarter? Bucket the date in SQL (`DATE_TRUNC`) and chart the bucketed column; the axis picks up the grain. See [Use Time Grain](guides/best-practices.md#use-time-grain). ### Can I join data from two different sources? Yes. Reference each query's cached rows with `{{ queries.name.cache }}` and the join runs in the local cache engine (DuckDB). Plain `{{ queries.name }}` references must share a source. See [Cross-Source Composition via Cache](queries.md#cross-source-composition-via-cache). ### How do I refresh the data? Locally, restart `dct serve`, pass `--no-cache`, or set a `cache.ttl` in `dbt_charts.yml`. See [Caching](cli/serve.md#caching); Cloud's refresh is described in [Refresh](cloud/refresh.md). ### Can I schedule a refresh? Locally, a cron job running `dct render` with a persistent `--cache` file does the job. For Cloud, see [Refresh](cloud/refresh.md). ### Why is my dashboard slow? Usually one heavy query. Add `limit:`, aggregate in SQL rather than fetching raw rows, and reuse queries across charts. See [Performance Optimization](guides/best-practices.md#performance-optimization). ### How do I check my SQL without rendering anything? `dct query board.yml ` runs one query and prints rows; `--validate` lints it. See [dct query](cli/query.md). --- ## Filters and variables ### How do I add a filter (dropdown) to a dashboard? Define it under `variables:` and reference it in each query's `WHERE` clause with `{{ filter('column', variable_name) }}`. Variables never filter anything on their own. See [Variables](variables/index.md). ### How do I set the dropdown's options? `options.static` for a fixed list, `column:` to read distinct values from a table column, or `query:` for custom SQL. See [Variables](variables/index.md). ### How do I set a default value? Set `default:` on the variable. Unset, it starts null and `filter()` applies no filter; a non-required select or multiselect already offers a clear-selection row back to that state, so you don't need to author an "All" option yourself. See [UI Elements](variables/ui-elements.md#default). ### How do I add a date range picker? `input: daterange`, then `{{ filter_date_range('created_at', date_range) }}` in the query. Date columns default to this input. See [Date Range Filtering](variables/expressions.md#date-range-filtering). ### How do I make a filter apply to every chart? Reference the variable in every query that should respond to it. Charts whose queries do not mention it are unaffected. See [Wiring Variables to Queries](variables/index.md#wiring-variables-to-queries). ### How do I make one dropdown depend on another? Source the second variable from a query that filters on the first. See [Dependent Variables](variables/advanced.md#dependent-variables). ### How do I set a filter from the URL? Add it as a query parameter: `/sales/?region=West`. Every variable is settable this way, including hidden ones. See [URL routing](cli/serve.md#url-routing). ### How do I hide a variable's control but keep using it? Set `visible: false` on the variable. It can still be set from the URL or a link. See [Variable](reference/yaml-reference.md#variable). ### How do I require a value before the dashboard renders? Set `required: true`. The board asks for a value instead of rendering. See [Variables](variables/index.md). ### How do I click a bar to filter the rest of the page? Use `link:` with a query parameter for the variable, for example `link: "?region={{ x }}"`. See [In-page variable updates](charts/interactions.md#in-page-variable-updates). ### What input types are there? `select`, `multiselect`, `text`, `textarea`, `number`, `slider`, `datepicker`, `daterange`, `checkbox`, and `radio`. See [Variable UI Elements](variables/ui-elements.md). --- ## Styling ### How do I change the theme? Set `theme:` at the board root to `clarity`, `paper`, `vivid`, `neon`, or `stark`. See [Themes](themes.md). ### Is there a dark mode? `neon` is the dark theme. Custom dark themes extend it. See [The five built-in themes](themes.md#the-five-built-in-themes). ### How do I make my own brand theme? Create `charts/_my-brand.yaml` with `extends: clarity` and your overrides, then `extends:` it from each board. See [Custom themes](themes.md#custom-themes). ### How do I set the theme for a whole project? Put `theme:` and other presentation keys in `charts/meta.yml`. `dbt_charts.yml` is for engine settings only and rejects them. See [Project Configuration](guides/installation.md#project-configuration-optional). ### How do I change the font? Set `style.fonts.body.family` for prose and `style.fonts.heading.family` for titles and headings. See [Typography](styling.md#typography). ### How do I change the background color? `style.background` on the board for the page, or on a chart for that chart's card. See [Global Styling](styling.md#global-styling). ### How do I add a border or rounded corners to a section? Set `style.border` on the nested board, as a CSS shorthand or a `width` / `color` / `radius` block. See [Board Styling](boards/index.md#board-styling). ### How do I move or remove the "Data as of" timestamp? `style.timestamp.position` and `style.timestamp.align` place it; `style.timestamp.format` rewrites the text. See [Data-freshness stamp](styling.md#data-freshness-stamp). ### Can I use raw HTML? Yes, with `html_policy: trusted-raw` at the board root, for dashboards you control. Cloud caps this. See [HTML Policy](boards/html-policy.md). ### Why do my emoji look gray? Emoji render in a calm monochrome by default. Set `style.font.emoji: system-default` for color emoji. See [Emoji handling](styling.md#emoji-handling). --- ## Sharing, exporting, and Cloud ### How do I export a dashboard as an image or PDF? `dct render board.yml --format png` (or `pdf`, `svg`, `html`). See [Output formats](cli/render.md#output-formats). ### How do I export one chart? `dct render board.yml --chart --format png`. See [dct render](cli/render.md). ### How do I export the underlying data? `dct render board.yml --format data` returns every query's rows as JSON. See [Extract the underlying data](cli/render.md#extract-the-underlying-data). ### How do I embed a dashboard in another site? Render to HTML or SVG and include the file, or use the [MkDocs integration](integrations/mkdocs.md) to embed live boards in documentation. Set `public_url` so links resolve when embedded elsewhere. See [Origin-absolute links](cli/render.md#origin-absolute-links-in-exports). ### How do I share a dashboard with my team? Push the board to git and open it in [dbt Charts Cloud](cloud/index.md). See [Access Control](cloud/access-control.md). ### How do I make a dashboard public? Public access is a Cloud feature. See [Public access](cloud/access-control.md#public-access-org-and-project-opt-in). ### Who can see which dashboards? In Cloud, that is decided by [Access Control](cloud/access-control.md). `dct serve` has no access control; everything it serves is visible to whoever can reach it. ### How do I invite someone? By email, from the organization's members page in Cloud. See [Sign in and create an organization](cloud/getting-started.md#sign-in-and-create-an-organization). ### How do I connect Cloud to my repository? Through the dbt Charts GitHub App, or a repository's HTTPS URL (private ones with a username and password or token). See [Connect your git repository](cloud/getting-started.md#connect-your-git-repository). ### Where do my edits in Cloud go? Into git, as commits. See [Git workflow](cloud/git-workflow.md). ### Can I require a review before a change goes live? Yes, in Cloud. See [Review mode](cloud/git-workflow.md#review-mode). With `dct serve`, your git host's pull-request review does the same job, and `dct validate` in CI keeps broken boards out; see [Validating Boards in CI](guides/ci.md). ### How do I duplicate a dashboard? Copy the YAML file. If you want two dashboards to share content, use a [partial import](boards/imports.md) instead of copying. ### How do I rename or move a dashboard? Rename or move the file; the URL follows the path. See [URL routing](cli/serve.md#url-routing). A moved board's Cloud permissions follow the path too; see [Access Control](cloud/access-control.md#the-filesystem-mental-model). ### How do I delete a dashboard? Delete the file and commit. ### Where is version history? In git. Every change is a commit. Cloud surfaces it too; see [Commit history](cloud/git-workflow.md#commit-history). ### Can I email a dashboard on a schedule? Not built in, locally or in Cloud. `dct render --format pdf` in a cron job, piped to your mailer, is the workaround. ### What credentials does Cloud need? A read-only warehouse user. See [Data connections](cloud/connections.md) and [Warehouse privileges](cloud/warehouse-privileges.md). ### Can I use DuckDB in Cloud? DuckDB is a local-development source. What Cloud connects to is listed under [Supported warehouses](cloud/connections.md#supported-warehouses). --- ## Tooling and troubleshooting ### How do I check a dashboard for mistakes before committing? `dct validate charts/` checks schema and cross-references in under a second. Add `--warehouse` to check columns against the database. See [dct validate](cli/validate.md). ### How do I validate dashboards in CI? `dct init ci` scaffolds a GitHub Actions workflow. See [Validating Boards in CI](guides/ci.md). ### What does this error code mean? Look it up in the [Error Reference](reference/errors.md) or [Warning Reference](reference/warnings.md), or run `dct docs warnings `. ### How do I silence a warning I have decided to accept? Pass `--ignore-warning ` to `dct render`, or suppress it in the board. See [Suppressing a Diagnostic](guides/error-handling.md#suppressing-a-diagnostic). ### My YAML worked last month and now fails. What changed? The grammar moved. Run `dct migrate --dry-run` to preview the rewrite, then `dct migrate` to write it. See [dct migrate](cli/migrate.md). ### How do I find which dashboards use a column I am about to rename? `dct impact `. See [dct impact](cli/impact.md). ### How do I search across all my dashboards? `dct search ` ranks boards by title, notes, and query text. See [dct search](cli/search.md). ### Which editor should I use? Any editor built on VS Code (VS Code, Cursor). `dct init code` installs the extension with highlighting, snippets, and live preview. See [VS Code and Cursor Extension](editor-extension.md). ### Is there somewhere to try YAML without installing anything? The [Playground](https://play.dbtcharts.com) renders a board from YAML in the browser, with sample data. ### Why does `ref()` say the manifest is missing? `target/manifest.json` is gitignored build output. Run `dbt parse` in the dbt project first. See [Queries](queries.md). ### Why does dbt suddenly report version 1.x, or say the adapter is missing? The `dct` install pulled its own dbt into the same environment. See [dbt v2 troubleshooting](guides/dbt-fusion.md#troubleshooting). ### Where do I ask a question the docs do not answer? Open an issue on the dbt Charts GitHub repository. Include the board YAML and the output of `dct validate`. # Queries (https://docs.dbtcharts.com/queries/) # Queries Queries define what data to fetch from your data sources. dbt Charts supports **four types** of queries. Most projects start with SQL. 1. **SQL queries** - Direct SQL against database tables (also how you query dbt models, via `ref()`) 2. **Values queries** - Inline data embedded directly in your YAML 3. **HTTP / API queries** - Fetch JSON data from REST endpoints 4. **Schema queries** - Introspect configured sources, schemas, tables, and columns --- ## Basic Query Structure Every query has a unique name and defines what data to fetch. **Query names must be unique within a file and across all imported files.** The simplest way to write a SQL query is a bare string: just the SQL itself. Set `source:` once at the board level and every string query uses it automatically: ```yaml {contract=board-root} source: my_postgres variables: status: input: select options: static: ["new", "open", "solved"] date_range: input: daterange default: ["2024-01-01", "2024-12-31"] queries: tickets: | SELECT status, priority, COUNT(*) AS ticket_count FROM dundersign.tickets WHERE {{ filter('status', status) }} AND {{ filter_date_range('created_at', date_range) }} GROUP BY 1, 2 ``` When you need to override the source, use `notes` for AI search metadata (it never renders), or mix query types on the same board, use the dict form instead: ```yaml {contract=board-root} queries: tickets: notes: "Open tickets by priority" sql: | SELECT ... source: my_postgres ``` --- ## Query Types ### 1. SQL Queries (Baseline) **String shorthand**: set `source:` once at the board level and write queries as bare SQL strings: ```yaml {contract=board-root} source: my_postgres variables: status: input: select options: static: ["new", "open", "solved"] date_range: input: daterange default: ["2024-01-01", "2024-12-31"] queries: tickets: | SELECT status, priority, COUNT(*) as ticket_count FROM dundersign.tickets WHERE {{ filter('status', status) }} AND {{ filter_date_range('created_at', date_range) }} GROUP BY 1, 2 ORDER BY ticket_count DESC ``` **Dict form**: use when you need to override the source per query, set a `target`, or add a `notes` (never rendered; AI search metadata only): ```yaml {contract=board-root} queries: sales: sql: | SELECT ... source: my_postgres # Required when no board-level source target: prod # Optional: dbt target name (defaults to 'dev') ``` Use the pipe (`|`) after `sql:` (or directly after the query name for string shorthand) to start a YAML literal block. Everything you indent under it becomes one multiline string, so you can paste your SQL verbatim. #### `setup_sql`: a preamble statement `setup_sql` runs as its own statement on the same connection, immediately before `sql`. Use it for a warehouse-specific declaration your main query depends on; the canonical case is a BigQuery `CREATE TEMP FUNCTION`: ```yaml {contract=board-root} queries: scaled_revenue: setup_sql: | CREATE TEMP FUNCTION double_it(x FLOAT64) AS (x * 2); sql: SELECT month, double_it(revenue) AS revenue_x2 FROM dundersign_serving.monthly_metrics source: my_warehouse ``` `setup_sql` is **non-nestable**: it may not contain `{{ queries.X }}` references, and a query that references another query's SQL still picks up that query's `setup_sql` automatically (the engine walks the dependency chain and runs every preamble, once each, before the composed query). Only `CREATE TEMP FUNCTION` / `CREATE TEMP TABLE` / `CREATE TEMP VIEW`, and DuckDB's `CREATE [OR REPLACE] MACRO`, are accepted; anything else (including non-`TEMP` `CREATE`, `DROP`, `INSERT`) is rejected. Not supported for SQLite sources. #### Database Connection via Source Configuration **dbt Charts uses source configuration for SQL database connections.** A source name can refer to a dbt profile defined in `profiles.yml`. This means: - ✅ **Works with dbt profiles** - dbt Charts reads your existing `profiles.yml` file - ✅ **Works with all dbt-supported databases** - Postgres, Snowflake, BigQuery, Redshift, DuckDB, etc. - ✅ **Leverages dbt's adapter system** - Handles database-specific quirks automatically - ✅ **Same credentials as dbt** - Uses your existing database connections **Setting up sources:** 1. **Create or edit `~/.dbt/profiles.yml`** (or `profiles.yml` in your project directory) 2. **Define your database connection** using dbt's profile format: ```yaml {contract=unverified} # ~/.dbt/profiles.yml my_postgres: outputs: dev: type: postgres host: localhost port: 5432 user: myuser password: mypass dbname: mydb schema: public prod: type: postgres host: prod.example.com port: 5432 user: prod_user password: "{{ env_var('DB_PASSWORD') }}" dbname: analytics schema: public target: dev # Default target ``` 3. **Reference the source in your queries** using the `source` field (and optionally `target`): ```yaml {contract=board-root} queries: tickets: sql: SELECT * FROM dundersign.tickets source: my_postgres # Uses 'my_postgres' source (matches dbt profile name) target: prod # Uses 'prod' target (defaults to 'dev' if not specified) ``` **For more information:** - See [dbt's profile documentation](https://docs.getdbt.com/reference/profiles.yml) for complete profile configuration options - See [dbt adapter documentation](https://docs.getdbt.com/docs/connect-data-platform) for database-specific connection parameters To query a dbt model, write a plain SQL query against a `dbt_profile` source and resolve the model with dbt's `ref()` macro: ```yaml {contract=board-root} queries: model_sales: sql: SELECT month, region, revenue, order_count FROM {{ ref('fct_orders') }} source: my_postgres ``` `ref()` and `source()` are resolved from the dbt manifest, so the project needs one: run `dbt parse` (or any command that writes `target/manifest.json`). A `dbt_profile` source with `target_path:` reads `/manifest.json` instead ([details](sources/index.md#which-manifest-ref-resolves-against)). `target/` is gitignored dbt build output, so it must be regenerated after every fresh clone. Without a manifest the query fails with `ERR-DBT-MANIFEST-MISSING`. On dbtcharts.com you need no manifest: refs resolve against your production warehouse ([details](sources/index.md#on-dbtcharts-com)). **Type Inference**: Query types are automatically inferred from the keys present: - `sql:` → SQL Query (requires `source` field) - `rows:` or `values:` → Values Query (inline data) - `url:` → HTTP Query - Schema queries have no key-based inference; set `type: schema` explicitly **Use cases:** - Most common use case - direct SQL queries - Full control over query logic - Works with any dbt-supported database - Requires source configuration (minimal setup - just `profiles.yml`) ### 2. Values Queries (Inline Data) Values queries embed data directly in your YAML; no database or file needed. Perfect for examples, documentation, small reference datasets, and prototyping. **Dict rows syntax**: each row is a key-value mapping: ```yaml {contract=board-root} queries: products: rows: - { product: "Widget A", revenue: 100, category: "Electronics" } - { product: "Widget B", revenue: 140, category: "Electronics" } - { product: "Gadget X", revenue: 180, category: "Accessories" } ``` **Columns + values syntax**: compact, SQL-style: ```yaml {contract=board-root} queries: products: columns: [product, revenue, category] values: - ["Widget A", 100, "Electronics"] - ["Widget B", 140, "Electronics"] - ["Gadget X", 180, "Accessories"] ``` Both syntaxes produce identical results. Use whichever reads better for your data. **Type Inference**: Detected by the presence of `rows:` or `values:`; no explicit `type:` needed. **Use cases:** - Examples and documentation that need to be self-contained - Small lookup/reference tables (categories, labels, thresholds) - Prototyping charts before connecting a real data source - Test fixtures and demo dashboards ### 3. HTTP / API Queries Fetch data from any JSON API. This is useful for integrating external data sources (weather, stock prices) or calling your own services (ML models, Jupyter notebooks exposed as endpoints). Runs under local `dct` only; Not supported on dbtcharts.com. ```yaml {contract=board-root} queries: prediction: type: http url: "https://api.model-service.com/predict" method: POST headers: Authorization: "Bearer {{ env.MODEL_API_KEY }}" body: features: region: "{{ region }}" date: "{{ date_range[0] }}" ``` **Type Inference**: Detected by the presence of `url:` or explicit `type: http`. **Use cases:** - Calling ML inference endpoints - Fetching data from 3rd party APIs - Triggering server-side workflows (if read-only) ### 4. Schema Queries Introspect the project's own configured sources, schemas, tables, and columns instead of fetching business data. Useful for building meta-dashboards (data catalogs, source health checks) on top of dbt Charts's own configuration: ```yaml {contract=board-root} queries: orders_columns: type: schema source: my_postgres schema: public table: orders ``` Set only the fields that narrow the introspection you want: omit `table` to list tables in a schema, omit `schema` to list schemas in a source, omit `source` to list configured sources, or set `column` (requires `schema` and `table`) to profile a single column's value distribution. Each field requires the ones above it: `table` needs `schema`, `schema` needs `source`. `type: schema` is always explicit; there's no key-based inference for this type. Schema rows carry whatever metadata the source and its profiling depth provide, so their key set varies. `fields:` projects (and orders) the result rows to exactly the listed keys, the schema-query counterpart of a SQL SELECT list. A projected key a row lacks yields null, giving the consuming table a stable column set: ```yaml {contract=board-root} queries: tables: type: schema source: warehouse schema: analytics fields: [name, row_count] ``` **Use cases:** - Data-catalog / "what's in this warehouse" dashboards - Health checks over your own `dbt_charts.yml` sources ### Column formats A query knows what its columns mean, so it can say how they print. `formats:` maps a column to a format name, and every chart showing that column uses it: axis ticks, tooltips, mark labels, table cells and KPI values. Works on every query type. ```dbt-charts-example title: "EMEA results" queries: region_results: columns: [region, revenue, target, churn_rate] values: - ["EMEA", 1234567, 1500000, 0.031] formats: revenue: currency target: currency churn_rate: percent charts: revenue_bar: query: region_results type: bar x: region y: [revenue, target] results_table: query: region_results type: table churn: query: region_results type: kpi value: churn_rate rows: - cols: - revenue_bar - churn - results_table ``` The bar, the table and the KPI above carry no format of their own. A value is a format name: an engine name (`currency`, `percent`, `integer`, `year`, ...) or a name the board defines in `style.formats`. A d3 spec such as `"$,.0f"` is rejected with `ERR-QUERY-FORMAT-SPEC`; define a name for it in `style.formats` and use that. Names resolve against the board that renders the query, so a shared query naming a format of your own needs it defined in every board that imports it. See [Format names and aliases](styling.md#format-names-and-aliases). From weakest to strongest: the engine's format for the kind (`number`), the board's redefinition of that name in `style.formats`, board- and theme-level slot formats (`style.charts.axis_y.labels.format`), then the query's `formats:`, then a chart's own slot format: a KPI `value.format`, a table `columns..format`, a chart's `axis_y.labels.format`. A board-wide axis format therefore never hides a query's `formats:`; the chart that wants something else says so in its own slot. A format reaches an axis of its own kind: a number format on a numeric axis, a time format on a date axis. A year-shaped integer `x` is a date axis and prints its own years; a band axis prints its category text as it is. An axis painting several columns (`y: [revenue, orders]`) uses a query format only when every column names the same one; otherwise it keeps the engine's `number`. Heatmap cells and tooltips take the color column's format, a pie its `theta` column's. A continuous color legend (a numeric `color:` on a bar or scatter) has no format slot and keeps its default. A `formats:` key must be a column the query returns, or the render fails with `ERR-QUERY-FORMAT-COLUMN-MISSING` listing the columns that came back. A `values` query is checked when the board compiles, a SQL query by `dct validate --warehouse` and when its rows return. An empty result has no columns to check. #### Formats from dbt A dbt model is where a column is defined, so it can say how the column prints once for every board that reads it. Set `meta.format` on the column: ```yaml {contract=unverified} models: - name: orders columns: - name: amount config: meta: format: currency ``` A board query that reads `{{ ref('orders') }}` or `{{ source(...) }}` then prints `amount` as `currency` with no `formats:` of its own. The format follows the column through the SQL by a fixed set of rules, and stops where it cannot be known: | Expression | Prints as | |---|---| | a column, an alias, `CAST` to a numeric type | the column's format | | `SUM`, `AVG`, `MIN`, `MAX`, `MEDIAN`, `PERCENTILE_*` (and window forms) | the column's format; `SUM` of a `percent` column has none | | `COUNT(...)` | `integer` | | `a + b`, `a - b` with the same format | that format | | `a / b` with the same format | `percent` | | `currency / integer` | `currency` | | `CASE`, `COALESCE`, `IF` whose branches share a format | that format | | `EXTRACT(year ...)`, `YEAR(...)` | `year` | | `ROUND`, `ABS`, `FLOOR`, `CEIL` | the column's format | | `UNION` whose branches share a format | that format | Everything else has no format, including `price * quantity`, `amount * 100`, and inputs with different formats. An `id` column under any aggregate but `COUNT` has none. Set the format yourself with the query's `formats:`, which always beats the dbt column. A `meta.format` that is neither an engine format nor defined in the root board's `style.formats` (nested boards inherit it) fails the chart that reads the column with `ERR-DBT-COLUMN-FORMAT-UNKNOWN`, naming the model and column. A column no query reads, or one the query's own `formats:` names, is never checked. The format comes from `target/manifest.json`, so run `dbt parse` after changing it. --- ## Field Reference ### Values Queries ```yaml {contract=unverified} queries: : # Option 1: Dict rows rows: # List of {key: value} dicts - { col1: val1, col2: val2 } # Option 2: Columns + values (compact) columns: [string] # Column names values: # List of value arrays - [val1, val2] ``` ### HTTP / API Queries Runs under local `dct` only; Not supported on dbtcharts.com. ```yaml {contract=unverified} queries: : type: http # Required url: string # Required: URL (supports Jinja) method: GET | POST # Optional: Default GET headers: Record # Optional: HTTP headers params: Record # Optional: Query parameters body: any # Optional: JSON body (for POST) json_path: string # Optional: dot-path to the row array in a nested response limit: number # Optional: max rows ``` Non-GET http sources are refused under `dct mcp serve` and other agent surfaces; `dct render`, `dct serve`, Streamlit, and the docs renderer allow them. Use `json_path` when the API wraps its rows inside an envelope object instead of returning a bare array. It's dot-notation (`$.key.subkey`), not full JSONPath; it must resolve to a list: ```yaml {contract=board-root} queries: orders_api: type: http url: "https://api.example.com/orders" json_path: "$.data.orders" # response is { "data": { "orders": [ {...}, {...} ] } } ``` Omit `json_path` when the response is already a bare array of row objects. ### Raw SQL Queries **String shorthand** (recommended when all queries share one source): ```yaml {contract=unverified} source: my_postgres # board-level default queries: : string # bare SQL; inherits board source ``` **Dict form** (use when overriding source, setting target, or adding notes): ```yaml {contract=unverified} queries: : sql: string # Required: SQL query (supports Jinja) source: string # Required if no board-level source target: string # Optional: dbt target name (defaults to 'dev') dialect: string # Optional: warehouse dialect the SQL is written in (see below) notes: string # Optional: metadata for AI search; never rendered ``` **Example:** ```yaml {contract=board-root} source: my_postgres queries: tickets: | SELECT * FROM dundersign.tickets WHERE status = 'solved' LIMIT 100 ``` **Database Connection via Source Configuration:** dbt Charts uses **source configuration** for SQL database connections. The `source` field must match a profile name defined in your dbt `profiles.yml` file (typically located at `~/.dbt/profiles.yml` or in your project directory). **Source Configuration:** 1. **Create or edit `~/.dbt/profiles.yml`** with your database connection: ```yaml {contract=unverified} my_postgres: outputs: dev: type: postgres host: localhost port: 5432 user: myuser password: mypass dbname: mydb schema: public prod: type: postgres host: prod.example.com port: 5432 user: prod_user password: "{{ env_var('DB_PASSWORD') }}" dbname: analytics schema: public target: dev # Default target ``` 2. **Reference the source in your queries:** ```yaml {contract=board-root} queries: tickets: sql: SELECT * FROM dundersign.tickets source: my_postgres # Uses 'my_postgres' source target: prod # Uses 'prod' target (defaults to 'dev' if not specified) ``` **For more information:** - See [dbt's profile documentation](https://docs.getdbt.com/reference/profiles.yml) for complete profile configuration options - See [dbt adapter documentation](https://docs.getdbt.com/docs/connect-data-platform) for database-specific connection parameters (Postgres, Snowflake, BigQuery, Redshift, etc.) --- ## Filters SQL queries have no declarative `filters:` field; you filter by writing the `WHERE` clause yourself, using Jinja to reference variables. Reach for the `filter()` / `filter_date_range()` helpers (below) instead of hand-rolled conditionals; they handle null-checking, list-to-`IN`, and parameter binding for you. ### Jinja in SQL Board SQL is ordinary SQL with Jinja. You can use your board's variables, the `filter()` and `filter_date_range()` helpers, `{{ queries. }}` to compose another query, and `{{ ref() }}` / `{{ source() }}` to name your dbt models and sources. Write it in one dialect and set `dialect:`: dbt Charts translates common SQL to the warehouse you run it on, and raises an error on constructs it can't translate. dbt Charts doesn't run dbt macros: put business logic in your models. Reference variables directly in the SQL text: ```yaml {contract=board-root} variables: priority: input: text ticket_type: input: text queries: filtered_tickets: sql: | SELECT priority, status, COUNT(*) AS ticket_count FROM dundersign.tickets WHERE priority = '{{ priority }}' AND ticket_type = '{{ ticket_type }}' GROUP BY priority, status source: my_warehouse ``` See the [Expressions](variables/expressions.md) guide for the `filter()` / `filter_date_range()` helpers, which parameterize these Jinja-based filters against SQL injection and handle unset variables automatically. ### Writing SQL once with `dialect:` A board that has to run on several warehouses, such as a dashboard pack shipped inside a dbt package, would otherwise need one query per dialect. Set `dialect:` to the dialect you wrote the SQL in. When it differs from the source's warehouse, dbt Charts translates the query before running it: ```yaml {contract=board-root} queries: monthly_tickets: dialect: duckdb source: my_warehouse sql: | SELECT date_trunc('month', created_at) AS month, COUNT(*) AS tickets, SUM(solved) / NULLIF(COUNT(*), 0) AS solve_rate FROM {{ ref('fct_tickets') }} GROUP BY 1 ``` Run against BigQuery, Snowflake, Redshift, Databricks, or Postgres, the same query gets each warehouse's spelling of `date_trunc` and integer division. What to know: - Without `dialect:`, nothing is translated. The SQL runs as written. - Translation fails with `ERR-SQL-DIALECT-UNSUPPORTED` when the SQL does not parse, holds more than one statement, calls a function dbt Charts does not know, or uses a construct the target cannot express. It is not a guarantee: a function that exists in both dialects but behaves differently, or one the warehouse lacks, can still fail at the warehouse. - Queries joined with `{{ queries. }}` must all set the same `dialect:`, or all leave it unset (`ERR-QUERY-DIALECT-MIXED`). - `setup_sql` is never translated. Write it in the source's own dialect. - `dialect:` is not supported on file sources, with `{{ queries..cache }}` (which runs on DuckDB), or with `incremental:`. Each raises `ERR-SQL-DIALECT-UNSUPPORTED` when the dialect differs from the one the query runs in. - A variable that ends up inside a string after translation, such as `INTERVAL {{ n }} DAY` on Postgres, is rejected with `ERR-SQL-DIALECT-UNSUPPORTED`. Write `{{ n }} * INTERVAL '1 day'` instead. - `dialect:` takes one spelling per warehouse: `athena`, `bigquery`, `clickhouse`, `databricks`, `duckdb`, `fabric`, `mysql`, `postgres`, `presto`, `redshift`, `snowflake`, `spark`, `sqlite`, `sqlserver`, `trino`. Some behavior differs by warehouse even when the SQL translates. BigQuery weeks start on Sunday. On Postgres, `datediff` counts full 24-hour periods, while Snowflake counts calendar boundaries crossed. **Percentiles and medians.** Percentile functions do not translate reliably. Compute an exact median in plain SQL instead: rank the rows and average the one or two middle values. ```yaml {contract=board-root} queries: median_resolution_by_group: dialect: duckdb source: my_warehouse sql: | WITH ranked AS ( SELECT group_name, resolution_minutes, ROW_NUMBER() OVER (PARTITION BY group_name ORDER BY resolution_minutes) AS rn, COUNT(*) OVER (PARTITION BY group_name) AS n FROM dundersign.tickets WHERE resolution_minutes IS NOT NULL ) SELECT group_name, AVG(resolution_minutes) AS median_resolution_minutes FROM ranked WHERE rn IN (FLOOR((n + 1) / 2.0), CEIL((n + 1) / 2.0)) GROUP BY group_name ``` --- ## Pivot (Cross-Tab Tables) A tidy (long-form) query is pivoted into a cross-tab grid at render time by adding `rows` / `columns` / `values` channels to a **table** chart. A field on `columns` *is* the pivot: the SQL and the rows returned by the database are unchanged; only the table's layout is reshaped. ```yaml {contract=board-root} queries: tickets_long: sql: | SELECT priority, ticket_type, COUNT(*) AS ticket_count FROM dundersign.tickets GROUP BY priority, ticket_type ORDER BY priority, ticket_type source: warehouse charts: tickets_crosstab: type: table query: queries.tickets_long rows: [priority] # row headers down the left side columns: [ticket_type] # distinct values become column headers (the pivot) values: [ticket_count] # measure(s) that fill the cells ``` The table has one row per `priority` and one column per distinct `ticket_type`, with `ticket_count` filling the cells. List more than one measure on `values` (for example, `values: [revenue, units]`) to get a spanning column group per pivot value. If a cell maps to more than one source row the engine raises an error instead of silently aggregating; move the extra field onto `rows` or `columns`. Column order: a pivot dimension whose values have a canonical order (dates and timestamps (chronological) or numbers (ascending)) renders its columns in that order regardless of the query's row order, the way every reference tool does. Any other dimension (plain strings, booleans, or a mix of dates and a literal like `"Total"`) orders its columns by each value's first appearance in the result set, kept as one consistent order across the whole header when another dimension sorts, so an `ORDER BY` stays your lever for business-ordered categories and total-column placement. That lever is not optional for those dimensions. First-seen order is whatever order the warehouse hands back, and a `GROUP BY` whose `ORDER BY` does not constrain the pivot dimension has no defined order at all; parallel aggregation can return a different one run to run, so the same data renders a different column order on different days. Rows are first-seen too, and unconditionally, no row dimension is ever sorted for you, so order by every field you put on `rows` *and* `columns` (that is why the example above orders by both `priority` and `ticket_type`), or accept an arbitrary order. Pivot channels apply only to the `table` chart type; bar, line, and other charts use `x` / `y` / `color` and are unaffected. ### Row and Column Totals Totals are never computed by the render layer; a pivot table with totals needs a query that emits them. Two independent signals drive the two kinds of total: - **Row totals** (a trailing "Total" column): emit a literal `"Total"` value in the `columns` field alongside the real pivot values; it reshapes into a trailing column like any other value. - **Column totals** (a "Total" row): emit rows tagged with the same row-role column flat tables use for summary/total styling (`style.table.row.role`, for example, `_df_row_role`). Rows whose role resolves to `"total"` bucket by their `rows` value like every other row and get the total row's styling; a single grand total therefore means one consistent `rows` label (`priority = 'Total'` in the example) and merges into one row, while distinct total labels (per-group subtotals) stay distinct rows. Placement is the query's job: render never reorders *rows* (they keep first-seen order), so an `ORDER BY` decides where a total lands; put the total partition last to get a bottom total row. Render never merges or re-labels the values: the `rows` value on a total row is real data, not decoration. ```yaml {contract=board-root} queries: tickets_with_totals: sql: | SELECT priority, ticket_type, COUNT(*) AS ticket_count, 'value' AS row_role FROM dundersign.tickets GROUP BY priority, ticket_type UNION ALL SELECT priority, 'Total' AS ticket_type, COUNT(*) AS ticket_count, 'value' AS row_role FROM dundersign.tickets GROUP BY priority UNION ALL SELECT 'Total' AS priority, ticket_type, COUNT(*) AS ticket_count, 'total' AS row_role FROM dundersign.tickets GROUP BY ticket_type UNION ALL SELECT 'Total' AS priority, 'Total' AS ticket_type, COUNT(*) AS ticket_count, 'total' AS row_role FROM dundersign.tickets ORDER BY CASE WHEN priority = 'Total' THEN 1 ELSE 0 END, priority, CASE WHEN ticket_type = 'Total' THEN 1 ELSE 0 END, ticket_type source: warehouse charts: tickets_crosstab_with_totals: type: table query: queries.tickets_with_totals rows: [priority] # Both the priority rows and the ticket_type columns reshape in first-seen # (query) order; rows always do, and ticket_type is a string dimension, the # kind whose columns follow query order (dates/numbers would sort instead), # so the ORDER BY above pins both: priorities then types, each with its # "Total" sorted last. The total ROW lands last only because the ORDER BY # puts the 'Total' priority-partition last; drop that clause and the total # row renders wherever the query returns it. columns: [ticket_type] values: [ticket_count] style: row: role: row_role # tags "total" rows for styling; query order places them ``` The role tag marks *rows*, not cells: the row-total UNION branch above stays `'value'` because it's still a detail priority row (it just carries a `'Total'` ticket_type cell), and only the bottom-row partition is tagged `'total'`. Tagging the row-total branch `'total'` would collide it with that priority's detail bucket and raise a `conflicting row roles` error at render. A role-tagged total row gets the same double-rule styling as a flat table's total row; pivot render only reshapes the role-tagged rows, it does not restyle or re-derive them. Nothing styles a column by value; the trailing total column is plain, it's only in the last position because of query order. --- ## Best Practices ### Reuse Queries Across Charts Multiple charts can reference the same query, which is more efficient: ```yaml {contract=unverified} queries: sales: sql: SELECT month, region, SUM(revenue) AS total_revenue FROM dundersign.orders GROUP BY 1, 2 source: my_postgres rows: - cols: - chart1: query: queries.sales # Same query type: bar x: month y: total_revenue - chart2: query: queries.sales # Same query type: line x: month y: total_revenue color: region ``` ### Use Query References for DRY SQL Instead of repeating SQL, reference other queries: ```yaml {contract=unverified} # ❌ Bad: Repetitive SQL queries: open_tickets: sql: | SELECT priority, COUNT(*) as ticket_count FROM dundersign.tickets WHERE status = 'new' GROUP BY 1 source: my_postgres resolved_tickets: sql: | SELECT priority, # Repeated COUNT(*) as ticket_count # Repeated FROM dundersign.tickets WHERE status = 'solved' # Only difference GROUP BY 1 source: my_postgres # ✅ Good: DRY with query references queries: base_tickets: sql: | SELECT priority, status, COUNT(*) as ticket_count FROM dundersign.tickets GROUP BY 1, 2 source: my_postgres open_tickets: sql: | SELECT * FROM {{ queries.base_tickets }} WHERE status = 'new' source: my_postgres resolved_tickets: sql: | SELECT * FROM {{ queries.base_tickets }} WHERE status = 'solved' source: my_postgres ``` ### Performance Considerations - Use `limit` when you don't need all rows - Filter early, in the query's own `WHERE` clause (using `filter()`/`filter_date_range()`) rather than fetching all rows and filtering elsewhere - Reuse queries across multiple charts ### Naming Conventions - Use descriptive names (for example, `sales`, `products`) and reference with explicit namespacing (`queries.sales`) - Use descriptive names that indicate what data the query fetches - Keep names lowercase with underscores --- ## Testing and Validation ### Validate Your Dashboard After writing queries, validate your dashboard to catch errors early: ```bash dct validate charts/sales.yml ``` See the [CLI Reference](cli/validate.md) for validation options. ### Render to Inspect Resolved Queries Render your dashboard to JSON to see the resolved query structure with executed results: ```bash dct render charts/sales.yml --format json ``` See the [CLI Reference](cli/render.md) for rendering options. --- ## Query References You can reference other queries within your SQL using Jinja template syntax. This enables CTE-style query composition where complex queries can be built from simpler building blocks. ```yaml {contract=board-root} queries: # Base query - tickets by status tickets_by_status: sql: | SELECT status, COUNT(*) as ticket_count FROM dundersign.tickets WHERE status = 'solved' GROUP BY 1 source: my_postgres # Reference the base query top_status_tickets: sql: | SELECT ticket_count as current_ticket_count FROM {{ queries.tickets_by_status }} ORDER BY ticket_count DESC LIMIT 1 source: my_postgres ``` ### How It Works When you use `{{ queries.query_name }}`, dbt Charts: 1. Substitutes the referenced query's SQL inline as a parenthesized subquery 2. Aliases that subquery to the **query name**, so you reference its columns as `query_name.column`, and the same SQL is valid on both DuckDB and Postgres (Postgres requires the alias; DuckDB tolerates it) 3. Detects circular dependencies You don't add the parentheses or the alias yourself. The example above becomes: ```sql SELECT ticket_count as current_ticket_count FROM ( SELECT status, COUNT(*) as ticket_count FROM dundersign.tickets WHERE status = 'solved' GROUP BY 1 ) AS tickets_by_status ORDER BY ticket_count DESC LIMIT 1 ``` ### Chained References You can chain multiple query references: ```yaml {contract=board-root} queries: solved_tickets: sql: | SELECT * FROM dundersign.tickets WHERE status = 'solved' source: my_postgres priority_totals: sql: | SELECT priority, COUNT(*) as ticket_count FROM {{ queries.solved_tickets }} GROUP BY 1 source: my_postgres top_priority: sql: | SELECT ticket_count FROM {{ queries.priority_totals }} ORDER BY ticket_count DESC LIMIT 1 source: my_postgres ``` ### Referencing Columns (the automatic alias) The subquery is aliased to the **query name**, so qualify its columns with that name. Don't add your own `AS` alias; the query already has one and writing `FROM {{ queries.base_tickets }} AS b` produces a SQL parse error (`... AS base_tickets AS b`) because the alias is inserted automatically. To use a different alias, rename the query itself: ```yaml {contract=board-root} queries: base_tickets: sql: | SELECT status, ticket_type FROM dundersign.tickets source: my_postgres top_statuses: sql: | SELECT base_tickets.status, COUNT(*) as ticket_count FROM {{ queries.base_tickets }} GROUP BY base_tickets.status HAVING COUNT(*) > 10 source: my_postgres ``` Referencing the **same** query twice in one statement collides on the alias and fails; give each its own query name instead. ### Cross-Source Composition via Cache `{{ queries.X }}` inlines query X as a SQL subquery; both queries must run against the **same source**. To join results from **different sources**, use the cache read token `{{ queries.X.cache }}` instead. `{{ queries.X.cache }}` reads query X's **cached result rows** and evaluates the composing query in the local cache engine (DuckDB), making cross-source joins possible: ```yaml {contract=board-root} queries: # Runs against your data warehouse monthly_revenue: sql: | SELECT month, revenue FROM dundersign_serving.monthly_metrics source: my_warehouse # Runs against your CRM (different source) monthly_leads: sql: | SELECT month, COUNT(*) AS leads FROM leads GROUP BY 1 source: my_crm # Joins the two sources; evaluated in the local cache engine. # Each cache reference is aliased to its query name (no AS needed). efficiency: sql: | SELECT month, monthly_revenue.revenue, monthly_leads.leads, monthly_revenue.revenue / NULLIF(monthly_leads.leads, 0) AS revenue_per_lead FROM {{ queries.monthly_revenue.cache }} JOIN {{ queries.monthly_leads.cache }} USING (month) source: my_warehouse ``` **How it works:** dbt Charts executes `monthly_revenue` and `monthly_leads` first, caches their results, then evaluates the composing query inside the cache engine (DuckDB) using the cached rows as tables. **Opt out of caching** by setting `cache: false` on a query. Queries with caching disabled cannot be referenced via `.cache`: ```yaml {contract=board-root} queries: sensitive_data: sql: SELECT * FROM pii_table source: my_warehouse cache: false # results never cached ``` ### How long results stay cached `cache:` takes a duration: `5m`, `1h`, `7d`, or a compound like `1h30m` (`m` is minutes). Cached rows older than that are recomputed on the next read. `cache: forever` never auto-expires, and `cache: false` turns caching off. The default is 24 hours. One limit on `forever`: the default cache file of `dct render` and `dct serve` drops entries older than about a week, whatever their duration. Give them a cache file of your own (`--cache `) to keep results longer. `dct render` and `dct serve` keep cached results on disk between runs, so rendering a board again inside that window does not query the warehouse again. [Caching in `dct render`](cli/render.md#caching) covers where the file lives, how to force fresh data, and running several processes at once. Write it once at the top of a dashboard and every query in it inherits: ```yaml {contract=board-root} title: Sales cache: 1h # every query on this board expires after an hour queries: overview: sql: SELECT * FROM daily_rollup # 1h, inherited live_tickets: sql: SELECT * FROM dundersign.tickets WHERE status = 'new' cache: 30s # this one needs to be fresher pii_lookup: sql: SELECT * FROM pii_table cache: false # never cached ``` The nearest setting wins: a query's own `cache:` beats the dashboard's, which beats the source's, which beats the project default in `dbt_charts.yml`. Anything you leave out is inherited, so `cache: 30s` above changes only the duration. ### Limitations & Notes **Query references are powerful** - they let you build complex analyses by composing simpler queries. Just two simple limitations: **1. Top-Level Only** **All query references** (`` `{{ queries.* }}` ``) **MUST reference top-level queries** in the current dashboard's `queries:` section: ```yaml {contract=unverified} queries: base_query: # ✅ Top-level - can be referenced sql: SELECT * FROM dundersign.tickets source: my_postgres # Import from other files and add to top level: shared_sales: _shared_queries.queries.base_sales # ✅ Top-level - can be referenced my_analysis: # ✅ Top-level - can be referenced sql: | SELECT * FROM {{ queries.shared_sales }} WHERE amount > 100 UNION ALL SELECT * FROM {{ queries.base_query }} source: my_postgres rows: - queries: # ❌ NESTED - cannot be referenced anywhere! nested_query: sql: SELECT * FROM products source: my_postgres ``` **Important:** Even if you import a query from another file that has internal references (like `{{ queries.raw_data }}`), you must import ALL referenced queries to the top level of your current dashboard for them to work. **2. SQL Only** Only SQL queries can reference other queries. Values, HTTP, CSV, and other query types cannot use `{{ queries.* }}`. ## Importing Queries from Other Files You can reuse queries across multiple dashboards by referencing queries from other files directly in your `queries:` section. ### Import Syntax Use the format: `file_name.queries.query_name` ```yaml {contract=unverified} queries: # Import queries from other files base_sales: _shared_queries.queries.base_sales customer_data: _shared_queries.queries.customer_data # Use imported queries like any other query my_analysis: sql: | SELECT * FROM {{ queries.base_sales }} WHERE revenue > 1000 source: my_postgres charts: sales_chart: query: my_analysis type: bar x: month y: revenue ``` ### Import Examples **Import from same directory:** ```yaml {contract=unverified} queries: # _shared_queries.yml is in the same directory (underscore prefix means not rendered) orders: _shared_queries.queries.base_orders customers: _shared_queries.queries.base_customers ``` **Import from subdirectory:** ```yaml {contract=unverified} queries: # analytics/metrics.yml revenue: analytics/metrics.queries.total_revenue # ml/predictions.yml top_customers: ml/predictions.queries.predict_top_customers ``` **Import from a sibling directory:** A reference may start with one or more `../` segments to climb out of the current directory. This is how a board in one folder reuses queries defined in another: ```yaml {contract=unverified} queries: # from charts/gtm_weekly/mqls.yml, reaching charts/sales/pipeline.yml mqls_by_week: ../sales/pipeline.queries.mqls_by_week ``` Paths are resolved relative to the importing file, and may not escape the project root; `../../../etc/passwd.queries.x` is rejected. **Use imported queries:** ```yaml {contract=unverified} queries: base_sales: _shared.queries.sales filtered_sales: sql: | SELECT * FROM {{ queries.base_sales }} WHERE region = '{{ filter("region", "US") }}' source: my_postgres summary: sql: | SELECT COUNT(*) as total_orders, SUM(amount) as total_revenue FROM {{ queries.filtered_sales }} source: my_postgres charts: summary_chart: query: summary type: kpi value: total_revenue ``` ### Best Practices 1. **Use imports for shared queries**: Create `_shared_queries.yml` files (underscore prefix means not rendered as dashboards) for queries used across multiple dashboards 2. **Don't overdo it**: If you find yourself creating many shared query files, consider whether you need better data modeling in dbt or a proper data mart instead. Query references are for composition, not data transformation pipelines. 3. **Use for composition**: Build complex queries from simple, reusable building blocks 4. **Consider CTEs**: For simple cases or one-off queries, standard SQL CTEs might be clearer 5. **Reference columns by query name**: A reference is auto-aliased to its query name, so qualify columns as `base.column`; don't add your own `AS` alias 6. **Test separately**: Ensure referenced queries work independently before chaining them 7. **Keep it simple**: Avoid deep chains (A → B → C → D). Two levels is usually enough --- ## Security: Parameterized Queries **dbt Charts uses parameterized queries to prevent SQL injection attacks.** When you use variables in your SQL queries, they are automatically passed as parameters to the database driver rather than being interpolated directly into the SQL string. ### How It Works When you write: ```yaml {contract=board-root} variables: priority: input: text status: input: text queries: tickets: sql: | SELECT * FROM dundersign.tickets WHERE priority = '{{ priority }}' AND {{ filter('status', status) }} source: my_postgres ``` dbt Charts converts this to a parameterized query: ```sql -- SQL sent to database: SELECT * FROM dundersign.tickets WHERE priority = $1 AND status = $2 -- Parameters sent separately: params = ['normal', 'new'] ``` This separation ensures that: - **Malicious input cannot alter query logic** - Even if a user enters `'; DROP TABLE tickets; --` as input, it's treated as a literal string value, not SQL code - **Database can cache query plans** - The same parameterized SQL structure enables query plan reuse - **No escaping needed** - The database driver handles type conversion and escaping automatically ### Filter Helper Behavior The `filter()` and `filter_date_range()` helpers return `1=1` (always true) when the variable is `None`, `undefined`, or empty. This means: - **Filter applied:** `{{ filter('priority', 'normal') }}` → `priority = $1` (with 'normal' as parameter) - **Filter skipped:** `{{ filter('priority', None) }}` → `1=1` (all rows match, no filtering) This pattern allows optional filtering where unset variables don't restrict results. #### Deny-on-null fallback For mandatory-scope dashboards (audit views, customer-specific dashboards, anything where the unfiltered superset is the wrong default), pass `none='deny'` to flip the fallback from `1=1` to `1=0` (zero rows): - **`{{ filter('priority', None, none='deny') }}` → `1=0`** (no rows when unset) - **`{{ filter('priority', 'normal', none='deny') }}` → `priority = $1`** (normal filter when set) `none` is keyword-only; valid values are `'allow'` (default) and `'deny'`. Anything else raises `ValueError`. ### Security Validations The parameterized filter helpers include additional security measures: 1. **Operator validation**: Only valid SQL operators (=, !=, >, <, >=, <=, LIKE, IN, etc.) are allowed. Invalid operators raise an error. 2. **Column name validation**: Column names must contain only letters, numbers, underscores, and optionally one dot (for `table.column` format). ### Best Practices - **Use `filter()` for user-controlled values** - Always use the filter helpers for variables that come from user input - **Don't construct SQL from user strings** - Avoid patterns like `WHERE {{ user_column }} = ...` where column names come from user input - **Validate at the variable level** - Use variable validation (data types, options) to restrict allowed values --- ## Related - [Variables](variables/index.md) - Using variables in filters - [Expressions](variables/expressions.md) - Variable references and Jinja expressions - [Charts](charts/index.md) - Visualizing query data - [CLI Reference](cli/index.md) - Validate, render, serve, and more - [YAML Schema Reference](reference/yaml-reference.md) - Complete field reference - [Troubleshooting Guide](guides/troubleshooting.md) - Common query issues # Overview (https://docs.dbtcharts.com/sources/) # Sources Sources define where a board reads data from. Most projects use one warehouse connection for SQL queries, but a board can also read files, HTTP endpoints, inline values, or dbt models. Connection sources are defined once, in the project's root `dbt_charts.yml`, as a named `sources:` registry. A board (or `charts/meta.yml`) never defines a connection inline; it only references a source by name: ```yaml {contract=project-config} # dbt_charts.yml sources: analytics: type: dbt_profile profile: my_dbt_project target: dev ``` ```yaml {contract=board-root} source: analytics # References the "analytics" source above queries: tickets: SELECT * FROM dundersign.tickets ``` Every `sources:` block on this page belongs in `dbt_charts.yml`, not in a board. A board's own `source:` field always holds a name (or, for a single colocated data file, an inline path; see [Inline File Sources](#inline-file-sources)), never a connection definition. dbt Charts `sources:` are connection definitions. They are separate from dbt `sources:` resources, which document raw tables inside a dbt project. If you are setting up dbt source tables, use dbt's [source properties](https://docs.getdbt.com/reference/source-properties) reference. ## Source Types For warehouse-backed SQL, dbt Charts uses dbt-compatible connection settings. In most dbt projects, use `type: dbt_profile` and let dbt own the adapter-specific credentials. Direct warehouse source types use the same field names as dbt profiles, but they are flat because boards read data; they do not build dbt models. You do not need `profiles.yml` for direct sources such as `postgres`, `snowflake`, `bigquery`, `duckdb`, or `sqlite`. dbt Charts uses the direct source block as the connection config. The dbt links below are still useful because they document the same adapter credential fields. | Source | dbt Charts setup | Credential fields / setup | |---|---|---| | Existing dbt project | `type: dbt_profile` | [dbt profiles.yml](https://docs.getdbt.com/docs/local/profiles.yml) | | BigQuery | `type: bigquery` | [BigQuery connection](https://docs.getdbt.com/docs/local/connect-data-platform/bigquery-setup) | | DuckDB | `type: duckdb` | [DuckDB connection](https://docs.getdbt.com/docs/local/connect-data-platform/duckdb-setup) | | Postgres | `type: postgres` | [Postgres connection](https://docs.getdbt.com/docs/local/connect-data-platform/postgres-setup) | | Redshift | `type: redshift` | [Redshift connection](https://docs.getdbt.com/docs/local/connect-data-platform/redshift-setup) | | Snowflake | `type: snowflake` | [Snowflake connection](https://docs.getdbt.com/docs/local/connect-data-platform/snowflake-setup) | | SQL Server | `type: dbt_profile` | [dbt-sqlserver connection](https://docs.getdbt.com/docs/local/connect-data-platform/mssql-setup) (install `dbt-charts[sqlserver]`) | | Microsoft Fabric | `type: dbt_profile` | [dbt-fabric connection](https://docs.getdbt.com/docs/local/connect-data-platform/fabric-setup) (install `dbt-charts[fabric]`) | | Trino / Starburst | `type: trino` | [Trino](#trino) (install `dbt-charts[trino]`) | | Athena | `type: dbt_profile` | [dbt-athena connection](https://docs.getdbt.com/docs/local/connect-data-platform/athena-setup) (install `dbt-charts[athena]`) | | ClickHouse | `type: dbt_profile` | [dbt-clickhouse connection](https://docs.getdbt.com/docs/local/connect-data-platform/clickhouse-setup) (install `dbt-charts[clickhouse]`) | | SQLite | `type: sqlite` | dbt Charts direct source; no dbt setup | | Databricks | `type: databricks` | [Databricks](#databricks), token or OAuth service principal | | Lake compute | `type: lakecompute` | [Lake compute](#lake-compute) (install `dbt-charts[lakecompute]`) | | Spark | `type: dbt_profile` | [Apache Spark connection](https://docs.getdbt.com/docs/local/connect-data-platform/spark-setup) | | dbt models | `sql` query using `ref()` with a dbt profile source | [dbt profiles.yml](https://docs.getdbt.com/docs/local/profiles.yml) | | CSV files | `type: csv` | [Files and APIs](#files-and-apis) | | Parquet files | `type: parquet` | [Files and APIs](#files-and-apis) | | JSON files | `type: json` | [Files and APIs](#files-and-apis) | | HTTP endpoints | `type: http` query | [Files and APIs](#files-and-apis) | | Inline static data | `type: values` query | No source setup | **ClickHouse and `max_execution_time`.** `max_query_duration_seconds` is a ceiling. If your dbt profile's `custom_settings` already sets a stricter `max_execution_time`, dbt Charts keeps yours; a looser one is capped at the ceiling. ClickHouse types the setting as whole seconds and reads `0` as *no limit*, so a fractional or zero value is rejected rather than silently disabling the cap. **ClickHouse read-only users need `readonly = 2`.** dbt Charts caps every query with ClickHouse's `max_execution_time`, sent as a per-request setting. A `readonly = 1` user cannot change any setting, so the connection fails at setup with ClickHouse's own `Cannot modify 'max_execution_time' setting in readonly mode`. `readonly = 2` is the read-only mode ClickHouse gives BI clients: read-only queries, settings changes allowed. **SQL Server and `query_timeout`.** `max_query_duration_seconds` is enforced through your dbt profile's `query_timeout`, which T-SQL needs because it has no per-statement `SET`. A stricter `query_timeout` of your own is kept; `0` means no cap and is replaced by the ceiling. The `mssql-python` backend ignores `query_timeout`, so dbt Charts refuses it rather than run uncapped; use the default `pyodbc` backend. SQL Server also needs Microsoft ODBC Driver 17 or 18 installed on the host, which `pip` cannot provide. A query that fails on SQL Server is not retried, including one that failed because an idle session was dropped; run it again. ## dbt Profile Sources dbt Charts can either read your warehouse credentials from a dbt profile or accept a direct source definition with dbt-style field names. Use `dbt_profile` when your project already has `profiles.yml`: ```yaml {contract=project-config} # dbt_charts.yml sources: analytics: type: dbt_profile profile: my_dbt_project target: dev ``` dbt Charts resolves `profiles.yml` in this order: 1. `profiles_dir` on the source config 2. `DBT_PROFILES_DIR` 3. The linked dbt project directory, next to `dbt_charts.yml` by default, or an external directory named via `--dbt-project-dir` / `DBT_PROJECT_DIR` / `dbt_project_dir:` (see [`--dbt-project-dir`](../cli/index.md#-dbt-project-dir-path)) 4. `~/.dbt/profiles.yml` If none of those locations contains `profiles.yml`, dbt Charts raises an error. Without a linked dbt project, steps 2 and 4 still apply (`profiles_dir` is relative to the project, so it needs one). `profile:` is optional. When the source omits it, dbt Charts uses `DBT_PROFILE`, then the `profile:` in the linked dbt project's `dbt_project.yml`, the same order dbt uses. A board shipped inside a dbt package can therefore declare `type: dbt_profile` and follow whichever profile the consuming project names. If none of the three names a profile, the query fails with an error listing them. ### No `dbt_charts.yml` needed inside a dbt project When `dbt_project.yml` is at the project root and `dbt_charts.yml` declares no `sources:` (or there is no `dbt_charts.yml` at all), dbt Charts adds one source for you. It is named after the project's `profile:` and behaves as if you had written `type: dbt_profile` for it, so it shows up in `dct doctor` and is usable as `source: `. A SQL query that names no `source:` (on the query, at the board root, or in a `meta.yml`) runs against it. Boards shipped in a dbt package can leave `source:` out and follow whichever profile the consuming project names. Declaring any `sources:` turns the implicit source off, and a SQL query that names no source then fails with `ERR-SOURCE-NOT-NAMED`, as it does outside a dbt project. A `dbt_project.yml` with no `profile:` (a dbt package) gets no implicit source. On dbt Charts Cloud the implicit source appears as one unmapped connection to bind; Cloud never reads your `profiles.yml`. ### Which manifest `ref()` resolves against `target:` picks the connection, but `ref()` and `source()` resolve against a dbt manifest, and dbt does not record which target built it. With `target: prod` and a manifest from `dbt parse --target dev`, queries run against dev relations. Point the source at the manifest built for its target with `target_path:`, the same meaning as dbt's `--target-path`, relative to the dbt project directory: ```yaml {contract=project-config} # dbt_charts.yml sources: analytics: type: dbt_profile profile: my_dbt_project target: prod target_path: target/prod ``` ```bash dbt parse --target prod --target-path target/prod ``` Without `target_path:`, the manifest is `/manifest.json`, where `` is `DBT_TARGET_PATH`, then `target-path:` in `dbt_project.yml`, then `target`. The directory must be relative to the dbt project (`ERR-DBT-TARGET-PATH-INVALID` otherwise). A missing manifest at the resolved path raises `ERR-DBT-MANIFEST-MISSING` naming it; dbt Charts never falls back to another one. A query inlined through `{{ queries.X }}` resolves its `ref()` calls against the including query's source. #### On dbtcharts.com dbtcharts.com has no manifest to build and resolves `ref()` and `source()` against your production warehouse only. It reads the model, seed, snapshot and source definitions in your dbt project on the default branch, lists the tables in the project's default source, and links each name to the table that exists there. A `target/manifest.json` committed to the repository still wins. dbt macros in board SQL are not supported: put the logic in a dbt model and `ref()` it. A name that cannot be linked fails its chart with `ERR-DBT-REF-UNKNOWN-NODE` (or `ERR-DBT-SOURCE-UNKNOWN-TABLE`) instead of guessing a table. The usual reasons: - The model has not been built in production yet, or is ephemeral or disabled. - Its schema, alias or database comes from Jinja or a macro rather than a literal value in `dbt_project.yml`, a properties file or `config()`. - Its name exists in more than one candidate schema, or the project overrides `generate_alias_name` or `generate_database_name`. - It lives in another database than the default source's. A project with no default source, or whose first index is not built yet, fails `ref()` charts with `ERR-DBT-MANIFEST-MISSING`. The index rebuilds when dbt files on the default branch change and when the default source changes. A chart that fails on a name the index lacks, on a missing manifest, or with `ERR-BINDER-UNKNOWN-RELATION` (the warehouse reports the table or view does not exist) in a board that uses `ref()` or `source()` also asks for a rebuild, so a table a production run creates or moves is picked up by the next render. That request is made at most once every five minutes. ### Who validates the target `profiles.yml` is dbt's file, so **dbt validates it**; dbt Charts applies no schema of its own. Every field your installed dbt adapter accepts works, including `threads` and dbt's canonical `database:` / `schema:` spelling for BigQuery (`project:` / `dataset:` work too, exactly as in dbt). A field dbt requires but your target omits is reported when the dashboard runs, naming the profile and target. The flip side: a misspelled key is **not** an error. dbt ignores keys it does not recognize, so `hostt: localhost` is silently unused, by dbt Charts and by `dbt run` alike. If a connection behaves unexpectedly, check the spelling of your target's fields. This differs from the direct warehouse sources below, where dbt Charts owns the schema and an unknown key *is* an error. Installing that adapter is a separate step: dbt Charts connects through a Python adapter package, which ships as an extra. See [Warehouse adapters](../guides/installation.md#warehouse-adapters). ## Direct Warehouse Sources Direct warehouse sources are useful for small projects that do not share a dbt project. This is the full connection config; no `profiles.yml` file is required. For larger dbt projects, prefer `type: dbt_profile`. Postgres (`connect_timeout`) and BigQuery (`job_creation_timeout_seconds`) give up connecting after 5 seconds unless you set a different value. Redshift's `connect_timeout` is the socket timeout for the whole connection, so unset it follows `max_query_duration_seconds` plus a small headroom. ### Postgres ```yaml {contract=project-config} # dbt_charts.yml sources: analytics: type: postgres host: "{{ env_var('ANALYTICS_HOST') }}" port: 5432 dbname: analytics schema: public user: "{{ env_var('ANALYTICS_USER') }}" password: "{{ env_var('ANALYTICS_PASSWORD') }}" ``` ### DuckDB ```yaml {contract=project-config} # dbt_charts.yml sources: local_db: type: duckdb path: ./data/analytics.duckdb schema: crm ``` DuckDB sources are **local-only**. A `.duckdb` file is a version-coupled, single-writer binary, so dbt Charts Cloud does not read `.duckdb` files from connected repositories. For Cloud, export the tables to Parquet or CSV and register them as [file sources](#files-and-apis) instead. ### SQLite ```yaml {contract=project-config} # dbt_charts.yml sources: local_sqlite: type: sqlite path: ./data/analytics.sqlite ``` ### Databricks A Databricks source connects to a SQL warehouse and authenticates with exactly one of two credential sets: a personal access token, or an OAuth machine-to-machine (M2M) service principal. Databricks recommends the service principal for automation, and it is the only option when your admins have turned tokens off. ```yaml {contract=project-config} # dbt_charts.yml sources: db: type: databricks host: "{{ env_var('DATABRICKS_HOST') }}" http_path: /sql/1.0/warehouses/abc123def456 catalog: main schema: analytics client_id: "{{ env_var('DATABRICKS_CLIENT_ID') }}" client_secret: "{{ env_var('DATABRICKS_CLIENT_SECRET') }}" ``` For a personal access token, replace the two service principal fields with `token`: ```yaml {contract=project-config} # dbt_charts.yml sources: db: type: databricks host: "{{ env_var('DATABRICKS_HOST') }}" http_path: /sql/1.0/warehouses/abc123def456 catalog: main schema: analytics token: "{{ env_var('DATABRICKS_TOKEN') }}" ``` Setting both credential sets, only one of `client_id` and `client_secret`, or neither is an error; dbt Charts never picks a method for you. Create the secret under Settings, Identity and access, Service principals, Secrets in your workspace, and grant the principal `CAN USE` on the SQL warehouse (see the [Databricks OAuth M2M guide](https://docs.databricks.com/aws/en/dev-tools/auth/oauth-m2m)). A literal `client_secret` or `token` in `dbt_charts.yml` is rejected; use `env_var()`. `connect_timeout` is how many seconds a connection attempt may keep retrying while the SQL warehouse starts, 15 by default. `socket_timeout` bounds each socket read separately. ### Trino Trino connects through the `trino` Python client rather than a dbt adapter. Install the extra (`pip install "dbt-charts[trino]"`). ```yaml {contract=project-config} # dbt_charts.yml sources: lake: type: trino host: trino.example.com port: 8443 catalog: analytics schema: public user: ana method: ldap password: "{{ env_var('TRINO_PASSWORD') }}" ``` The field names are the keys of a dbt `type: trino` profile target. `catalog` and `database` both name the catalog. `method` picks the way to authenticate; leave it out to connect as `user` with no credential: | `method` | Fields | |---|---| | none (default) | `user` | | `ldap` | `user`, `password`, `impersonation_user` (optional) | | `jwt` | `jwt_token` | | `certificate` | `client_certificate`, `client_private_key` | | `oauth`, `oauth_console` | none; signs in through a browser or a printed link | | `kerberos`, `gssapi` | `principal`, `krb5_config`, `service_name` (all optional) | Every authenticated method connects over HTTPS. The server certificate is verified unless you set `cert: false`; `cert` also takes the path to a CA bundle. Kerberos reads its keytab from the `KRB5_CLIENT_KTNAME` environment variable, so `keytab:` on the source is an error. `max_query_duration_seconds` is sent as the `query_max_run_time` session property. If `session_properties` sets `query_max_run_time` itself, the shorter of the two applies. Column types come from Trino's own result metadata. `array`, `map` and `row` values arrive as JSON text. A type dbt Charts does not read (an `interval`, a `time with time zone`, a sketch type) fails the query with a message naming the column; `CAST` it in the query, for example to `varchar`. **Through `profiles.yml`.** A `type: trino` target works with `type: dbt_profile` and takes the same keys as the direct source, plus dbt's `method: none` for no authentication. `threads:` governs `dbt run`, so dbt Charts reads past it. Unlike other targets, dbt Charts validates this one itself, so an unknown key is an error. `target_path:` on the `dbt_profile` source is not supported for Trino and raises. If your dbt project's own target is Trino, a query that uses `{{ ref() }}` with no `source:` fails with `ERR-DBT-TARGET-NEEDS-SOURCE`. Declare a `dbt_profile` source for the target and set `source:` on the query. ### Lake compute Lake compute has no dbt Core adapter. dbt Charts connects through the same ADBC driver dbt v2 uses, which is why the source type is spelled the way dbt v2 spells it: `lakecompute`. `lake_compute` and `alt` are not accepted. Install the extra (`pip install "dbt-charts[lakecompute]"`). The driver itself is downloaded from dbt's CDN the first time a query runs, checked against a pinned SHA-256, and cached where dbt v2 keeps its own. A machine that has already run dbt v2 downloads nothing. ```yaml {contract=project-config} # dbt_charts.yml sources: lake: type: lakecompute token: "{{ env_var('DBT_COMPUTE_AUTH_TOKEN') }}" ``` The field names are dbt v2's profile keys, flat. `method` picks the way to authenticate, and each method takes only its own fields: | `method` | Fields | |---|---| | `token` (default) | `token` | | `api_key` | `api_key` | | `okta_browser` | `okta_auth_url`, `okta_token_url`, `okta_client_id` (all optional) | | `fivetran` | `fivetran_credential`, `fivetran_auth_url` (optional) | Every method also accepts `base_url` and `catalog_bundle`, both optional: ```yaml {contract=project-config} # dbt_charts.yml sources: lake: type: lakecompute method: fivetran fivetran_credential: "{{ env_var('FIVETRAN_PAT') }}" base_url: https://api.lake-compute.fivetran.com/ catalog_bundle: analytics ``` A field from another method (`token` next to `api_key`), `database:`, `schema:`, `attribution:`, or any key not listed above is an error rather than being ignored. `base_url` defaults to the production endpoint, set in `dbt_charts.yml` as `execution.lakecompute.default_base_url`. dbt v2 also reads `DBT_COMPUTE_BASE_URL` for that default; dbt Charts does not read environment variables itself. If you point dbt v2 at another endpoint through that variable, write `base_url: "{{ env_var('DBT_COMPUTE_BASE_URL') }}"` so both agree. Lake compute runs DuckDB, so write SQL as DuckDB SQL (`dialect: lakecompute` on a query names it). `max_query_duration_seconds` is sent to the driver as its own per-query timeout. If the backend attaches a warning to a result, such as an export limit that cut the rows short, the query fails with `ERR-WAREHOUSE-RESULT-WARNING` instead of rendering a partial result. **Through `profiles.yml`.** A `type: lakecompute` target works with `type: dbt_profile`, without dbt Core in the picture. The target takes the same keys as the direct source above (`method`, `token`, `base_url`, and so on), so a target named `lake` in the `analytics` profile is used like this: ```yaml {contract=project-config} # dbt_charts.yml sources: lake: type: dbt_profile profile: analytics target: lake ``` `database:`, `schema:`, `threads:`, `snowflake_pat_duration_days:` and `organization:` in the target govern what `dbt run` writes or how dbt itself authenticates, so dbt Charts reads past them. `path:` selects dbt's local DuckDB engine and is rejected. A target written as a list of connections, or with a `lakecompute:` sibling key, is dbt v2's multi-connection shape, which dbt Charts does not read yet. `attribution:` and `target_path:` on the `dbt_profile` source are not supported for lake compute yet and raise. If your dbt project's own target is lake compute, a query that uses `{{ ref() }}` with no `source:` fails with `ERR-DBT-TARGET-NEEDS-SOURCE`. Declare a source like the one above and set `source:` on the query. ## Files and APIs CSV, Parquet, JSON, and HTTP sources do not need dbt. They are simpler local dbt Charts source definitions for examples, prototypes, and lightweight external data. ### CSV A CSV source is a namespace of relations. Each key under `files:` is a table name; the value is the path to the CSV file relative to the project root. ```yaml {contract=project-config} # dbt_charts.yml sources: sales_csv: type: csv files: sales: assets/data/sales.csv ``` Then query it by name: ```yaml {contract=board-root} queries: sales: source: sales_csv sql: SELECT * FROM sales ``` A single source can expose multiple CSV files as separate tables: ```yaml {contract=project-config} # dbt_charts.yml sources: sales_data: type: csv files: orders: assets/data/orders.csv returns: assets/data/returns.csv ``` ### JSON ```yaml {contract=project-config} # dbt_charts.yml sources: product_json: type: json files: products: assets/data/products.json ``` ```yaml {contract=board-root} queries: products: source: product_json sql: SELECT * FROM products ``` ### Parquet ```yaml {contract=project-config} # dbt_charts.yml sources: events: type: parquet files: events: assets/data/events.parquet ``` ```yaml {contract=board-root} queries: event_counts: source: events sql: SELECT * FROM events ``` ### Guardrails Two limits bound a single file source's growth: `execution.file_source_max_tables` caps the number of table entries a single `files:` map may declare (default 500), and `execution.file_source_max_bytes` caps each table's uncompressed byte size (default 5 GB), summed across all files matched by its path or glob. Every format is measured the same way: the file's own bytes for CSV and JSON, and for Parquet the uncompressed total recorded in the file's footer, so choosing the compact format never costs you budget. Both are hard errors, not truncation. Narrow the `files:` map, raise the limit in `dbt_charts.yml`, or use a database connection for data at that scale. A hosted deployment may pin its own ceiling, and a project's setting can only lower it, never raise it; where a ceiling is what fired, the limit below is not the knob that moves: ```yaml {contract=project-config} # dbt_charts.yml execution: file_source_max_tables: 100 file_source_max_bytes: 1000000000 # 1 GB ``` ### Exploring a file source `dct query` runs ad-hoc SQL against a file source directly, the same way it does against a warehouse source: ```bash dct query sales_csv 'SELECT * FROM sales LIMIT 5' ``` DuckDB has to materialize the source's files (read and parse them) before it can answer, and the materialized tables land in an in-memory cache that dies with the process. A *following* `dct query` invocation — a fresh process — gets no benefit from it and re-reads the files from scratch. The warmth only pays off inside one long-lived process that runs multiple queries against the source, such as `dct serve`, a single render, or one `dct validate --warehouse` sweep. ### HTTP Unlike SQL and file queries, an HTTP query is self-contained: set `url:` (and optional `headers:`, `method:`, `body:`) directly on the query. It has no `source:` field; there is no named registry entry to reference. Runs under local `dct` only; Not supported on dbtcharts.com. ```yaml {contract=board-root} queries: forecast: type: http url: "https://api.example.com/forecast" ``` See [Queries](../queries.md#3-http-api-queries) for the full HTTP query field reference. ## Inline File Sources A single CSV, JSON, or Parquet file that only one query reads doesn't need a named registry entry; reference the file directly on `source:`, as a path containing `/` or ending in a data-file extension: ```yaml {contract=board-root} queries: sales: source: ./data/sales.csv sql: SELECT * FROM sales ``` The path resolves against the board file's own directory or the project root, whichever exists (both existing at once is a compile error naming both). The table name is the file's stem (`sales.csv` → table `sales`); a stem that isn't a valid SQL identifier is a compile error rather than a silently sanitized name; rename the file or use the named-registry form below. Only a single file is allowed inline (no `files:` list); an inline file source is always materialized straight from the file's git content. Use the named-registry form (`type: csv`/`json`/`parquet` with a `files:` map in `dbt_charts.yml`, above) instead when a file is shared across boards or a source exposes more than one file as separate tables. ## Source Defaults Set `source:` on a board to make all queries in that board use a default source by name. A `charts/meta.yml` (or a directory's own `meta.yml`) can set the same field to default every board beneath it: ```yaml {contract=board-root} source: finance queries: monthly_revenue: sql: SELECT month, revenue FROM finance_revenue sales_pipeline: source: sales sql: SELECT stage, amount FROM sales_pipeline ``` Sources are resolved from most specific to broadest: 1. Query-level `source:` 2. Board-level `source:` 3. The nearest ancestor `source:` in the `charts/meta.yml` cascade An unknown source name is a validation error. ## Attribution Every query dbt Charts sends carries attribution so your warehouse's cost tracking can tell a dbt Charts query apart from ad-hoc human querying. On BigQuery it arrives as [job labels](https://docs.cloud.google.com/bigquery/docs/adding-labels#job-label), queryable as a real column in `INFORMATION_SCHEMA.JOBS`; on other warehouses it arrives as a leading JSON comment on the query text. dbt Charts sets the engine half itself; you don't configure it: | Label | Meaning | | --- | --- | | `app` | Always `dbt-charts`. The one key to filter on; dbt emits `app=dbt` under the same key, so one predicate covers both. | | `dbt_charts_version` | The `dct` version that sent the query. | | `dbt_charts_surface` | Which entry point: `cli`, `serve`, `mcp` (the local `dct mcp serve` server), or `cloud`. | | `dbt_charts_board` | The dashboard the query belongs to. | | `dbt_charts_query` | The query's name within that dashboard. | | `dbt_charts_target` | The dbt target, when the query names one. | | `dbt_charts_tool` | The AI tool call that issued the query, such as `execute_query`. Absent for queries no tool issued. | A host that knows *who* is asking adds three more, opaque per-request labels; no emails or display names, since warehouse query history is broadly readable and long-retained: | Label | Meaning | | --- | --- | | `dbt_charts_actor` | Opaque ID of the person or agent who caused the query. | | `dbt_charts_client` | Opaque ID of the software that asked: a browser session, an AI copilot turn, a connected MCP client (`connector` for Cloud's hosted connector). | | `dbt_charts_request` | A fresh opaque ID per call, correlating this query to the host's own audit log. | Only Cloud sets these today; it is the only host with a signed-in principal. The CLI and `dct serve` have no notion of "who," so they omit all three rather than inventing a value. Like `dbt_charts_board`/`_query`/`_target`/`_tool`, only BigQuery gets `dbt_charts_actor`, `_client`, and `_request` as real key/value labels; on every other warehouse they ride the query comment along with the rest of the per-request payload (see below), so finding them means matching the comment text, not filtering a column. ### Where it lands, per warehouse Only BigQuery stores the whole payload as structured key/value. Elsewhere dbt Charts fills the warehouse's own identity field so `app` is a column you can filter, and the rest of the payload rides the query comment: | Warehouse | Native field | Carries | | --- | --- | --- | | BigQuery | job labels (`JOBS.labels`) | everything, as key/value | | Snowflake | `QUERY_TAG` | `app`, version, surface, as JSON | | Databricks | query tags | `app`, version, surface, as JSON | | Trino | `client_tags` | `app`, version, surface | | Postgres | `application_name` | `dbt-charts` | | Redshift, Athena, ClickHouse, DuckDB, SQLite | none | query comment only | | Lake compute | none | nothing; `attribution:` is not supported yet | Setting the native field costs nothing: it travels with the connection, not with each query. If you have already set `query_tag`, `application_name`, or `client_tags` yourself, dbt Charts leaves your value alone. Redshift and Athena are deliberately left out: Redshift's `query_group` routes WLM queues and Athena's `work_group` routes data limits and engine settings, so writing either could move your queries somewhere else, too high a price for a label. ClickHouse's `log_comment` is inert but is a setting, which a `readonly = 1` user cannot send at all, and the query comment already lands in `system.query_log`. ### Your own labels Add ownership labels with `attribution:` on a source. They apply to every query against it: ```yaml {contract=project-config} # dbt_charts.yml sources: warehouse: type: bigquery project: my-gcp-project dataset: analytics attribution: team: architecture-analytics cost_center: data-platform ``` Keys and values must match BigQuery's label rules: keys `[a-z][a-z0-9_-]{0,62}`, values `[a-z0-9_-]{0,63}`, on every warehouse, so the same source config behaves identically everywhere. A value that doesn't match is an error, not a silently rewritten label. The `dbt_charts_` prefix and the `app` key are reserved for the engine. `env_var()` works here like anywhere else in a source: ```yaml {contract=project-config} # dbt_charts.yml sources: warehouse: type: bigquery project: my-gcp-project dataset: analytics attribution: env: "{{ env_var('DCT_ENV', 'dev') }}" ``` To find your dbt Charts spend on BigQuery: ```sql SELECT creation_time, total_bytes_billed, user_email FROM `region-us`.INFORMATION_SCHEMA.JOBS WHERE creation_date = CURRENT_DATE() AND EXISTS (SELECT 1 FROM UNNEST(labels) l WHERE l.key = 'app' AND l.value = 'dbt-charts') ``` This works whether the query ran under a service account or an individual developer's credentials. ## Environment Variables Use `env_var()` in source configuration to keep credentials out of YAML files: ```yaml {contract=project-config} # dbt_charts.yml sources: analytics: type: postgres host: "{{ env_var('DB_HOST', 'localhost') }}" user: "{{ env_var('DB_USER') }}" password: "{{ env_var('DB_PASSWORD') }}" ``` Store credentials in `.env` or your deployment environment: ```bash DB_HOST=localhost DB_USER=analytics_user DB_PASSWORD=secret123 ``` !!! warning "Keep credentials out of boards" `env_var()` is for source configuration. Do not put secrets in board YAML, query text, or rendered output. ## Read-only Posture dbt Charts boards read data. They never intentionally write to your warehouse, but connection-level enforcement depends on the source type. DuckDB and SQLite are opened with driver-level read-only settings. Every other warehouse should use credentials that can only read data. dbt Charts also applies an in-process SQL allowlist before execution, but SELECT-only credentials are the warehouse-side defense. | Warehouse | In-process enforcement | Recommended operator posture | |---|---|---| | DuckDB | Driver read-only mode | Handled by dbt Charts | | SQLite | Driver read-only URI mode | Handled by dbt Charts | | Postgres | SQL allowlist only | SELECT-only role | | Snowflake | SQL allowlist only | SELECT-only role | | BigQuery | SQL allowlist only | `roles/bigquery.dataViewer` or equivalent | | Redshift | SQL allowlist only | SELECT-only IAM role or database user | | Lake compute | SQL allowlist only | SELECT-only token or credential | | dbt profile sources | Depends on adapter | SELECT-only profile credential | ### File access from SQL `dct serve` runs strictly read-only: author SQL cannot reach the filesystem or network through DuckDB, so functions like `read_csv(...)`, `read_json_auto(...)`, `read_parquet(...)`, and `httpfs` are not available in query SQL. Point data at a file source instead; declare it with `type: csv`, `type: json`, or `type: parquet` and a `files:` mapping (see [Files and APIs](#files-and-apis)), then query the table by name. dbt Charts parses the file and loads it for querying without granting SQL any file access. ## Related - [Queries](../queries.md) for the SQL, values, HTTP, and schema query types (dbt models are queried as plain SQL via `ref()`, not a separate query type) - [Quick Guide](../quick-guide.md) for the shortest syntax overview # Overview (https://docs.dbtcharts.com/charts/) # Charts Charts visualize your query data. Each chart references a query and defines how to display it. **Vega-Lite foundation**: Charts render through Vega-Lite, but authored chart YAML is a typed dbt Charts surface. Use top-level dbt Charts fields (`x`, `y`, `color`, `lookup`, `value`, etc.) and the typed `style` object exclusively. Arbitrary Vega-Lite `spec`, `mark`, `encoding`, `config`, `transform`, `params`, and composition (`hconcat`/`vconcat`/`concat`/`repeat`/`resolve`) blocks are rejected on the authored surface. 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 the typed `style` object. --- ## Basic Chart Structure ```yaml {contract=board-fragment} charts: revenue_chart: title: "Revenue by Month" query: queries.sales # References a query name type: bar # Chart type x: month # X-axis dimension y: total_revenue # Y-axis metric ``` Here's a working example: ```dbt-charts-example source: examples_db charts: revenue_chart: 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: - revenue_chart ``` --- ## Common Chart Types | Type | Best For | |------|----------| | `line` | Time series trends | | `bar` | Comparisons across categories | | `area` | Stacked trends over time | | `scatter` | Relationships between metrics | | `pie` / `donut` | Part-to-whole comparisons | | `heatmap` | Two-dimensional patterns | | `histogram` | Distributions | | `table` | Detailed data view | | `kpi` | Single metric display | | `callout` | Status or warning message card | | `map` | Geographic region maps | | `point_map` | Location markers | | `bubble_map` | Sized location markers | Every built-in type, the shapes that compose from them, and what to do when a shape is missing: [Chart types and extensibility](extensibility.md). **See also:** [Bar Charts](bar-charts.md) • [Line Graphs](line-graphs.md) • [Sector Charts](sector-charts.md) • [Point Charts](point-charts.md) • [Area Charts](area-charts.md) • [Layered Charts](layered-charts.md) • [Maps](maps.md) • [Tables and Text](tables.md) --- ## Field Reference ```yaml {contract=unverified} charts: : # Unique identifier (must be unique within a file and across all imported files) title: string # Optional query: string # Required type: string # Required (bar, line, area, etc.) # Channel fields x: string # Dimension for x-axis y: string | [string] # Metric(s) for y-axis color: string # Field to encode as color size: string # Field to encode as size shape: string # Field to encode as shape # KPI-specific value: string # KPI value binding or map fill field (column reference) label: string # KPI label above the value # Map-specific geo_source: string # Built-in geographic data source lookup: string # Query field to join to geography latitude: string # Point/bubble map latitude field longitude: string # Point/bubble map longitude field # Data and links sort: by: string order: asc | desc link: string # URL template for drill-down links ({{ x }}, {{ y }}, etc.) # Chart-specific presentation style: color: categorical: palette: string # Named categorical palette background: string # Chart SVG background fill legend: visible: boolean # false to hide legend axis: grid: visible: boolean # false to hide grid lines orientation: horizontal | vertical # Bar chart orientation stack: none | zero | normalize | center # Stack mode columns: column_name: {} # Table column config (mapping key = column name) # Axis, scale, legend, mark, and other style sections... notes: string # AI search/context metadata; never rendered ``` Row limiting belongs on the query (`queries..limit`), not on the chart. --- ## Chart Sizing Three fields control a chart's preferred geometry. `height` and `width` live at **chart root**, directly under `charts.:`; `aspect_ratio` is a style field, under the chart's `style:`. ```yaml {contract=board-fragment} charts: revenue_trend: query: revenue type: bar x: month y: total height: 400 # exact pixel height; wins over aspect_ratio and theme wide_chart: query: revenue type: line x: month y: total width: 800 # preferred dashboard width; the layout may allocate less style: aspect_ratio: 3.0 # wider than default; height = width / 3.0 ``` - **`height`**: fixes the pixel height exactly. Bypasses `min_height`/`max_height` clamps. Not supported on `kpi`, `table`, `callout`, or `spark_bar`. - **`width`**: pins the chart's slot in a `rows:` layout (a fixed footprint, capped at the row). In `cols:` and `grid:` layouts it requests an intrinsic width for dashboard measurement on boards with no width of their own; the layout still owns the final slot allocation. - **`style.aspect_ratio`**: controls shape without pinning a size. The theme default is `1.5` (3:2 landscape). Not supported on the same chart types as `height`. The theme-level form `style.charts.kpi.aspect_ratio` is also rejected; the per-type theme override is unsupported on `kpi`, `table`, `callout`, and `spark_bar`. The dashboard combines preferred widths recursively: rows and tabs use the widest child, while columns add child widths and gaps. A `style.frame.width` is the board's exact width wherever it is authored; without one, `style.frame.max_width` is the maximum the content hug can grow to. Layout rules still own the final chart allocation. See [Board Sizing](../boards/sizing.md) for the full three-layer model (layout tile → chart root → theme default) and guidance on when to use each layer. --- ## Chart Styling Customize chart appearance: ```yaml {contract=board-fragment} charts: styled_chart: query: sales type: bar x: month y: total_revenue style: color: categorical: palette: editorial-10 # Color palette legend: visible: true # Show legend (default) axis: grid: visible: true # Show grid lines background: "#f8fafc" # Chart background fill ``` **Style options:** - `color.categorical.palette`: Color palette name (for example, `editorial-10`, `vivid-10`, `dbt-seq-blue`; see the [Palettes guide](../guides/palettes.md)) - `legend.visible`: `false` to hide the legend - `axis.grid.visible`: `false` to hide grid lines - `background`: SVG/CSS-compatible background fill applied behind the chart See [Themes](../themes.md) for the built-in themes, and [Styling](../styling.md) for color palettes. ### Value Labels Bar, line, and scatter charts can show the numeric value on each mark. Enable per-chart via the family marks path: ```yaml {contract=unverified} style: bar: # or line: / scatter: marks: bar: # or line: / point: labels: visible: true # show value above each bar position: top # above | top | middle | middle_aligned | bottom (bar); top | bottom | left | right | middle (line/point) ``` `format` defaults to `null` (inherits from `axis_y` format). Set a d3-format string to override. See per-chart docs for the full field list: [Bar Charts](bar-charts.md#value-labels) · [Line Graphs](line-graphs.md#value-labels) · [Point Charts](point-charts.md#value-labels). --- ## Best Practices ### Choosing Chart Types - **Time series**: Use `line` or `area` - **Comparisons**: Use `bar` - **Relationships**: Use `scatter` - **Single numbers**: Use `kpi` - **Detailed data**: Use `table` ### Effective Color Encoding - Use `color` to distinguish categories - Choose accessible palettes (colorblind-friendly) - Limit distinct colors (5-7 max) ### Filtering - Use variables with dropdowns for cross-chart filtering - Tooltips appear automatically on hover See [Interactions](interactions.md) for hover tooltips and `link:` drill-down links. --- ## Previewing Charts Preview your charts interactively: ```bash dct serve ``` `dct serve` auto-discovers the project and prints the URL on startup. See [CLI Reference](../cli/serve.md). ### Validate First ```bash dct validate charts/sales.yml ``` --- ## Related - [Bar Charts](bar-charts.md) - Comparison-first charts and columns - [Line Graphs](line-graphs.md) - Line-graph style, examples, Vega-Lite mapping, and gaps - [Sector Charts](sector-charts.md) - Pie, donut, and related arc-based charts - [Point Charts](point-charts.md) - Dot plots, scatterplots, and other point-based charts - [Area Charts](area-charts.md) - Filled trend and composition charts - [Layered Charts](layered-charts.md) - Combo charts and dual-axis charts - [Maps](maps.md) - Geographic charts and projection-aware mapping - [Tables and Text](tables.md) - Exact-value tables, KPIs, and other data-powered text outputs - [Interactions](interactions.md) - Hover tooltips and `link:` drill-down links - [Queries](../queries.md) - Data sources for charts - [Boards](../boards/index.md) - Organizing charts in dashboards - [Variables](../variables/index.md) - Variables in query filters and inputs - [Themes](../themes.md) - The built-in themes and how to write your own - [Styling](../styling.md) - Color palettes and the rest of the style surface # Bar Charts (https://docs.dbtcharts.com/charts/bar-charts/) # 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](./axis-labels.md) 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](../reference/yaml-reference.md). --- ## 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](#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 ```dbt-charts-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](line-graphs.md#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. ```dbt-charts-example 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. ```dbt-charts-example 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: orientation: horizontal axis_y: labels: format: currency_whole 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:` naming `revenue` on a `stack: zero` chart whose `y` is `revenue`: the bar's length *is* the total. A `y: [...]` 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 with `stack: none` the 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`](../reference/errors.md#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](#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. ```dbt-charts-example 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. ```dbt-charts-example 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 when each category holds one color (as in a waterfall) and is an error otherwise. 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.grouped.padding_inner`, `style.grouped.padding_outer` | band scale `paddingInner`/`paddingOuter` | `float` (`0`–`1`) | Grouped bars only. Gap between groups and margin at both plot edges, as a share of the band step. Default `0.2` each. | | `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. | | `style.marks.bar.max_size` | `mark.width` | `float` (pixels) | The thickest a bar may be. Caps a band-scale bar's `band_width` fraction on wide charts and a continuous scale's computed width; unset, category bars stay uncapped and a continuous scale caps at 20. An authored `size` wins over it. | ### Bar Width This example thins the bars to 40% of their band, leaving visible whitespace between categories. ```dbt-charts-example 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. ```dbt-charts-example 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 axis_x: labels: max_width: 140 axis_y: scale: nice: true labels: format: currency_whole 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 when `stack` is omitted and a `color` field 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 `color` encoding and no `y: [...]` list**: a single series has nothing to name. (`y: [...]` and a `color:` column compose: one series per column value per measure, named ` - `; see [Multiple Measures by a Dimension](./line-graphs.md#multiple-measures-by-a-dimension). `layers:` still cannot combine with `y: [...]`.) - **`color:` names the same field as `x:`**, 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](./small-multiples.md). - **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 cumulative `0..Σ` 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 ```dbt-charts-example 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 ```dbt-charts-example 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`. ```dbt-charts-example 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. To label plain bars inside their top but give a stack one total outside it, set `labels.stacked: false` and `total_label.visible: true`: stacked segments go unlabeled while `total_label` marks each stack, and plain bars keep their labels. `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. ```yaml {contract=board-fragment} 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](layered-charts.md#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. --- ## Related - [Charts Overview](index.md) - [Line Graphs](line-graphs.md) - [Layered Charts](layered-charts.md) - [Sector Charts](sector-charts.md) # Line Graphs (https://docs.dbtcharts.com/charts/line-graphs/) # Line Graphs Line graphs connect values across an ordered axis, usually time, to emphasize continuity, direction, rate of change, and overall shape; this family includes single- or multi-series lines and slope graphs. They are most useful when the main question is how something changes over time rather than how big isolated categories are. dbt Charts line graphs use a small set of top-level shorthand fields together with `style`. In most cases, you only need `query`, `type: line`, `x`, and `y` to get started. 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 typed `style` objects. 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](../reference/yaml-reference.md). --- ## Minimum Required for a Line Graph These are the minimum fields required to render a basic line graph in dbt Charts. | dbt Charts field | Maps to Vega-Lite | Allowed values | Notes | |---|---|---|---| | `query` | `data.values` | Query name or query reference | Supplies the dataset. | | `type: line` | `mark.type: "line"` | Literal `line` | Selects the line mark. | | `x` | `encoding.x.field` | Field name | Ordered dimension, usually time. | | `y` | `encoding.y.field` | Field name | Numeric measure to plot. | ### Minimum Example ```dbt-charts-example source: examples_db charts: revenue_trend_minimal: query: sql: | SELECT date, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY date ORDER BY date type: line title: Daily Revenue x: date y: revenue rows: - revenue_trend_minimal ``` --- ## Top-Level Chart Fields These are the top-level chart properties you set directly on a line graph 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. | | `type: line` | `mark.type: "line"` | Literal `line` | Selects the line mark. | | `x` | `encoding.x.field` | Field name | Usually a temporal or ordered field. | | `y` | `encoding.y.field` | Field name or list of field names | A list creates layered multi-metric lines. | | `title` | `title.text` | String | Chart title. | | `notes` | metadata | String | AI search/context metadata. Never rendered. | | `color` | `encoding.color.field` | Field name | Groups a line into multiple series. | | `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](#field-labels). | | `format` | `encoding.y.format` and `encoding.y.axis.format` | Format string | Numeric formatting for the y channel. | | `sort` | categorical axis sort | Sort object | Most useful when the x-axis is categorical. | | `projection` | top-level `projection` | Vega-Lite projection name | Available for Vega-Lite projection overrides. | ### Multi-Series Line Graph This example adds `color` to split one line into multiple series while keeping the top-level chart definition compact. ```dbt-charts-example source: examples_db charts: revenue_by_product: query: sql: | SELECT DATE_TRUNC('week', date) AS week, product, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY week, product ORDER BY week, product type: line title: Revenue by Product x: week y: revenue color: product rows: - revenue_by_product ``` ### Axis Labels, Formatting, and Styling This example shows common top-level chart fields such as labels and numeric formatting. ```dbt-charts-example source: examples_db charts: revenue_formatted: query: sql: | SELECT date, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY date ORDER BY date type: line title: Revenue Over Time x: date y: revenue x_label: Date y_label: Revenue style: axis_y: scale: continuous: zero: false labels: format: currency_whole axis: grid: visible: false rows: - revenue_formatted ``` ### Layered Multi-Metric Lines When `y` is a list, dbt Charts creates a layered line graph. This is useful when you want to compare two measures that share the same ordered x-axis. ```dbt-charts-example source: examples_db charts: revenue_and_units: query: sql: | SELECT date, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY date ORDER BY date type: line title: Revenue And Units Sold x: date y: [revenue, units_sold] rows: - revenue_and_units ``` A pivoted result (several measures grouped by a dimension) adds `color:` naming the dimension; see [Multiple Measures by a Dimension](#multiple-measures-by-a-dimension). ### Field Labels Every surface that names a column (legend entries and titles, tooltip rows, axis titles) derives its text from the column name: `units_sold` becomes `units sold` (title-cased in legend titles). `field_labels` maps a column to the text to paint instead, written exactly as given. `x_label`/`y_label` and a layer's `label` still win on their own surface. A key that names no column of the chart's query is an error (checked when the query returns rows). Tables use `style.columns..label` instead. ```yaml {contract=board-root} charts: revenue_and_units_labeled: query: sql: | SELECT date, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY date ORDER BY date type: line title: Revenue And Units Sold x: date y: [revenue, units_sold] field_labels: revenue: Revenue (USD) units_sold: Units shipped rows: - revenue_and_units_labeled ``` --- ## 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 line graphs without changing the underlying data bindings. ```dbt-charts-example source: examples_db charts: revenue_formatted: query: sql: | SELECT date, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY date ORDER BY date type: line title: Revenue Over Time x: date y: revenue x_label: Date y_label: Revenue style: axis_y: scale: continuous: zero: false labels: format: currency_whole axis: grid: visible: false rows: - revenue_formatted ``` --- ## Axis and Scale Style Use `style` for axis and scale properties that shape how the line graph is framed and read. | dbt Charts field | Maps to Vega-Lite | Allowed values | Notes | |---|---|---|---| | `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). | ### Axis and Scale Controls This example uses `style` to control the y-axis scale and tick density. ```dbt-charts-example source: examples_db charts: revenue_formatted: query: sql: | SELECT date, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY date ORDER BY date type: line title: Revenue Over Time x: date y: revenue x_label: Date y_label: Revenue style: axis_y: scale: continuous: zero: false labels: format: currency_whole axis: grid: visible: false rows: - revenue_formatted ``` ### Log Scale Set `style.axis_y.scale.continuous.type: log` to plot a quantitative axis on a logarithmic scale, useful when values span multiple orders of magnitude (for example, exponential growth). Log scales are undefined at zero and for negative numbers; dbt Charts validates the domain at compile time and raises `ERR_LOG_SCALE_REQUIRES_POSITIVE_DATA` if any data value is zero or negative. Only use `axis_y.scale.continuous.type: log` on a measure you know is strictly positive. ```dbt-charts-example source: examples_db charts: revenue_log: query: sql: | SELECT date, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY date ORDER BY date type: line title: Revenue Over Time (Log Scale) x: date y: revenue x_label: Date y_label: Revenue style: axis_y: scale: continuous: type: log labels: format: currency_whole rows: - revenue_log ``` --- ## Endpoint Labels A multi-series line graph names its series **on the chart**, anchored to each line's final point, rather than in a side legend. This is the default on every built-in theme; you don't switch it on. Set `style.endpoint_labels.visible: false` to send the names back to a legend. | 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. | When the label pane is on, the categorical legend turns off automatically: the two would encode the same series→color mapping twice. The y-axis also auto-flips to the **left** so it doesn't collide with the right-edge label pane. On narrow cards, endpoint-label text compacts to fit the available width while its automatic vertical spacing stays readable for closely ending series. Endpoint labels require a multi-series chart: either a `color` channel or a `y: [field_a, field_b]` list. On single-series lines there is only one series to name, so the feature is a no-op. `y: [...]` composes with a `color:` column: the measures are crossed with the column's values, one series per value per measure, each named ` - `; see [Multiple Measures by a Dimension](#multiple-measures-by-a-dimension). `layers:` still cannot combine with `y: [...]`; dbt Charts rejects that at compile time. ```dbt-charts-example source: examples_db charts: revenue_by_product: query: sql: | SELECT DATE_TRUNC('week', date) AS week, product, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY week, product ORDER BY week, product type: line title: Revenue by Product x: week y: revenue color: product style: endpoint_labels: visible: true rows: - revenue_by_product ``` --- ## Smoothing & Step Lines `style.marks.line.curve` controls how adjacent points are joined, in place of the default straight-segment interpolation: | Value | Effect | |---|---| | `monotone` | A smooth curve that never overshoots the data; no wobble past a local min or max. A softer read than straight segments without misrepresenting the trend. | | `step` | On a categorical (nominal/ordinal) x-axis, a full-band-width plateau per x-value instead of a diagonal segment. On a continuous (temporal/quantitative) x-axis, Vega-Lite's own native step interpolate (points at band centers). | ```dbt-charts-example source: examples_db charts: revenue_smoothed: query: sql: | SELECT date, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY date ORDER BY date type: line title: Daily Revenue (Smoothed) x: date y: revenue style: marks: line: curve: monotone rows: - revenue_smoothed ``` ### Step Lines `curve: step` on a categorical x column draws a full-band-width plateau. Adjacent plateaus are joined by a vertical bridge by default; set `connect: false` to leave them as disconnected segments instead. Disconnected plateaus end flush with the band edge; each one is its own path, so the default round line cap would overhang every edge by half the stroke width and close up the gap between neighboring bands. Set `disconnected_cap: round` under `marks.line` to opt back into rounded ends. `disconnected_width` under `marks.line` (default `0.9`) sets the share of the band each plateau spans, so neighboring plateaus at a similar value stay visually separate. A `stroke.dasharray` on a disconnected step line is scaled per plateau so ink lands on both edges: a whole number of dashes, separated by gaps, spans the plateau. The rendered lengths therefore differ slightly from the authored ones. Connected step lines are unaffected: they are a single continuous path, so `stroke.cap` still governs their two outer ends. ```dbt-charts-example source: examples_db charts: revenue_by_product_step: query: sql: | SELECT product, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY product ORDER BY revenue DESC type: line title: Product Revenue (Step) x: product y: revenue style: marks: line: curve: step connect: false rows: - revenue_by_product_step ``` ### Halo Multi-series lines that cross each other can be hard to trace right at the crossing point. `style.marks.line.halo_multiplier` draws a knockout-colored stroke behind each line, sized relative to the line's own stroke width, so crossings stay legible. Set it to `0` to disable the halo. --- ## Value Labels Show the numeric value beside each data point on a line chart by enabling `style.marks.line.labels.visible`. ```dbt-charts-example source: examples_db charts: revenue_labeled: query: sql: | SELECT DATE_TRUNC('week', date) AS week, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY week ORDER BY week type: line title: Weekly Revenue x: week y: revenue style: marks: line: labels: visible: true position: top # top | bottom | left | right | middle format: ",.1f" # omit to inherit axis_quantitative.format font: color: "#6b7280" size: 11 rows: - revenue_labeled ``` **Position values:** | Value | Placement | |-------|-----------| | `top` | Above the point, default | | `bottom` | Below the point | | `left` | To the left of the point | | `right` | To the right of the point | | `middle` | Centered on the point | `dx`/`dy` (pixel offsets) are also available under `labels` to nudge a label off its default position. Line and point charts share the same position vocabulary and Vega-Lite mapping. On a `curve: step` line over a categorical x, the mark is a plateau spanning the whole band rather than a point, so `left` and `right` resolve against the band's leading and trailing edge instead of the datum at its center, otherwise the caption would land on top of the mark it is meant to sit beside. This tracks the real band width, so no `dx` is needed to clear the mark at any board width or category count. Where the band is the first or last in the domain and the preferred side has no room, that one label falls back to `top`, keeping it inside its own band rather than over its neighbor's. `top`, `bottom` and `middle` are unaffected: band width is horizontal, and the band's center is the mark's center. --- ## Overlays (`layers:`) Add a `layers:` list to a line chart to overlay additional marks: a bar for context, a scatter reference, or a second line on a separate y-axis. The base line chart owns the x-axis, frame, title, legend, and sort. Each layer contributes its own mark and legend entry. ```yaml {contract=board-fragment} charts: trend_with_target: type: line x: month y: actual query: monthly layers: - type: line y: target label: Target axis_y: position: right title: Target style: marks: line: stroke: width: 2 dasharray: "4 2" ``` A layer on its own axis gets an independent scale; see [Dual y-axis](layered-charts.md#dual-y-axis) for why dbt Charts warns about it and what to use instead. For a dual-axis example where the overlay draws from a separate query: ```yaml {contract=board-root} queries: monthly_signups: columns: [month, signups] values: - ["2024-01", 480] - ["2024-02", 560] weekly_conversion: columns: [month, conversion_pct] values: - ["2024-01", 12.5] - ["2024-02", 14.2] charts: signups_and_conversion: type: line x: month y: signups query: monthly_signups layers: - type: line y: conversion_pct label: Conversion (%) query: weekly_conversion axis_y: position: right title: Conversion (%) scale: domain: [0, 100] ``` Layer x-values from a separate query extend the base x-scale automatically; no `x_domain` field is needed or accepted. Layer fields: `type`, `y`, `label`, `color`, `query`, `x`, `axis_y`, `style` (marks-only patch). `sort:` is base-only. --- ## Authored Surface dbt Charts line charts are authored with `type: line` plus top-level channels such as `x`, `y`, and `color`. Arbitrary Vega-Lite `spec`, `mark`, `encoding`, `config`, `transform`, `params`, and composition blocks are rejected on the authored surface. Use top-level dbt Charts fields and the typed `style:` object. --- ## Multiple Measures by a Dimension A pivoted result, several measures as columns grouped by a dimension, is the shape every semantic layer and most warehouse rollups produce. Add `color:` naming the dimension and each of its values is crossed with each measure: two measures by three categories draws six lines, each named ` - `. The legend, endpoint labels, and tooltip all read those composite names. ```dbt-charts-example source: examples_db charts: order_size_by_category: query: sql: | SELECT date, category, AVG(revenue) AS avg_order_value, MAX(revenue) AS largest_order FROM ecommerce_orders GROUP BY date, category ORDER BY date, category type: line title: Average And Largest Order by Category x: date y: [avg_order_value, largest_order] color: category rows: - order_size_by_category ``` --- ## Related - [Charts Overview](index.md) - [Layered Charts](layered-charts.md) - [Sector Charts](sector-charts.md) # Sector Charts (https://docs.dbtcharts.com/charts/sector-charts/) # Sector Charts Sector charts divide a whole into angular parts, making the total-and-parts relationship visually explicit; this family includes pie charts, donut charts, and polar-area charts. They are most useful when showing simple composition with a small number of categories, especially when the main message is share of a whole rather than precise comparison between similar values. dbt Charts sector charts use a small set of top-level shorthand fields. In most cases, you only need `query`, `type: pie`, `theta`, and `color` to get started. 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 the typed `style` object. This page demonstrates every sector-specific authored capability with at least one runnable example. For the field-level source of truth (default ownership and the shared style properties every chart family inherits: palette, border, padding, font, tooltip) see the [YAML Schema Reference](../reference/yaml-reference.md). --- ## A Complete Sector Chart The full package: a donut that reports a formatted grand total in the center and labels every slice with its category, share of the whole, and value. The rest of the page breaks these capabilities apart field by field. ```dbt-charts-example source: examples_db style: frame: width: 700 charts: revenue_share_hero: query: sql: | SELECT category, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC type: donut title: Revenue by Category theta: revenue color: category total: label: Total Revenue style: total: value: format: "$,.0f" marks: slice: labels: template: "{{ '{:.0%}'.format(percent) }} {{ category }}\n{{ '${:,.0f}'.format(revenue) }}" rows: - revenue_share_hero ``` --- ## Minimum Required for a Sector Chart These are the minimum fields required to render a basic sector chart in dbt Charts. | dbt Charts field | Maps to Vega-Lite | Allowed values | Notes | |---|---|---|---| | `query` | `data.values` | Query name or query reference | Supplies the dataset. | | `type: pie` | `mark.type: "arc"` | Literal `pie` | Selects the standard pie form. | | `theta` | `encoding.theta.field` | Field name | Numeric field that sets wedge size. | | `color` | `encoding.color.field` | Field name | Category field that splits the whole into slices. | ### Minimum Example ```dbt-charts-example source: examples_db style: frame: width: 700 charts: revenue_share_minimal: query: sql: | SELECT category, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC type: pie title: Revenue Share by Category theta: revenue color: category rows: - revenue_share_minimal ``` --- ## Top-Level Chart Fields These are the top-level chart properties you set directly on a sector 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. | | `type` | `mark.type: "arc"` | `pie` or `donut` | `donut` is shorthand for a pie with an inner radius (see `style.inner_radius`). | | `theta` | `encoding.theta.field` | Field name | Required wedge-size field. | | `color` | `encoding.color.field` | Field name | Category field that splits the whole into slices. | | `title` | `title.text` | String | Chart title. | | `subtitle` | `title.subtitle` | String | Secondary line displayed under the title. | | `notes` | metadata | String | AI search/context metadata. Never rendered. | | `link` | drill-down URL | URL template string | Click-through target; `{{ column }}` tokens interpolate per slice. | | `total` | center total slot | `{visible, label}` | Donut center total: the summed value with an optional caption. The number format is styled separately at `style.total.value.format`. | | `labels` | per-slice text | `{template, where}` | Direct per-slice labels. `template` is a Jinja string over each row's columns plus the injected `percent`, `value`, `color`, and `total`; `where` is a Jinja expression that filters which slices are labeled. Omitting `labels` applies the default two-line template: `percent` and the slice name on the first line, `value` on the second. | | `height` | chart viewport height | Positive number (pixels) | Explicit chart height. Overrides `style.aspect_ratio` and the theme cascade when set. | | `width` | chart viewport width | Positive number (pixels) | Chart width. Pins the chart's slot in a `rows:` layout (capped at the row); in `cols:` and `grid:` it contributes to the dashboard's intrinsic width and the layout owns the final slot. | ### Title, Subtitle, and Drill-Down Link `title` and `subtitle` render above the chart. `notes` and `link` are non-visual: `notes` feeds AI search and tooling (never rendered), and `link` turns each slice into a click-through (here into that category's page). (The center total and its formatting are shown in [A Complete Sector Chart](#a-complete-sector-chart) above.) ```dbt-charts-example source: examples_db style: frame: width: 700 charts: revenue_share_composition: query: sql: | SELECT category, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC type: pie title: Product Mix subtitle: Fiscal year to date notes: Share of total revenue by product category. link: "/category/{{ category }}" theta: revenue color: category style: marks: slice: labels: template: "{{ category }}" rows: - revenue_share_composition ``` ### Slice Label Templates Override the default template with `style.marks.slice.labels.template`. Following the default's convention, lead with `percent`. Three donuts on the same data: a compact percent-and-name line, a name over a currency-formatted value, and a `where`-filtered label that names only the largest slices. ```dbt-charts-example source: examples_db style: frame: width: 700 queries: by_category: sql: | SELECT category, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC charts: labels_compact: query: by_category type: donut title: Percent and Name theta: revenue color: category total: visible: false style: marks: slice: labels: template: "{{ '{:.0%}'.format(percent) }} {{ category }}" labels_currency: query: by_category type: donut title: Name and Value theta: revenue color: category total: visible: false style: marks: slice: labels: template: "{{ category }}\n{{ '${:,.0f}'.format(revenue) }}" labels_filtered: query: by_category type: donut title: Largest Slices Only theta: revenue color: category total: visible: false style: marks: slice: labels: template: "{{ category }}" where: "percent >= 0.3" rows: - labels_compact - labels_currency - labels_filtered ``` --- ## Style Fields Use `style` for the sector-specific presentational surface: ring geometry, slice paint, and the center-total paint. Shared style properties every chart inherits (palette, border, padding, font, tooltip) live in the [YAML Schema Reference](../reference/yaml-reference.md). | dbt Charts field | Maps to Vega-Lite | Allowed values | Notes | |---|---|---|---| | `style.inner_radius` | `mark.innerRadius` | `0`–`1` | Hole-to-disk ratio. `0` (or omitted) is a solid pie; `type: donut` defaults it to `0.6`. Set it explicitly for any other ring thickness. | | `style.aspect_ratio` | viewport ratio | Positive number | Width ÷ height of the chart viewport. | | `style.marks.slice` | arc mark paint | `{opacity, gap, corner_radius, stroke, labels}` | Per-slice mark paint and layout. | | `style.legend.position` | legend or table placement | `{edge, align}` | Where the key sits: `edge` is `left`, `right`, `top` or `bottom`; `align` is `start`, `center` or `end` along that edge. Unset, the key sits right or below depending on width. A pie key never overlays the wheel. | | `style.total` | center total paint | `{value, label}` | Typography of the donut center total's number (`value`, which also carries its `format`) and caption (`label`). | ### Ring Thickness `style.inner_radius` sets any hole-to-disk ratio beyond the `donut` default of `0.6`. `0.3` is a thick ring; values near `1` are a thin band. ```dbt-charts-example source: examples_db style: frame: width: 700 charts: revenue_share_ring: query: sql: | SELECT category, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC type: pie title: Thick Ring theta: revenue color: category total: visible: false style: inner_radius: 0.3 marks: slice: labels: template: "{{ category }}" rows: - revenue_share_ring ``` ### Viewport `style.aspect_ratio` reshapes the chart viewport: its width ÷ height. Above `1` is wider than tall. ```dbt-charts-example source: examples_db style: frame: width: 700 charts: revenue_share_viewport: query: sql: | SELECT category, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC type: pie title: Wide Viewport theta: revenue color: category style: aspect_ratio: 1.6 marks: slice: labels: template: "{{ category }}" rows: - revenue_share_viewport ``` ### Slice Paint `style.marks.slice` paints the arcs: here noticeably rounded corners and a wide angular gap between slices. ```dbt-charts-example source: examples_db style: frame: width: 700 charts: revenue_share_slice_paint: query: sql: | SELECT category, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC type: donut title: Rounded, Gapped Slices theta: revenue color: category total: visible: false style: marks: slice: corner_radius: 10 gap: 0.05 labels: template: "{{ category }}" rows: - revenue_share_slice_paint ``` ### Center-Total Styling `style.total` styles the center-total typography. Here the total value is painted in a distinct accent color. ```dbt-charts-example source: examples_db style: frame: width: 700 charts: revenue_share_total_style: query: sql: | SELECT category, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC type: donut title: Accented Total theta: revenue color: category total: label: Total style: total: value: format: "$,.0f" font: color: "#2563eb" marks: slice: labels: template: "{{ category }}" rows: - revenue_share_total_style ``` --- ## Slice Count and Density How a sector chart labels itself is **automatic and data-and-width aware**; you do not author it. As slice count rises or the chart narrows, direct labels stop fitting, so the renderer switches presentation. Understanding the transitions is the difference between a chart that reads cleanly and one that collapses into unreadable callouts. The rules, at a glance: - Wedges under **8%** of the whole keep their slice and hover tooltip but drop their direct label: small slices don't get to crowd the disk. - Once the number of labelable wedges exceeds what fits around the disk (about 4 at a typical width, fewer when narrow), the chart renders as a donut plus an **attached table** that carries slice identity, color, and value. - Very narrow charts always use the attached table. ### Small Slices Drop Their Label Four slices, one of them under 8%. The small wedge still renders (and its value is in the tooltip), but it gets no direct label rather than cluttering the disk. ```dbt-charts-example source: examples_db style: frame: width: 700 queries: long_tail: type: values columns: [segment, revenue] values: - ["Enterprise", 50] - ["Mid-Market", 30] - ["SMB", 15] - ["Trial", 5] charts: small_slices: query: long_tail type: donut title: Small Slices Suppressed theta: revenue color: segment total: visible: false style: marks: slice: labels: template: "{{ segment }}" rows: - small_slices ``` ### High Cardinality Attaches a Table Eight products is more than fits as direct callouts, so the chart renders the donut with an attached table instead, each row keyed by slice color, with the value alongside. ```dbt-charts-example source: examples_db style: frame: width: 700 charts: by_product: query: sql: | SELECT product, SUM(units_sold) AS units FROM ecommerce_orders GROUP BY product ORDER BY units DESC type: donut title: Units Sold by Product theta: units color: product rows: - by_product ``` --- ## Explicit Height and Width `height` and `width` pin the chart's viewport in pixels, overriding `style.aspect_ratio` and the theme cascade. Use them when a sector chart needs a fixed footprint rather than one derived from the surrounding layout. ```dbt-charts-example source: examples_db style: frame: width: 400 charts: revenue_share_fixed_size: query: sql: | SELECT category, SUM(revenue) AS revenue FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC type: pie title: Fixed 320x320 Viewport theta: revenue color: category height: 320 width: 320 rows: - revenue_share_fixed_size ``` --- ## Key Placement `style.legend.position` places the key: the legend when one is shown (`style.legend.visible: true`), or the attached table when the chart attaches one. Here the legend sits left of the wheel and is centered down its height. ```dbt-charts-example source: examples_db style: frame: width: 900 charts: by_product_left: query: sql: | SELECT product, SUM(units_sold) AS units FROM ecommerce_orders GROUP BY product ORDER BY units DESC type: donut title: Legend Left, Centered theta: units color: product style: legend: visible: true position: edge: left align: center rows: - by_product_left ``` --- ## Field Labels The legend title and tooltip rows derive their text from the column name. `field_labels` maps a column to the text to paint instead, written exactly as given; the donut's center total reads `Total