Dashboards

Build interactive dashboards combining charts, pivot tables, data tables, and reports into unified views.


Table of Contents

Everything You Need Is Already Here

You have the data. You have the data warehouse — DuckDB for fast embedded analytics, ClickHouse for enterprise scale. You have powerful OLAP engines that handle everything from 100 rows to 10 billion. You have data-driven web components — pivot tables, charts, data grids, and parameterized reports — ready to drop into any page.

So what would stop you from building the best BI dashboards in the world?

Nothing. Absolutely nothing.

Note: Below you will see various code snippets — scripts, HTML templates, component configurations. None of this code needs to be written by hand. ReportBurster's built-in AI tools generate everything for you. Just click the "Hey AI, Help Me with..." buttons and describe what you need.

Example: Get a Dashboard in 5 Minutes

Can you get this dashboard done in 5 minutes? Let's see...


ReportBurster - The Northwind Sales Dashboard — KPI cards, charts, data table, pivot table, and country filter

Want to see it before you build it? This exact dashboard ships as a built-in sample. Go to Samples — Try All and click Try It on the Sales Dashboard entry to open the live, working dashboard instantly — no configuration needed.


ReportBurster - Sales Dashboard sample in the Samples list — click Try It to open the live dashboard

Follow these steps to build a working Northwind Sales Dashboard — complete with KPI cards, charts, a data table, a pivot table, and a country filter. We'll use pre-canned configuration so you can focus on learning the workflow.

Note: The Northwind SQLite database already ships with ReportBurster and contains all the data needed for this tutorial. To get the dashboard up and running as fast as possible, simply copy each code snippet below into the corresponding tab — get your dashboard ready in 5 minutes (and, along the way, learn how to do it for yourself).

When building your own dashboards over your own data, you'd use the AI tools built into ReportBurster to generate all the code. We'll show you exactly where those buttons are at each step.

Step 1: Configure the Database Connection

The sample Northwind SQLite database is included with ReportBurster — no Docker or external setup needed. Go to Configuration → Connections (Email, Databases) and configure your connection.


ReportBurster - Database Connection dialog — SQLite, Northwind database

Click Test Connection & Fetch Database Schema. ReportBurster validates your connection and retrieves the complete database schema.


ReportBurster - Database Schema tab — Northwind tables listed

Step 2: Create the Report


ReportBurster - Create Report dialog — name the dashboard, Report Generation checked

Step 3: The Datasource

On the DataSource tab, set the Input Type to Script (for fetching dashboard data) and select the database connection you created in Step 1 (e.g., Northwind (default)) from the Database Connection dropdown.


ReportBurster - DataSource tab — Script input type, Groovy code editor with data script

In practice you'd let the built-in AI write this script for you. For now, grab the ready-made script below so we can jump straight to the dashboard.

import groovy.sql.Sql
 
def dbSql = ctx.dbSql
def componentId = ctx.variables?.get('componentId')
 
// Get filter parameters from ctx.variables (the correct API for accessing report parameters)
// Note: ctx.token may be null during data fetch, so use empty string as fallback
def userVars = ctx.variables.getUserVariables(ctx.token ?: '')
def country = userVars?.get('country')?.toString()
def filterByCountry = country && country != 'null' && country != 'All' && country != '-- All --' && country.trim() != ''
 
log.info("Dashboard params - componentId: {}, country: {}, filterByCountry: {}", componentId, country, filterByCountry)
 
// KPI base query (shared WHERE clause)
def kpiBase = """
    FROM Orders o
    JOIN "Order Details" od ON o.OrderID = od.OrderID
"""
if (filterByCountry) kpiBase += " WHERE o.ShipCountry = '${country}'"
 
// Component: atomicValues — single query returning all 4 KPI values as columns
if (!componentId || componentId == 'atomicValues') {
    def data = dbSql.rows("""
        SELECT
            ROUND(SUM(od.UnitPrice * od.Quantity * (1 - od.Discount)), 0) AS revenue,
            COUNT(DISTINCT o.OrderID) AS orders,
            ROUND(SUM(od.UnitPrice * od.Quantity * (1 - od.Discount)) / COUNT(DISTINCT o.OrderID), 0) AS avgOrderValue,
            COUNT(DISTINCT o.CustomerID) AS customers
    """ + kpiBase)
    ctx.reportData('atomicValues', data)
}
 
// Component: revenueTrend (Chart — monthly revenue)
if (!componentId || componentId == 'revenueTrend') {
    def sql = """
        SELECT
            STRFTIME('%Y-%m', o.OrderDate / 1000, 'unixepoch') AS month,
            ROUND(SUM(od.UnitPrice * od.Quantity * (1 - od.Discount)), 0) AS revenue
        FROM Orders o
        JOIN "Order Details" od ON o.OrderID = od.OrderID
        WHERE o.OrderDate IS NOT NULL
    """
    if (filterByCountry) sql += " AND o.ShipCountry = '${country}'"
    sql += " GROUP BY STRFTIME('%Y-%m', o.OrderDate / 1000, 'unixepoch') ORDER BY month"
    def data = dbSql.rows(sql)
    ctx.reportData('revenueTrend', data)
}
 
// Component: revenueByCategory (Chart — revenue per product category)
if (!componentId || componentId == 'revenueByCategory') {
    def sql = """
        SELECT
            c.CategoryName AS category,
            ROUND(SUM(od.UnitPrice * od.Quantity * (1 - od.Discount)), 0) AS revenue
        FROM "Order Details" od
        JOIN Products p ON od.ProductID = p.ProductID
        JOIN Categories c ON p.CategoryID = c.CategoryID
        JOIN Orders o ON od.OrderID = o.OrderID
    """
    if (filterByCountry) sql += " WHERE o.ShipCountry = '${country}'"
    sql += " GROUP BY c.CategoryName ORDER BY revenue DESC"
    def data = dbSql.rows(sql)
    ctx.reportData('revenueByCategory', data)
}
 
// Component: topCustomers (Tabulator — top 10 by revenue)
if (!componentId || componentId == 'topCustomers') {
    def sql = """
        SELECT
            cu.CompanyName AS company,
            cu.Country AS country,
            cu.ContactName AS contact,
            COUNT(DISTINCT o.OrderID) AS orders,
            ROUND(SUM(od.UnitPrice * od.Quantity * (1 - od.Discount)), 2) AS revenue
        FROM Customers cu
        JOIN Orders o ON cu.CustomerID = o.CustomerID
        JOIN "Order Details" od ON o.OrderID = od.OrderID
    """
    if (filterByCountry) sql += " WHERE o.ShipCountry = '${country}'"
    sql += " GROUP BY cu.CustomerID, cu.CompanyName, cu.Country, cu.ContactName ORDER BY revenue DESC LIMIT 10"
    def data = dbSql.rows(sql)
    ctx.reportData('topCustomers', data)
}
 
// Component: orderExplorer (Pivot Table — orders by country, category, year)
if (!componentId || componentId == 'orderExplorer') {
    def sql = """
        SELECT
            o.ShipCountry AS country,
            c.CategoryName AS category,
            STRFTIME('%Y', o.OrderDate / 1000, 'unixepoch') AS year,
            ROUND(SUM(od.UnitPrice * od.Quantity * (1 - od.Discount)), 2) AS revenue,
            SUM(od.Quantity) AS quantity
        FROM Orders o
        JOIN "Order Details" od ON o.OrderID = od.OrderID
        JOIN Products p ON od.ProductID = p.ProductID
        JOIN Categories c ON p.CategoryID = c.CategoryID
        WHERE o.OrderDate IS NOT NULL
    """
    if (filterByCountry) sql += " AND o.ShipCountry = '${country}'"
    sql += " GROUP BY o.ShipCountry, c.CategoryName, STRFTIME('%Y', o.OrderDate / 1000, 'unixepoch') ORDER BY country, category, year"
    def data = dbSql.rows(sql)
    ctx.reportData('orderExplorer', data)
}

This script fetches data for 5 dashboard components:

Component IDTypeWhat it returns
atomicValuesrb-value (KPIs)Revenue, orders, avg order value, customers — as columns in one row
revenueTrendrb-chartMonthly revenue for a line chart
revenueByCategoryrb-chartRevenue per product category for a doughnut chart
topCustomersrb-tabulatorTop 10 customers by revenue
orderExplorerrb-pivot-tableRevenue by country, category, and year

Each block is guarded by if (!componentId || componentId == '...') — so when a single component refreshes (e.g., after a parameter change), only its query runs.

Step 4: Parameters

This adds a Country dropdown filter to the dashboard. The dropdown options are populated dynamically from the database. When the user selects a country, all dashboard components automatically refresh with filtered data.


ReportBurster - Report Parameters tab with country filter configuration
reportParameters {
    parameter(
        id: 'country',
        type: String,
        label: 'Country',
        defaultValue: '-- All --'
    ) {
        constraints(required: false)
        ui(
            control: 'select',
            options: "SELECT '-- All --' AS ShipCountry UNION ALL SELECT DISTINCT ShipCountry FROM Orders WHERE ShipCountry IS NOT NULL ORDER BY ShipCountry"
        )
    }
}

Step 5: The Dashboard Template


ReportBurster - Output Template tab — Dashboard output type, HTML template with live preview

Same idea here — the AI can generate this template, but we already have one ready:

<meta charset="utf-8">
<div class="rb-dashboard-root">
  <style>
    .rb-dashboard-root {
      all: initial;
      display: block;
      font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
      box-sizing: border-box;
      color: #1e293b;
      background: #f8fafc;
      padding: 24px;
    }
    .rb-dashboard-root *, .rb-dashboard-root *::before, .rb-dashboard-root *::after {
      box-sizing: inherit;
    }
 
    /* Color palette — warm teal for a wholesale/trade feel */
    .rb-dashboard-root {
      --accent: #0f766e;
      --accent-light: #ccfbf1;
      --accent-dark: #064e3b;
      --surface: #ffffff;
      --border: #e2e8f0;
      --text-primary: #0f172a;
      --text-secondary: #475569;
      --text-muted: #94a3b8;
      --shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
      --radius: 10px;
    }
 
    /* Header */
    .rb-dashboard-root .dash-header {
      display: flex;
      justify-content: space-between;
      align-items: flex-end;
      margin-bottom: 28px;
      padding-bottom: 16px;
      border-bottom: 2px solid var(--accent);
    }
    .rb-dashboard-root .dash-title {
      font-size: 22px;
      font-weight: 700;
      color: var(--text-primary);
      letter-spacing: -0.3px;
      margin: 0;
    }
    .rb-dashboard-root .dash-subtitle {
      font-size: 13px;
      color: var(--text-secondary);
      margin: 4px 0 0 0;
      font-weight: 400;
    }
 
    /* Parameters */
    .rb-dashboard-root .params-bar {
      margin-bottom: 24px;
    }
 
    /* KPI row */
    .rb-dashboard-root .kpi-row {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      gap: 16px;
      margin-bottom: 28px;
    }
    .rb-dashboard-root .kpi-card {
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 20px 22px;
      box-shadow: var(--shadow);
      position: relative;
      overflow: hidden;
    }
    .rb-dashboard-root .kpi-card::before {
      content: '';
      position: absolute;
      top: 0; left: 0; right: 0;
      height: 3px;
      background: var(--accent);
    }
    .rb-dashboard-root .kpi-label {
      font-size: 11px;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.6px;
      color: var(--text-muted);
      margin: 0 0 6px 0;
    }
    .rb-dashboard-root .kpi-value {
      font-size: 28px;
      font-weight: 800;
      color: var(--accent-dark);
      margin: 0;
      line-height: 1.1;
    }
 
    /* Charts row */
    .rb-dashboard-root .charts-row {
      display: grid;
      grid-template-columns: 2fr 1fr;
      gap: 20px;
      margin-bottom: 28px;
    }
    .rb-dashboard-root .card {
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 20px;
      box-shadow: var(--shadow);
    }
    .rb-dashboard-root .card-title {
      font-size: 14px;
      font-weight: 700;
      color: var(--text-primary);
      margin: 0 0 16px 0;
      letter-spacing: -0.2px;
    }
 
    /* Table section */
    .rb-dashboard-root .table-section {
      margin-bottom: 28px;
    }
 
    /* Pivot section */
    .rb-dashboard-root .pivot-section {
      margin-bottom: 12px;
    }
 
    /* Responsive */
    @media (max-width: 900px) {
      .rb-dashboard-root .kpi-row { grid-template-columns: repeat(2, 1fr); }
      .rb-dashboard-root .charts-row { grid-template-columns: 1fr; }
    }
    @media (max-width: 500px) {
      .rb-dashboard-root .kpi-row { grid-template-columns: 1fr; }
      .rb-dashboard-root { padding: 12px; }
    }
  </style>
 
  <!-- Header -->
  <div class="dash-header">
    <div>
      <h1 class="dash-title">Northwind Sales Dashboard</h1>
      <p class="dash-subtitle">Wholesale distribution - revenue, customers &amp; product performance</p>
    </div>
  </div>
 
  <!-- Parameters -->
  <div class="params-bar">
    <rb-parameters report-code="dashboard-test" api-base-url="http://localhost:9090/api/jobman/reporting" show-reload="true"></rb-parameters>
  </div>
 
  <!-- KPI Cards — all 4 share component-id="atomicValues" (1 fetch, cached), each picks a different field -->
  <div class="kpi-row">
    <div class="kpi-card">
      <p class="kpi-label">Revenue</p>
      <p class="kpi-value">
        <rb-value report-code="dashboard-test" api-base-url="http://localhost:9090/api/jobman/reporting" component-id="atomicValues" field="revenue" format="currency"></rb-value>
      </p>
    </div>
    <div class="kpi-card">
      <p class="kpi-label">Orders</p>
      <p class="kpi-value">
        <rb-value report-code="dashboard-test" api-base-url="http://localhost:9090/api/jobman/reporting" component-id="atomicValues" field="orders" format="number"></rb-value>
      </p>
    </div>
    <div class="kpi-card">
      <p class="kpi-label">Avg Order Value</p>
      <p class="kpi-value">
        <rb-value report-code="dashboard-test" api-base-url="http://localhost:9090/api/jobman/reporting" component-id="atomicValues" field="avgOrderValue" format="currency"></rb-value>
      </p>
    </div>
    <div class="kpi-card">
      <p class="kpi-label">Customers</p>
      <p class="kpi-value">
        <rb-value report-code="dashboard-test" api-base-url="http://localhost:9090/api/jobman/reporting" component-id="atomicValues" field="customers" format="number"></rb-value>
      </p>
    </div>
  </div>
 
  <!-- Charts -->
  <div class="charts-row">
    <div class="card">
      <h2 class="card-title">Revenue Trend</h2>
      <rb-chart report-code="dashboard-test" api-base-url="http://localhost:9090/api/jobman/reporting" component-id="revenueTrend"></rb-chart>
    </div>
    <div class="card">
      <h2 class="card-title">Revenue by Category</h2>
      <rb-chart report-code="dashboard-test" api-base-url="http://localhost:9090/api/jobman/reporting" component-id="revenueByCategory"></rb-chart>
    </div>
  </div>
 
  <!-- Top Customers Table -->
  <div class="table-section">
    <div class="card">
      <h2 class="card-title">Top 10 Customers</h2>
      <rb-tabulator report-code="dashboard-test" api-base-url="http://localhost:9090/api/jobman/reporting" component-id="topCustomers"></rb-tabulator>
    </div>
  </div>
 
  <!-- Order Explorer Pivot -->
  <div class="pivot-section">
    <div class="card">
      <h2 class="card-title">Order Explorer</h2>
      <rb-pivot-table report-code="dashboard-test" api-base-url="http://localhost:9090/api/jobman/reporting" component-id="orderExplorer"></rb-pivot-table>
    </div>
  </div>
</div>

Notice how all the rb-* web components — rb-value, rb-parameters, rb-chart, rb-tabulator, and rb-pivot-table — are used across the dashboard. The data for each of these components is fetched by the script you previously configured. Each component's component-id matches the names used in ctx.reportData() in the data script.

Step 6: Configure Tabulator, Charts & Pivot Table Components

In this step you'll configure the individual components: rb-tabulator controls which columns to show, formatting, sorting, filtering, and many other visual aspects; rb-chart configures how charts look visually — type, colors, labels, and layout; and rb-pivot-table defines which columns are available for rows, columns, and values, along with other pivot table settings.

Tabulator tab:


ReportBurster - Tabulator tab with DSL configuration
tabulator('topCustomers') {
  layout "fitColumns"
  columns {
    column { title "Company"; field "company"; headerFilter "input"; widthGrow 2 }
    column { title "Country"; field "country"; headerFilter "list" }
    column { title "Contact"; field "contact" }
    column { title "Orders"; field "orders"; hozAlign "right"; sorter "number" }
    column { title "Revenue"; field "revenue"; hozAlign "right"; sorter "number"; formatter "money"; formatterParams([thousand: ',', symbol: '$', precision: 2]) }
  }
}

'topCustomers' — the same ID used in ctx.reportData('topCustomers', data) in the script and component-id="topCustomers" in the HTML template.

Chart tab:


ReportBurster - Chart tab with DSL configuration
chart('revenueTrend') {
  type 'line'
 
  data {
    labelField 'month'
 
    datasets {
      dataset {
        field 'revenue'
        label 'Revenue'
        backgroundColor 'rgba(15, 118, 110, 0.1)'
        borderColor '#0f766e'
        borderWidth 2
        fill true
        tension 0.3
        pointRadius 3
        pointBackgroundColor '#0f766e'
      }
    }
  }
 
  options {
    plugins {
      legend { display false }
    }
    scales {
      y {
        beginAtZero true
        title { display true; text 'Revenue ($)' }
      }
      x {
        title { display true; text 'Month' }
      }
    }
  }
}
chart('revenueByCategory') {
  type 'doughnut'
 
  data {
    labelField 'category'
 
    datasets {
      dataset {
        field 'revenue'
        label 'Revenue'
        backgroundColor(['#0f766e', '#e15759', '#4e79a7', '#f28e2b', '#76b7b2', '#59a14f', '#edc949', '#af7aa1'])
        borderColor '#ffffff'
        borderWidth 2
      }
    }
  }
 
  options {
    plugins {
      legend { position 'right' }
    }
  }
}

'revenueTrend' and 'revenueByCategory' — the same IDs used in ctx.reportData('revenueTrend', data) / ctx.reportData('revenueByCategory', data) in the script and component-id="revenueTrend" / component-id="revenueByCategory" in the HTML template.

Pivot Table tab:


ReportBurster - Pivot Table tab with DSL configuration
pivotTable('orderExplorer') {
  rows 'country'
  cols 'year'
  vals 'revenue'
  aggregatorName 'Sum'
  rendererName 'Table Heatmap'
  rowOrder 'value_z_to_a'
}

'orderExplorer' — the same ID used in ctx.reportData('orderExplorer', data) in the script and component-id="orderExplorer" in the HTML template.

Step 7: View Your Dashboard


ReportBurster - Output Template tab — View Template in Browser button highlighted
ReportBurster - The finished Northwind Sales Dashboard in the browser — KPI cards, charts, data table, pivot table, and country filter

Step 8: Share Your Dashboard

All dashboards created with ReportBurster are embeddable — go to the Usage tab to find how.

ReportBurster dashboards can be embedded in any external web application using the <rb-dashboard> web component.

The exact syntax is shown in the screenshot below.


ReportBurster - Usage tab — rb-dashboard embed snippet, shareable dashboard URL, and ${dashboard_url} email variable

In addition, every dashboard gets a shareable URL automatically.

The dashboard URL is also available as the built-in variable ${dashboard_url} in email templates. Combined with ReportBurster's scheduling capabilities, you can set up automated emails that send dashboard links to stakeholders on a recurring schedule — daily, weekly, monthly, or any custom interval. Recipients click the link and see the live, always-up-to-date dashboard.

The Building Blocks

Every dashboard you build with ReportBurster is assembled from these components:

ComponentWhat It DoesDashboard Use
<rb-parameters>Date pickers, dropdowns, filtersDashboard-wide filters (date range, department, region)
<rb-tabulator>Interactive data grid with sorting, filtering, paginationDetail tables, transaction lists, drill-down views
<rb-chart>Bar, line, pie, area, scatter, and moreTrends, distributions, comparisons
<rb-pivot-table>Multi-dimensional drag-and-drop analysisRevenue breakdowns, cross-tabulations, ad-hoc exploration
<rb-report>Complete report viewer combining all of the aboveFull self-contained report pages
<rb-value>Displays a single scalar value — a number, text, or date that doesn't belong in a table or chart (see Atomic Values below)Summary cards, totals, counts, averages

Each component connects to your data through ReportBurster's backend — whether that data lives in a transactional database, DuckDB, or ClickHouse. Mix and match them on a single page to create exactly the dashboard you need.

Atomic Values (rb-value)

The <rb-value> component displays a single value — a revenue total, order count, average, calculated result, or any scalar. The value can come from a database query, a computation in your script, or any other source. It renders as a plain inline <span> with no table or chart overhead.

Attributes

AttributeRequiredDescription
report-codeYesReport folder name
api-base-urlYesBase URL for API
component-idYesMatches ctx.reportData('id', data) in the Groovy script
fieldYesColumn name from the query result to display
formatNocurrency ($58,153), number (1,234), percent (73%), date (Mar 15, 2024), or omit for raw value

Multiple Values with Shared Fetch

Multiple <rb-value> elements with the same component-id make one HTTP request — the result is cached client-side and each element picks its column via field:

<!-- 1 SQL query, 1 HTTP request, 4 value cards -->
<div class="value-card">
  <span class="label">Revenue</span>
  <rb-value report-code="dashboard" api-base-url="http://localhost:9090/api/jobman/reporting"
            component-id="atomicValues" field="revenue" format="currency"></rb-value>
</div>
<div class="value-card">
  <span class="label">Orders</span>
  <rb-value report-code="dashboard" api-base-url="http://localhost:9090/api/jobman/reporting"
            component-id="atomicValues" field="orders" format="number"></rb-value>
</div>
<div class="value-card">
  <span class="label">Avg Order Value</span>
  <rb-value report-code="dashboard" api-base-url="http://localhost:9090/api/jobman/reporting"
            component-id="atomicValues" field="avgOrderValue" format="currency"></rb-value>
</div>
<div class="value-card">
  <span class="label">Customers</span>
  <rb-value report-code="dashboard" api-base-url="http://localhost:9090/api/jobman/reporting"
            component-id="atomicValues" field="customers" format="number"></rb-value>
</div>

Groovy Script

Return all values as columns in a single row. No extra configuration is needed.

if (!componentId || componentId == 'atomicValues') {
    def data = dbSql.rows("""
        SELECT
            ROUND(SUM(od.UnitPrice * od.Quantity), 0) AS revenue,
            COUNT(DISTINCT o.OrderID) AS orders,
            ROUND(SUM(od.UnitPrice * od.Quantity) / COUNT(DISTINCT o.OrderID), 0) AS avgOrderValue,
            COUNT(DISTINCT o.CustomerID) AS customers
        FROM Orders o
        JOIN "Order Details" od ON o.OrderID = od.OrderID
    """)
    ctx.reportData('atomicValues', data)
}

The column aliases (revenue, orders, avgOrderValue, customers) match the field attributes on the HTML elements.

Supported Databases


ReportBurster - Supported database connections

Next: Make It Fast

Your dashboard is designed and assembled. Now make it performant. The Performance & Real-Time guide covers the implementation details — multi-component report configuration, efficient data loading with componentId, Redis caching for expensive queries, and real-time data patterns.