DuckVizBeta
Packages

@duckviz/dashboard

Embeddable dashboard component with D3 chart rendering and draggable grid layout.

@duckviz/dashboard provides the <Dashboard /> component — a draggable, resizable widget grid that renders 80+ D3 chart types via @duckviz/widgets. It auto-wires to the DuckDB context from @duckviz/db so each widget's dataKey runs as SQL. Every widget is wrapped in a ChartErrorBoundary so one bad query can't kill the grid.

Installation

npm install @duckviz/dashboard @duckviz/db @duckviz/ui @duckviz/widgets
npm install react react-dom d3

Quick start

import { DuckvizDBProvider } from "@duckviz/db";
import { Dashboard, type DashboardConfig } from "@duckviz/dashboard";

const config: DashboardConfig = {
  name: "Sales Dashboard",
  widgets: [
    {
      id: "w1",
      type: "bar",
      title: "Revenue by Category",
      description: "Top 10 categories",
      dataKey: "SELECT category, SUM(amount) AS value FROM t_sales GROUP BY category ORDER BY value DESC LIMIT 10",
    },
    {
      id: "w2",
      type: "line",
      title: "Monthly Trend",
      description: "Revenue over time",
      dataKey: "SELECT month AS category, total AS value FROM t_monthly",
    },
  ],
};

function App() {
  return (
    <DuckvizDBProvider persistence>
      <Dashboard config={config} />
    </DuckvizDBProvider>
  );
}

<Dashboard> props

Prop

Type

Auto-ingest with datasets

Skip the manual db.ingest() call by passing your row data directly:

import { Dashboard } from "@duckviz/dashboard";
import type { DuckvizDataset } from "@duckviz/db";

const datasets: DuckvizDataset[] = [
  { name: "Sales", data: salesRows },  // derives t_sales
];

<Dashboard config={config} datasets={datasets} />

Widgets render only after ingestion finishes. Tables are dropped on unmount by default.

Pre-fetch async data — don't pass a Promise or loader

<Dashboard> (and <ReportBuilder>, <DeckBuilder>) accept a resolved array of DuckvizDataset. The async loader mode is Explorer-only — Dashboard, Report, and Deck silently render empty if you pass a Promise, an async function, or a loader that returns datasets without an activeId. Await your fetch first, then pass the array.

DashboardConfig

interface DashboardConfig {
  name?: string;
  description?: string;
  dataset?: string;
  widgets: WidgetConfig[];
  grid?: {
    cols?: number;        // Default: 12
    rowHeight?: number;   // Default: 30
    margin?: [number, number]; // Default: [8, 8]
  };
  theme?: ThemeConfig;
}

WidgetConfig

Each widget maps a SQL query to a chart type:

interface WidgetConfig {
  id: string;            // Unique identifier
  type: string;          // Chart type from the registry (e.g. "bar", "scatter", "heatmap")
  title: string;         // Displayed above the chart
  description?: string;  // Subtitle text
  dataKey: string;       // DuckDB SQL query to execute
  config?: Record<string, unknown>; // Chart-specific options
  layout?: {
    x: number;  // Grid column position
    y: number;  // Grid row position
    w: number;  // Width in grid columns
    h: number;  // Height in grid rows
  };
}

The dataKey field contains a SQL query with standardized column aliases that the chart component expects. For example, a bar chart expects columns named category and value.

Auto-wired data provider

<Dashboard> automatically looks for a DuckvizDBProvider in the component tree using useDuckDBOrNull(). When found, it builds a data provider that runs each widget's dataKey as a SQL query against DuckDB.

This means you don't need to pass data manually — just wrap your dashboard in a DuckvizDBProvider and the queries execute automatically.

renderWidgetActions slot

Add custom actions to each widget's toolbar:

import { Dashboard, type WidgetActionContext } from "@duckviz/dashboard";

<Dashboard
  config={config}
  renderWidgetActions={(ctx: WidgetActionContext) => (
    <>
      <button onClick={() => ctx.refresh()}>Refresh</button>
      <button onClick={() => handleDelete(ctx.widgetId)}>Delete</button>
    </>
  )}
/>

The WidgetActionContext provides:

  • widgetId — the widget's ID
  • refresh() — re-run the SQL query and re-render the chart

Grid layout

The dashboard uses react-grid-layout v2 with a 12-column grid. Widgets can be:

  • Dragged to any position
  • Resized from the bottom-right corner
  • Auto-arranged — overlapping widgets push others down

If no layout is specified in WidgetConfig, widgets are auto-arranged.

ThemeConfig

interface ThemeConfig {
  mode?: "light" | "dark";
  variables?: Partial<ThemeVariables>;
}

See @duckviz/ui theming for the full ThemeVariables reference.

Converting RecommendedWidget to WidgetConfig

When you get widgets from the Explorer (or from @duckviz/sdk), convert them to WidgetConfig:

import type { RecommendedWidget } from "@duckviz/explorer";
import type { WidgetConfig } from "@duckviz/dashboard";

function toWidgetConfig(widget: RecommendedWidget, id: string): WidgetConfig {
  return {
    id,
    type: widget.type,
    title: widget.title,
    description: widget.description,
    dataKey: widget.duckdbQuery,
    config: widget.config,
  };
}