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. DataPallas's built-in AI tools generate everything for you. Just click the "Hey AI, Help Me with..." buttons and describe what you need.


Hey AI, Help Me button

How To Build Dashboards

DataPallas offers two alternative ways for building dashboards.

Approach 1: Build it on the Data Canvas is the more exploratory, visual path. Drop cubes or tables onto the canvas, try different chart shapes, drag and resize widgets — see your data take shape before you commit to anything. When you're happy, click Publish Dashboard.

Approach 2: Fully Configure Every Aspect of the Dashboard gives you full control. You provide the data-fetching script (DataPallas uses Groovy — a concise, readable scripting language), configure each widget precisely, and own the layout down to the last detail. That said — you are not expected to write any of those scripts yourself. Everywhere a script is needed, DataPallas puts a "Hey AI, Help Me with..." button right there. Don't be shy: AI is great at this, and more often than not it hands you the exact script you need on the first shot. Of course, if you prefer to write the scripts yourself, you can do that too.

The end result is exactly the same, regardless of which path you take. Both approaches produce the same report configuration files on disk — a data script, an HTML template, per-component configurations — so there is no "better" method. They are genuinely equivalent.

This gives you tremendous flexibility. A practical strategy when building something new: start on the Data Canvas, explore the data visually, and publish a solid working prototype in minutes. Then go and check the exported dashboard configurations in DataPallas and fine-tune the scripts, layout, and component configs until the dashboard is exactly where you want it. The best of both worlds.

Approach 1: Build Dashboards Using the Data Canvas

This approach lets you visually explore your data and configure most widgets entirely through the UI — no code needed. And when you need more power, the Finetune option is always there: switch to a custom SQL query or a script for any individual widget without leaving the canvas.

The dashboard below is a good real-world example of that mix. It has six widgets total:

  • 4 widgets configured entirely through the UI — drop a cube, tick a couple of fields, pick a visualization
  • 1 widget using Finetune SQL — to bucket revenue by month, which requires a custom time-grouping query the visual builder can't express
  • 1 widget using Finetune Script — to calculate the Pareto share (what % of revenue comes from the top 20% of customers), which requires distribution math across all rows

This covers the situations you'll realistically meet when building your own dashboards: many things you configure through the UI or, when you need increased flexibility, with a targeted SQL or script. The bundled database Northwind SQLite sample ships with DataPallas, so you can follow along on a fresh install.

(And yes — do we even need to remind you again? The "Hey AI, Help Me with..." button is right there for both the SQL and the script. 😄)

Step 1 — Open the Canvas

From DataPallas: Processing → Explore Data & Build Dashboards, start the Explore Data Canvas app, then click Launch to open the app in a browser.

The landing page lists all your dashboards — this is where newly created dashboards will appear. DataPallas ships with two sample dashboards so you can see what a finished result looks like before you build your own.


DataPallas - Apps Manager with the Explore Data Canvas app running and Launch button enabled

Click + New Canvas, name it Northwind Sales Overview, and in the left-panel connection dropdown pick the bundled rbt-sample-northwind-sqlite-... connection. The cube list and table tree populate — you're ready to drop widgets.

Step 2 — Add a Title

Good dashboards lead with context, not numbers. Drop a Text UI Element at the top of the canvas to give the dashboard a clear title and a one-line summary of what's on it.

  1. In the left panel, click the UI Elements tab.
  2. Click + Text. An empty Text widget appears.
  3. In the Display panel on the right, paste a markdown heading and a one-line subtitle. Markdown's ## is a section heading; plain text after it becomes a subtitle line.

Each step's screenshot below highlights the just-added widget with a blue ring so you can see exactly which piece of the canvas this step builds.


DataPallas - Northwind Sales Overview canvas with a Text Block widget at the top, showing the dashboard title and a one-line subtitle

Step 3 — KPI: Total Revenue

The simplest visual flow: drop a cube, tick one measure, pick Number. With zero dimensions ticked the cube emits SELECT SUM(Revenue) FROM cube — no GROUP BY — which is exactly what a top-line KPI wants.

  1. Click the Northwind Sales cube. A widget appears on the canvas, and the right panel opens its cube tree.
  2. In the cube tree, tick Revenue under Measures. The widget refreshes (you'll see a tiny single-row table).
  3. In the VISUALIZE AS strip, click Number. The widget reshapes into a big number.
  4. Switch to the Display tab and set LabelTotal Revenue. (Without an override, the raw field name "Revenue" leaks into the UI.)

You now have your first KPI card. The screenshot shows the rendered value AND the right-panel cube tree with Revenue ticked — the inputs that produced the output.


DataPallas - Total Revenue KPI rendered from the Northwind Sales cube — Number widget showing $58,153 highlighted with a blue ring, with the cube tree on the right showing Revenue ticked

Step 4 — KPI: Total Orders

Same flow as Step 3, different measure. Drop the Northwind Sales cube again, tick OrderCount (instead of Revenue), switch viz to Number, set the Label to Total Orders. The CamelCase measure name OrderCount would otherwise leak straight to the user — the label override fixes that on the canvas AND in the published dashboard (the override is persisted in displayConfig.numberLabel).


DataPallas - Total Orders KPI — second Number widget showing 79 highlighted with a blue ring, OrderCount ticked in the right-panel cube tree, alongside the first Total Revenue card

Step 5 — KPI: Top-20% Customer Share (Finetune Script)

"What % of revenue comes from the top 20% of customers?" needs distribution math: query all customers, sort by revenue, slice the top quintile, divide. The cube tree's checkboxes can't express this — it's the first widget where we reach for Finetune → Script.

  1. Drop the Northwind Customers cube.
  2. Switch viz to Number first (so the single-row script result lands in the right shape).
  3. In the right panel click Finetune → Script and paste:
def rows = ctx.dbSql.rows("""
    SELECT cu.CompanyName AS customer,
           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
    GROUP BY cu.CustomerID, cu.CompanyName
    ORDER BY revenue DESC
""")
 
def total = rows.collect { it.revenue }.sum() ?: 0
def topN = Math.max(1, (int)Math.ceil(rows.size() * 0.20))
def topRevenue = rows.take(topN).collect { it.revenue }.sum() ?: 0
def share = total > 0 ? (topRevenue / total) * 100.0 : 0
 
return [[
  "Top 20% Share": Math.round(share) / 100.0,
  "Top N Customers": topN,
  "Total Customers": rows.size()
]]
  1. Click Run. Set Label → % of Revenue from Top 20% Customers and Format → Percent (the script returns the fraction 0.36; the percent formatter renders it as 36%).

Don't write this from scratch. Click Hey AI, Help Me… above the editor and describe what you want ("Pareto share — what percent of revenue comes from the top 20% of customers"). DataPallas drafts the script against your live schema; you tweak and click Run.


DataPallas - Pareto KPI from a script — third Number widget highlighted with a blue ring, showing 36% with the script editor visible on the right panel

Step 6 — Trend: Monthly Revenue (Finetune SQL)

The Trend widget (<rb-trend>) shows "current vs prior period" — only meaningful when the data is bucketed. Cube visual mode groups by raw OrderDate (~daily granularity), giving the headline a noisy day-over-day delta. Switch to Finetune → SQL and bucket monthly:

  1. Drop the Northwind Sales cube.
  2. In the right panel click Finetune → SQL and paste:
SELECT
  STRFTIME('%Y-%m', o.OrderDate / 1000, 'unixepoch') AS month,
  ROUND(SUM(od.UnitPrice * od.Quantity * (1 - od.Discount)), 2) AS revenue
FROM Orders o
JOIN "Order Details" od ON o.OrderID = od.OrderID
GROUP BY 1
ORDER BY 1
  1. Click Run, then VISUALIZE AS → Trend. Set the trend label to Last month's revenue (vs. prior month) so the ▼ X% vs prior delta reads unambiguously.

The /1000, 'unixepoch' modifier is required because Hibernate persists OrderDate (a Java LocalDateTime) as epoch milliseconds; SQLite's STRFTIME expects ISO strings or epoch seconds. Without the divide-by-1000, every row collapses to a single NULL group and the headline shows the grand total with no sparkline.


DataPallas - Monthly revenue trend — rb-trend widget highlighted with a blue ring, sparkline showing last month's revenue and percent change vs prior month, with the SQL editor visible on the right panel

Step 7 — Chart: Revenue by Category

Back to pure visual mode. Drop the Northwind Sales cube again. In the cube tree tick CategoryName (under Dimensions) and Revenue (under Measures). Switch the viz to Chart. Eight bars, one per Northwind category.

Cube mode emits GROUP BY CategoryName ORDER BY CategoryName, so bars come out alphabetical (Beverages, Condiments, … Seafood). If you specifically need them sorted by revenue instead, that's a Finetune-SQL job — ORDER BY revenue DESC — but for most dashboards alphabetical is fine.


DataPallas - Revenue by Category bar chart — eight bars highlighted with a blue ring, alphabetical by category name, with CategoryName + Revenue ticked in the cube tree on the right

Step 8 — Tabulator: All Products

The product leaderboard. Drop the Northwind Sales cube. Tick ProductName + CategoryName dims and Revenue + TotalQuantity measures. Switch the viz to Tabulator. You get a click-sortable table with one row per product — click the Revenue header once to sort descending, and you have your leaderboard. (The bundled Northwind sample database has 20 products; on a real dataset you'd see one row per distinct ProductName/CategoryName combination.)

(Cube mode has no LIMIT N support, so this returns all rows. For a hard "Top 10" with LIMIT 10 you'd switch to Finetune SQL — but with sortable columns the user can click their way to a leaderboard view at runtime.)


DataPallas - Product leaderboard tabulator highlighted with a blue ring, showing ProductName, CategoryName, Revenue, TotalQuantity columns sortable by clicking the headers, with the cube tree on the right showing the four ticked fields

Step 9 — Publish Dashboard

The dashboard is fully built. Six widgets — three KPI cards, a monthly trend, a category chart, and a product table — laid out across the canvas in a clean three-row grid. The screenshot below is the final canvas WITHOUT any blue-ring highlight: this is what your dashboard looks like, ready to publish.


DataPallas - Northwind Sales Overview dashboard arranged on the canvas — 3 KPI cards on top, monthly revenue trend in the middle, category chart and product leaderboard at the bottom

Click Publish Dashboard in the top-right toolbar. DataPallas exports the canvas into a regular Report:

  • A data script with one if (!componentId || componentId == '...') block per widget — the SQL/script you wrote in each Finetune tab goes verbatim into the corresponding block.
  • An HTML template with <rb-value> / <rb-trend> / <rb-chart> / <rb-tabulator> web components wired to those component IDs.
  • Per-component configurations (Tabulator columns, Chart options, etc.).

The dashboard appears in DataPallas's dashboards list with a shareable URL, embeddable via <rb-dashboard> in any external page, and reachable via ${dashboard_url} in scheduled emails — exactly like a dashboard built directly as a Report (next section).

Approach 2: Fully Configure Every Aspect of the Dashboards

The other path: skip the canvas and author the dashboard directly in Report Generation — a data script, an HTML template, and per-component configurations (small declarative config scripts, one per component). Best when you want full HTML/CSS layout control from the start, or when the dashboard's logic is complex enough that you'd rather write/AI-generate it once than configure widget-by-widget. Below: the ~5-minute Northwind walkthrough.

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


DataPallas - 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.


DataPallas - 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 DataPallas 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 DataPallas 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 DataPallas — no Docker or external setup needed. Go to Configuration → Connections (Email, Databases) and configure your connection.


DataPallas - Database Connection dialog — SQLite, Northwind database

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


DataPallas - Database Schema tab — Northwind tables listed

Step 2: Create the Report


DataPallas - 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.


DataPallas - DataSource tab — Script input type, script 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.


DataPallas - 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


DataPallas - 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-id="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-id="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-id="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-id="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-id="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-id="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-id="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-id="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-id="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:


DataPallas - Tabulator tab with component 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:


DataPallas - Chart tab with component 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:


DataPallas - Pivot Table tab with component 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


DataPallas - Output Template tab — View Template in Browser button highlighted
DataPallas - 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 DataPallas are embeddable — go to the Usage tab to find how.

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

The exact syntax is shown in the screenshot below.


DataPallas - 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 DataPallas'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 DataPallas 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 DataPallas'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-idYesReport folder name
api-base-urlYesBase URL for API
component-idYesMatches ctx.reportData('id', data) in the data 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-id="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-id="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-id="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-id="dashboard" api-base-url="http://localhost:9090/api/jobman/reporting"
            component-id="atomicValues" field="customers" format="number"></rb-value>
</div>

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


DataPallas - 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.