GestaltBI la forma all’origine del significato

dbtcharts

@gestaltbi/dbtcharts

dbt Charts board YAML, drawn from GestaltBI’s own data.

Reads the dbt Charts board format — layout, charts, variables — and resolves it against the rows GestaltBI’s pipeline produces. Renders nothing: it works out what each chart shows, and the host decides how it looks.

It reads the format and nothing else. There is no SQL, no warehouse and no dct here.

npm install @gestaltbi/dbtcharts on npm →

The shape of it

import { readBoard, boardSources, resolveBoard } from '@gestaltbi/dbtcharts';

const { board, problems } = readBoard(yamlText);

boardSources(board);            // the processes to load: ['date_agg', 'prod_agg']

const resolved = resolveBoard(board, {
  frames: (process) => rowsFor(process),
  variables: { region: 'North' },
  label: (column) => labelFor(column),
});

resolved.layout     // rows, cols, grid, tabs — charts referenced by id
resolved.charts     // each chart's data, ready to draw
resolved.variables  // each control's value and options

Resolving is pure: the same board, rows and variable values always give the same result, so a host calls it again whenever any of the three change.

A query points at a process

In dbt Charts a query is usually SQL run in a warehouse. GestaltBI has neither, so a query names a process from processing.json and reads its rows whole.

queries:
  monthly:
    source: date_agg

charts:
  revenue:
    type: line
    query: queries.monthly
    x: uatu:date
    y: [smartbi:calc:daily_revenue:sum, smartbi:calc:daily_cost:sum]

source rather than a key of our own, because every query shape in the dbt Charts schema rejects properties it does not define — the tests check that process: would have failed. A query with a source and no sql stays valid dbt Charts YAML, so a file named *.board.yml still gets dbt Labs’ VS Code extension’s autocomplete and schema checks.

Inline data needs no process at all, exactly as in dbt Charts:

queries:
  targets:
    columns: [family, target]
    values:
      - [Tools, 1200]
      - [Garden, 800]

What is supported

Queriessource: <process>, inline rows, compact columns + values
Chartskpi table bar line area scatter pie donut heatmap spark_bar callout
Layoutrows cols grid tabs details, and charts: declared inside any of them
Variablescolumn binding, every input type, default, required, operator, static and query options
Textmarkdown, with {{ name }} and one level of {% if name %}

And what is not — each reported rather than guessed at:

  • SQL, HTTP and schema queries, and file paths. Each is reported with what to write instead.
  • histogram and the four map types. They resolve as unavailable with a reason, so a host can show the gap in place.
  • style and theme. GestaltBI’s design system decides how GestaltBI looks.
  • cache and incremental. Nothing is cached or refreshed, so there is nothing for them to change.

Charts hold no arithmetic

In the dbt Charts schema every encoding is a bare column name — x is a string, y a string or a list, color a string — and aggregation belongs to the query. So resolving a chart is mostly reading columns out of rows in the right order.

The one thing added is a sum. GestaltBI frames are not always pre-aggregated, so when several rows land on one bar, cell or slice they are added up — and reported as reaggregated, never done silently. dbt Charts warns about the same situation.

Order

Axes and pivot columns follow the order dbt Charts documents: dates chronological, numbers ascending, strings in the order the query returned them. A chart with sort follows the sorted rows instead. Dates print as whole days — nothing is truncated to a month.

Variables filter rows

In dbt Charts a variable becomes a bound SQL parameter and the query re-runs. Here the rows are already in hand, so a variable narrows them. For a query that passes rows straight through, the two give the same answer.

variables:
  region:
    column: region
    input: select
  • Unset — null, empty, an empty list — means no filter, as dbt Charts’ filter() returns 1=1.
  • A daterange includes the day it ends on.
  • A warehouse path like sales.orders.region falls back to its last segment.
  • Options come from a static list, a query’s column, or the column’s own values before filtering — so picking one never hides the rest.
  • A required variable with no value holds the charts back and is listed in blockedBy.

Reaching the totals

Narrowing finished frames cannot move a total: once rows are summed, the column a variable filters is gone. pipelineFilter translates variables into the filter GestaltBI’s own filter ops apply upstream of every aggregate, which is the GestaltBI equivalent of dbt Charts re-running the query.

const { filter, clientOnly } = pipelineFilter(board, values, rawColumns);
// { 'smartbi:delivery_region': ['Lombardia'], 'uatu:date': { between: [from, to] } }

What cannot go upstream is listed in clientOnly — operators the filter ops lack, and columns the raw data does not have, since a key no row carries would match nothing and empty every chart.

Dates

Read in local time. GestaltBI parses dates at local midnight, so in Rome the first of January is 2019-12-31T23:00Z, and a UTC reading labels every day as the day before. The suite runs in Europe/Rome and America/New_York, because it passed in UTC with exactly that bug in it.

Kept honest against dbt Labs’ schema

The tests validate every fixture board against dbt-charts-board.schema.json, copied unmodified from the dbt Charts VS Code extension 0.2.1 (publisher dbtLabsInc, Apache-2.0 — the licence sits beside it). They include the published KPI and pivot examples as written, a board in the GestaltBI dialect, and the check that process: would have been rejected.

The schema is pinned by hash. dbt Charts is pre-1.0 and will change; replacing the file with a newer version fails a test until someone has read what changed. The churn arrives as a red build, not as a board that quietly reads wrong.

Reference

Extracted from src/index.ts when this page was built, so it cannot drift from what the package actually exports.

Functions

applyVariables

fn
function applyVariables(board: Board, rows: Row[], values: Record<string, unknown>): Row[]

Apply every variable a frame can answer to that frame.

Client-side, which is the one real difference from dbt Charts: there a variable becomes a bound SQL parameter and the query re-runs. Here the rows are already in hand, so a variable narrows them. For a query that passes rows straight through, the two give the same answer.

boardSources

fn
function boardSources(board: Board): string[]

The processes a board reads, so a host knows what to load before resolving.

checkSources

fn
function checkSources(board: Board, processes: string[]): Problem[]

Report sources this config does not have.

Separate from resolving because a host usually knows its process list long before it has any rows: this is the check to run when a board is opened.

day

fn
function day(v: unknown): string | null

A calendar day as YYYY-MM-DD, or null when the value is not a date.

Read in local time, never through toISOString. GestaltBI parses dates with moment, which lands them on local midnight — so in Rome the first of January is 2019-12-31T23:00Z, and a UTC reading labels every day in the file as the day before. A date-only string is already a day and is taken as written.

interpolate

fn
function interpolate(text: string | undefined, values: Record<string, unknown>, path: string, problems: Problem[]): string | undefined

The slice of Jinja board prose actually uses: {{ name }}, and one level of {% if name %} … {% else %} … {% endif %} — the form in dbt Charts' own examples. Anything beyond that is left standing and reported, because a template quietly half-rendered reads worse than one shown as written.

keyOf

fn
function keyOf(v: unknown): string

How a value prints as a category key.

Dates print as whole local days — never truncated to months, and never shifted by reading them in UTC. A time of day is shown only when there is one.

matches

fn
function matches(variable: VariableSpec, column: string, value: unknown, row: Row): boolean

Whether a row survives one variable.

ordered

fn
function ordered(rows: Row[], column: string): Category[]

The distinct values of a column, in the order dbt Charts documents.

"Dates and numbers chronological or ascending; strings first-seen from the query." Applied to axes as well as pivot columns so a board reads the same way in both tools. A column that mixes kinds keeps query order — sorting apples against dates has no answer worth giving.

pipelineFilter

fn
function pipelineFilter(board: Board, values: Record<string, unknown>, upstreamColumns: string[]): PipelineFilter

Board variables, as a filter GestaltBI's pipeline can apply.

This is how a variable does what it does in dbt Charts — narrow the data before anything is totalled — rather than only trimming the frames that happen to still carry its column. In dbt Charts a variable re-runs the query; the GestaltBI equivalent is a filter op upstream of every aggregate, re-run with this value. A region picker then moves the totals, not only the chart that lists regions.

upstreamColumns are the columns of the raw dataset. A variable on a column missing there is kept out of the filter: the filter ops compare values directly, and a key no row has would match nothing and empty every chart.

readBoard

fn
function readBoard(input: string | Record<string, unknown> | null | undefined):

Read a board from YAML text, or from an object already parsed.

Always returns a board. A file with a bad query or an unknown chart still has a layout worth drawing, and the person who wrote it needs to see both what rendered and what did not — so problems are listed, never thrown.

resolveBoard

fn
function resolveBoard(board: Board, options: ResolveOptions): ResolvedBoard

Resolve a board against data.

Pure: the same board, rows and variable values always produce the same result, so a host can call it again whenever any of the three change.

resolveChart

fn
function resolveChart(chart: ChartSpec, input: Row[], ctx: ChartContext): ResolvedChart

Turn one chart and its rows into data a host can draw.

Charts in dbt Charts hold no arithmetic — every encoding is a bare column name, and aggregation belongs to the query. So resolving is mostly reading columns out of rows in the right order. The one thing added is a sum where several rows land on the same mark, because GestaltBI frames are not always pre-aggregated, and that is reported rather than done silently.

Interfaces

Board

interface
interface Board

A whole board, normalised: queries, charts and variables by name, and one layout tree.

title?: string
text?: string
source?: string

The board-wide default process, used by queries that name none.

queries: Record<string, QuerySpec>
charts: Record<string, ChartSpec>
variables: Record<string, VariableSpec>
layout: LayoutNode

Category

interface
interface Category

A category or series label, and the value it was made from.

key: string
value: unknown

ChartSpec

interface
interface ChartSpec

One chart, with every encoding flattened to plain column names.

id: string
type: string
query?: string

Name of the query it reads. Absent for a callout, which reads nothing.

title?: string
subtitle?: string
x?: string
y: string[]

Always a list: y: revenue and y: [revenue, cost] both arrive as an array.

color?: string
size?: string
theta?: string
value?: string
label?: string
message?: string
xLabel?: string
yLabel?: string
rows?: string[]

Pivot keys on a table.

columns?: string[]
values?: string[]
sort?: SortSpec
path: string

Dotted path to where the chart was defined, for messages.

GridCell

interface
interface GridCell

One cell of a grid: what it holds, and the span it takes.

node: LayoutNode
col?: number
row?: number
colSpan: number
rowSpan: number

PipelineFilter

interface
interface PipelineFilter

Board variables translated for a GestaltBI filter op.

filter is keyed by column in the shape @gestaltbi/stream's filter ops read: a list for "one of these", { between: [a, b] } for a range, null to clear a variable that was set before.

filter: Record<string, unknown[] | { between: [unknown, unknown] } | null>
clientOnly: string[]

Variables the pipeline cannot express — an operator the filter ops lack, or a column absent upstream. They still narrow the frames that carry their column, but cannot reach a total computed before them.

Problem

interface
interface Problem

Something about a board a person should be told.

severity: 'error' | 'warning'

error means a part of the board will not render. warning means it will, differently.

code: | 'yaml' | 'not-a-board' | 'unsupported-query' | 'unknown-query' | 'unsupported-chart' | 'unknown-chart' | 'duplicate-chart' | 'missing-field' | 'unknown-column' | 'unknown-source' | 'no-data' | 'reaggregated' | 'unbound-variable' | 'unknown-operator' | 'template'
path: string

Where in the board, as a dotted path: charts.revenue_kpi.value.

message: string

ResolvedBoard

interface
interface ResolvedBoard

A board resolved against data: everything a host needs to draw it, and nothing about how.

title?: string
text?: string
layout: LayoutNode
charts: Record<string, ResolvedChart>
variables: ResolvedVariable[]
blockedBy: string[]

Required variables with no value. While any are listed, charts are not drawn.

problems: Problem[]

ResolvedVariable

interface
interface ResolvedVariable

A variable as a control needs it: current value, and the options to offer.

name: string
input: string
label?: string
value: unknown
options: Array<{ value: unknown; label: string }>
bound: boolean

False when the variable names no column GestaltBI can filter by.

ResolveOptions

interface
interface ResolveOptions

What a board needs beyond itself: the rows, the variable values, and how to name a column.

frames: (source: string) => Array<Record<string, unknown>> | undefined

The rows a GestaltBI process produces, by process name.

Undefined means the host has nothing for it — not loaded yet, or not a process in this config — and the charts reading it say so.

variables?: Record<string, unknown>

Current variable values, by variable name. Missing means use the default.

label?: (column: string) => string

Human label for a column code. Defaults to the code itself.

optionsFor?: (column: string) => unknown[] | undefined

The distinct values of a column across the whole dataset, when the host knows them.

Offered as a variable's options in preference to the values in the frames. A host that filters its pipeline by variable (see {@link pipelineFilter}) hands over frames that are already narrowed — reading options from those would shrink a region list to the one region just chosen.

Series

interface
interface Series

One line or group of bars: a value per category, null where the data has none.

name: string
column?: string

Column the series was read from, so a host can label it.

data: Array<number | null>

SortSpec

interface
interface SortSpec

Which column orders a chart’s marks, and in which direction.

by: string
order: 'asc' | 'desc'

Tab

interface
interface Tab

One tab: its label, and what it shows when chosen.

title: string
content: LayoutNode

VariableOptionsSpec

interface
interface VariableOptionsSpec

Where a control’s choices come from: a static list, or a query’s column.

static?: unknown[]
query?: string
column?: string
labelColumn?: string

VariableSpec

interface
interface VariableSpec

A control on the board, and the column it narrows.

name: string
input: string
label?: string
column?: string
default?: unknown
required: boolean
options?: VariableOptionsSpec
operator?: string
min?: number
max?: number
step?: number

Types

ChartType

type
type ChartType = | 'bar' | 'histogram' | 'line' | 'area' | 'scatter' | 'heatmap' | 'pie' | 'donut' | 'kpi' | 'table' | 'point_map' | 'bubble_map' | 'map' | 'geoshape' | 'callout' | 'spark_bar'

Every chart family the board schema names. Not all of them resolve yet.

LayoutNode

type
type LayoutNode = | { kind: 'chart'; chart: string } | { kind: 'text'; markdown: string } | { kind: 'rows'; title?: string; items: LayoutNode[] } | { kind: 'cols'; title?: string; items: LayoutNode[] } | { kind: 'grid'; title?: string; columns: number; cells: GridCell[] } | { kind: 'tabs'; title?: string; position: 'top' | 'left'; initial?: string; tabs: Tab[] } | { kind: 'details'; summary: string; expanded: boolean; content: LayoutNode }

How the board is arranged. Charts are referenced by id, never inlined.

QuerySpec

type
type QuerySpec = | { name: string; kind: 'source'; source: string } | { name: string; kind: 'values'; rows: Record<string, unknown>[] } | { name: string; kind: 'unsupported'; reason: string }

Where a query's rows come from.

source is the GestaltBI binding: a query holding a source and no sql reads that process from processing.json. It is also valid dbt Charts YAML, which is why it was chosen over a key of our own — the schema rejects any property it does not define.

ResolvedChart

type
type ResolvedChart = | (ResolvedBase & { type: 'kpi'; label?: string; column: string; value: number | string | null }) | (ResolvedBase & { type: 'table'; pivot: boolean; columns: Array<{ key: string; column?: string }>; data: Array<Record<string, unknown>>; }) | (ResolvedBase & { type: 'bar' | 'line' | 'area'; x: string; xLabel?: string; yLabel?: string; categories: Category[]; series: Series[]; }) | (ResolvedBase & { type: 'scatter'; x: string; xLabel?: string; yLabel?: string; series: Array<{ name: string; column?: string; points: Array<{ x: number; y: number; size?: number }> }>; }) | (ResolvedBase & { type: 'pie' | 'donut'; slices: Array<{ label: string; value: number }> }) | (ResolvedBase & { type: 'heatmap'; xCategories: Category[]; yCategories: Category[]; cells: Array<{ x: number; y: number; value: number | null }>; min: number | null; max: number | null; }) | (ResolvedBase & { type: 'spark_bar'; bars: Array<{ label: string; value: number }> }) | (ResolvedBase & { type: 'callout'; message: string }) /** A chart that could not be drawn, and why. The host shows the reason in its place. */ | (ResolvedBase & { type: 'unavailable'; chartType: string; reason: string })

A chart’s data, ready to draw — or unavailable, with the reason to show in its place.

Constants

SUPPORTED_CHARTS

const
const SUPPORTED_CHARTS

Chart families this package can turn into drawable data.

UNSUPPORTED_CHARTS

const
const UNSUPPORTED_CHARTS

Families the schema defines that do not resolve yet, so they are reported rather than guessed at.