@bonnard/cli
Advanced tools
| # ApexCharts + Bonnard SDK | ||
| > Build HTML dashboards with ApexCharts and the Bonnard SDK. No build step required. | ||
| ApexCharts has the best visual defaults out of the box — no configuration needed for tooltips, responsive behavior, or dark mode. SVG-based rendering produces sharp visuals at any resolution. Moderate payload (~130KB gzip). | ||
| ## Starter template | ||
| Copy this complete HTML file as a starting point. Replace `bon_pk_YOUR_KEY_HERE` with your publishable API key, and update the view/measure/dimension names to match your schema. | ||
| Use `explore()` to discover available views and fields — see [sdk.query-reference](sdk.query-reference). | ||
| ```html | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>Dashboard</title> | ||
| <script src="https://cdn.jsdelivr.net/npm/apexcharts@3/dist/apexcharts.min.js"></script> | ||
| <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script> | ||
| <style> | ||
| * { margin: 0; padding: 0; box-sizing: border-box; } | ||
| body { | ||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | ||
| background: #09090b; color: #fafafa; padding: 24px; | ||
| } | ||
| h1 { font-size: 24px; font-weight: 600; margin-bottom: 24px; } | ||
| .error { color: #ef4444; background: #1c0a0a; padding: 12px; border-radius: 8px; margin-bottom: 16px; display: none; } | ||
| .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 32px; } | ||
| .kpi { background: #18181b; border: 1px solid #27272a; border-radius: 12px; padding: 20px; } | ||
| .kpi-label { font-size: 14px; color: #a1a1aa; margin-bottom: 8px; } | ||
| .kpi-value { font-size: 32px; font-weight: 600; } | ||
| .kpi-value.loading { color: #3f3f46; } | ||
| .charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 24px; } | ||
| .chart-card { background: #18181b; border: 1px solid #27272a; border-radius: 12px; padding: 20px; } | ||
| .chart-title { font-size: 16px; font-weight: 500; margin-bottom: 16px; } | ||
| .chart-container { height: 300px; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <h1>Dashboard</h1> | ||
| <div id="error" class="error"></div> | ||
| <div class="kpis"> | ||
| <div class="kpi"> | ||
| <div class="kpi-label">Revenue</div> | ||
| <div class="kpi-value loading" id="kpi-revenue">--</div> | ||
| </div> | ||
| <div class="kpi"> | ||
| <div class="kpi-label">Orders</div> | ||
| <div class="kpi-value loading" id="kpi-orders">--</div> | ||
| </div> | ||
| <div class="kpi"> | ||
| <div class="kpi-label">Avg Value</div> | ||
| <div class="kpi-value loading" id="kpi-avg">--</div> | ||
| </div> | ||
| </div> | ||
| <div class="charts"> | ||
| <div class="chart-card"> | ||
| <div class="chart-title">Revenue by City</div> | ||
| <div class="chart-container" id="bar-chart"></div> | ||
| </div> | ||
| <div class="chart-card"> | ||
| <div class="chart-title">Revenue Trend</div> | ||
| <div class="chart-container" id="line-chart"></div> | ||
| </div> | ||
| </div> | ||
| <script> | ||
| const bon = Bonnard.createClient({ | ||
| apiKey: 'bon_pk_YOUR_KEY_HERE', | ||
| }); | ||
| // --- Helpers --- | ||
| function showError(msg) { | ||
| const el = document.getElementById('error'); | ||
| el.textContent = msg; | ||
| el.style.display = 'block'; | ||
| } | ||
| function formatNumber(v) { | ||
| return new Intl.NumberFormat().format(v); | ||
| } | ||
| function formatCurrency(v) { | ||
| return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(v); | ||
| } | ||
| // ApexCharts dark mode defaults | ||
| const darkTheme = { | ||
| chart: { background: 'transparent', foreColor: '#a1a1aa' }, | ||
| theme: { mode: 'dark' }, | ||
| grid: { borderColor: '#27272a' }, | ||
| tooltip: { theme: 'dark' }, | ||
| }; | ||
| // --- Load data --- | ||
| (async () => { | ||
| try { | ||
| // KPIs | ||
| const kpis = await bon.query({ | ||
| measures: ['orders.revenue', 'orders.count', 'orders.avg_value'], | ||
| }); | ||
| if (kpis.data.length > 0) { | ||
| const row = kpis.data[0]; | ||
| document.getElementById('kpi-revenue').textContent = formatCurrency(row['orders.revenue']); | ||
| document.getElementById('kpi-revenue').classList.remove('loading'); | ||
| document.getElementById('kpi-orders').textContent = formatNumber(row['orders.count']); | ||
| document.getElementById('kpi-orders').classList.remove('loading'); | ||
| document.getElementById('kpi-avg').textContent = formatCurrency(row['orders.avg_value']); | ||
| document.getElementById('kpi-avg').classList.remove('loading'); | ||
| } | ||
| // Bar chart — revenue by city | ||
| const byCity = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| dimensions: ['orders.city'], | ||
| orderBy: { 'orders.revenue': 'desc' }, | ||
| limit: 10, | ||
| }); | ||
| new ApexCharts(document.getElementById('bar-chart'), { | ||
| ...darkTheme, | ||
| chart: { ...darkTheme.chart, type: 'bar', height: 300 }, | ||
| series: [{ name: 'Revenue', data: byCity.data.map(d => d['orders.revenue']) }], | ||
| xaxis: { categories: byCity.data.map(d => d['orders.city']) }, | ||
| yaxis: { labels: { formatter: v => formatCurrency(v) } }, | ||
| plotOptions: { bar: { borderRadius: 4, columnWidth: '60%' } }, | ||
| colors: ['#3b82f6'], | ||
| dataLabels: { enabled: false }, | ||
| }).render(); | ||
| // Line chart — revenue trend | ||
| const trend = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| timeDimension: { | ||
| dimension: 'orders.created_at', | ||
| granularity: 'month', | ||
| dateRange: 'last 12 months', | ||
| }, | ||
| }); | ||
| new ApexCharts(document.getElementById('line-chart'), { | ||
| ...darkTheme, | ||
| chart: { ...darkTheme.chart, type: 'area', height: 300 }, | ||
| series: [{ name: 'Revenue', data: trend.data.map(d => d['orders.revenue']) }], | ||
| xaxis: { | ||
| categories: trend.data.map(d => { | ||
| const date = new Date(d['orders.created_at']); | ||
| return date.toLocaleDateString('en-US', { month: 'short', year: '2-digit' }); | ||
| }), | ||
| }, | ||
| yaxis: { labels: { formatter: v => formatCurrency(v) } }, | ||
| colors: ['#3b82f6'], | ||
| fill: { type: 'gradient', gradient: { opacityFrom: 0.3, opacityTo: 0.05 } }, | ||
| stroke: { curve: 'smooth', width: 2 }, | ||
| dataLabels: { enabled: false }, | ||
| }).render(); | ||
| } catch (err) { | ||
| showError('Failed to load dashboard: ' + err.message); | ||
| } | ||
| })(); | ||
| </script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| ## Chart types | ||
| ### Bar chart | ||
| ```javascript | ||
| new ApexCharts(el, { | ||
| chart: { type: 'bar', height: 300 }, | ||
| series: [{ name: 'Revenue', data: data.map(d => d['view.measure']) }], | ||
| xaxis: { categories: data.map(d => d['view.dimension']) }, | ||
| plotOptions: { bar: { borderRadius: 4 } }, | ||
| colors: ['#3b82f6'], | ||
| }).render(); | ||
| ``` | ||
| ### Horizontal bar chart | ||
| ```javascript | ||
| new ApexCharts(el, { | ||
| chart: { type: 'bar', height: 300 }, | ||
| series: [{ name: 'Revenue', data: data.map(d => d['view.measure']) }], | ||
| xaxis: { categories: data.map(d => d['view.dimension']) }, | ||
| plotOptions: { bar: { horizontal: true, borderRadius: 4 } }, | ||
| }).render(); | ||
| ``` | ||
| ### Line chart | ||
| ```javascript | ||
| new ApexCharts(el, { | ||
| chart: { type: 'line', height: 300 }, | ||
| series: [{ name: 'Revenue', data: values }], | ||
| xaxis: { categories: labels }, | ||
| stroke: { curve: 'smooth', width: 2 }, | ||
| }).render(); | ||
| ``` | ||
| ### Area chart | ||
| ```javascript | ||
| new ApexCharts(el, { | ||
| chart: { type: 'area', height: 300 }, | ||
| series: [{ name: 'Revenue', data: values }], | ||
| xaxis: { categories: labels }, | ||
| fill: { type: 'gradient', gradient: { opacityFrom: 0.3, opacityTo: 0.05 } }, | ||
| stroke: { curve: 'smooth', width: 2 }, | ||
| }).render(); | ||
| ``` | ||
| ### Pie / donut chart | ||
| ```javascript | ||
| new ApexCharts(el, { | ||
| chart: { type: 'donut', height: 300 }, // or 'pie' | ||
| series: data.map(d => d['view.measure']), | ||
| labels: data.map(d => d['view.dimension']), | ||
| colors: ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6'], | ||
| }).render(); | ||
| ``` | ||
| ### Multi-series line chart | ||
| ```javascript | ||
| const cities = [...new Set(data.map(d => d['orders.city']))]; | ||
| const dates = [...new Set(data.map(d => d['orders.created_at']))]; | ||
| new ApexCharts(el, { | ||
| chart: { type: 'line', height: 300 }, | ||
| series: cities.map(city => ({ | ||
| name: city, | ||
| data: dates.map(date => | ||
| data.find(d => d['orders.city'] === city && d['orders.created_at'] === date)?.['orders.revenue'] || 0 | ||
| ), | ||
| })), | ||
| xaxis: { categories: dates.map(d => new Date(d).toLocaleDateString()) }, | ||
| stroke: { curve: 'smooth', width: 2 }, | ||
| }).render(); | ||
| ``` | ||
| ## Dark mode | ||
| ApexCharts has built-in dark mode support: | ||
| ```javascript | ||
| const darkTheme = { | ||
| chart: { background: 'transparent', foreColor: '#a1a1aa' }, | ||
| theme: { mode: 'dark' }, | ||
| grid: { borderColor: '#27272a' }, | ||
| tooltip: { theme: 'dark' }, | ||
| }; | ||
| new ApexCharts(el, { | ||
| ...darkTheme, | ||
| chart: { ...darkTheme.chart, type: 'bar', height: 300 }, | ||
| // ... rest of config | ||
| }).render(); | ||
| ``` | ||
| ## Color palette | ||
| ```javascript | ||
| const COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316']; | ||
| // Apply globally: | ||
| colors: COLORS, | ||
| ``` | ||
| ## See also | ||
| - [sdk.browser](sdk.browser) — Browser / CDN quickstart | ||
| - [sdk.query-reference](sdk.query-reference) — Full query API | ||
| - [sdk.chartjs](sdk.chartjs) — Chart.js alternative (smallest payload) | ||
| - [sdk.echarts](sdk.echarts) — ECharts alternative (more chart types) |
| # Authentication | ||
| > How to authenticate SDK requests — publishable keys for public dashboards, token exchange for multi-tenant apps. | ||
| ## Publishable keys | ||
| Publishable keys (`bon_pk_...`) are safe to use in client-side code — HTML pages, browser apps, mobile apps. They grant read-only access to your org's semantic layer. | ||
| ```javascript | ||
| const bon = Bonnard.createClient({ | ||
| apiKey: 'bon_pk_...', | ||
| }); | ||
| ``` | ||
| Create publishable keys in the Bonnard web app under **Settings > API Keys**. | ||
| **What publishable keys can do:** | ||
| - Query measures and dimensions | ||
| - Explore schema (views, fields) | ||
| **What they cannot do:** | ||
| - Modify data or schema | ||
| - Access other orgs' data | ||
| - Bypass governance policies (if configured at org level) | ||
| ## Token exchange (multi-tenant) | ||
| For B2B apps where each customer should only see their own data, use **secret key token exchange**. Your server exchanges a secret key for a short-lived JWT with a security context, then your frontend queries with that token. | ||
| ### Server-side: exchange secret key for scoped token | ||
| ```javascript | ||
| // Your backend (Node.js, Python, etc.) | ||
| const res = await fetch('https://app.bonnard.dev/api/sdk/token', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Authorization': `Bearer ${process.env.BONNARD_SECRET_KEY}`, // bon_sk_... | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify({ | ||
| security_context: { | ||
| tenant_id: currentCustomer.id, // your tenant identifier | ||
| }, | ||
| }), | ||
| }); | ||
| const { token } = await res.json(); | ||
| // Pass this token to your frontend | ||
| ``` | ||
| ### Client-side: query with scoped token | ||
| ```javascript | ||
| const bon = Bonnard.createClient({ | ||
| fetchToken: async () => { | ||
| const res = await fetch('/my-backend/bonnard-token'); | ||
| const { token } = await res.json(); | ||
| return token; | ||
| }, | ||
| }); | ||
| const { data } = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| dimensions: ['orders.status'], | ||
| }); | ||
| // Only returns rows matching the tenant's security context | ||
| ``` | ||
| ### How token refresh works | ||
| The SDK automatically: | ||
| 1. Calls `fetchToken()` on the first query | ||
| 2. Caches the returned JWT | ||
| 3. Parses the JWT `exp` claim | ||
| 4. Refreshes 60 seconds before expiry by calling `fetchToken()` again | ||
| You don't need to manage token lifecycle — just provide the `fetchToken` callback. | ||
| ### Security context and governance | ||
| The `security_context` object you pass during token exchange becomes available in your Cube models as `{securityContext.attrs.*}`. Use it in access policies to enforce row-level security: | ||
| ```yaml | ||
| # In your Cube view definition | ||
| access_policy: | ||
| - role: "*" | ||
| conditions: | ||
| - sql: "{TABLE}.tenant_id = '{securityContext.attrs.tenant_id}'" | ||
| ``` | ||
| See [security-context](security-context) for the full governance setup guide. | ||
| ## Browser HTML with token exchange | ||
| For HTML dashboards that need multi-tenant auth, your page fetches a token from your backend: | ||
| ```html | ||
| <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script> | ||
| <script> | ||
| const bon = Bonnard.createClient({ | ||
| fetchToken: async () => { | ||
| const res = await fetch('/api/bonnard-token'); | ||
| const { token } = await res.json(); | ||
| return token; | ||
| }, | ||
| }); | ||
| (async () => { | ||
| const { data } = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| }); | ||
| // Data is scoped to the authenticated tenant | ||
| })(); | ||
| </script> | ||
| ``` | ||
| ## When to use which | ||
| | Scenario | Auth method | Key type | | ||
| |----------|------------|----------| | ||
| | Internal dashboard (your team) | Publishable key | `bon_pk_...` | | ||
| | Public dashboard (anyone can view) | Publishable key | `bon_pk_...` | | ||
| | Embedded analytics (customer sees their data only) | Token exchange | `bon_sk_...` → JWT | | ||
| | Server-side data pipeline | Secret key directly | `bon_sk_...` | | ||
| ## See also | ||
| - [sdk.browser](sdk.browser) — Browser / CDN quickstart | ||
| - [sdk.query-reference](sdk.query-reference) — Full query API | ||
| - [security-context](security-context) — Row-level security setup |
| # Browser / CDN Quickstart | ||
| > Load the Bonnard SDK via a `<script>` tag and query your semantic layer from any HTML page. | ||
| ## Setup | ||
| Add the SDK script tag to your HTML. It exposes `window.Bonnard`. | ||
| ```html | ||
| <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script> | ||
| ``` | ||
| Alternative CDN: | ||
| ```html | ||
| <script src="https://unpkg.com/@bonnard/sdk/dist/bonnard.iife.js"></script> | ||
| ``` | ||
| Pin a specific version: | ||
| ```html | ||
| <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk@0.4.2/dist/bonnard.iife.js"></script> | ||
| ``` | ||
| ## First query | ||
| ```html | ||
| <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script> | ||
| <script> | ||
| const bon = Bonnard.createClient({ | ||
| apiKey: 'bon_pk_YOUR_KEY_HERE', | ||
| }); | ||
| (async () => { | ||
| const { data } = await bon.query({ | ||
| measures: ['orders.revenue', 'orders.count'], | ||
| dimensions: ['orders.city'], | ||
| orderBy: { 'orders.revenue': 'desc' }, | ||
| limit: 10, | ||
| }); | ||
| console.log(data); | ||
| // [{ "orders.revenue": 125000, "orders.count": 340, "orders.city": "Berlin" }, ...] | ||
| })(); | ||
| </script> | ||
| ``` | ||
| Note: the IIFE bundle uses a regular `<script>` tag (not `type="module"`), so top-level `await` is not available. Wrap async code in an IIFE or use `.then()`. | ||
| ## Async patterns | ||
| ### IIFE wrapper (recommended) | ||
| ```html | ||
| <script> | ||
| (async () => { | ||
| const bon = Bonnard.createClient({ apiKey: 'bon_pk_...' }); | ||
| const { data } = await bon.query({ measures: ['orders.revenue'] }); | ||
| document.getElementById('revenue').textContent = data[0]['orders.revenue']; | ||
| })(); | ||
| </script> | ||
| ``` | ||
| ### Promise chain | ||
| ```html | ||
| <script> | ||
| const bon = Bonnard.createClient({ apiKey: 'bon_pk_...' }); | ||
| bon.query({ measures: ['orders.revenue'] }) | ||
| .then(({ data }) => { | ||
| document.getElementById('revenue').textContent = data[0]['orders.revenue']; | ||
| }) | ||
| .catch(err => { | ||
| console.error('Query failed:', err.message); | ||
| }); | ||
| </script> | ||
| ``` | ||
| ### Parallel queries | ||
| ```html | ||
| <script> | ||
| (async () => { | ||
| const bon = Bonnard.createClient({ apiKey: 'bon_pk_...' }); | ||
| const [kpis, byCity] = await Promise.all([ | ||
| bon.query({ measures: ['orders.revenue', 'orders.count'] }), | ||
| bon.query({ | ||
| measures: ['orders.revenue'], | ||
| dimensions: ['orders.city'], | ||
| orderBy: { 'orders.revenue': 'desc' }, | ||
| }), | ||
| ]); | ||
| // Render KPIs | ||
| document.getElementById('revenue').textContent = kpis.data[0]['orders.revenue']; | ||
| document.getElementById('count').textContent = kpis.data[0]['orders.count']; | ||
| // Render chart with byCity.data... | ||
| })(); | ||
| </script> | ||
| ``` | ||
| ## Error handling | ||
| ```html | ||
| <script> | ||
| (async () => { | ||
| const bon = Bonnard.createClient({ apiKey: 'bon_pk_...' }); | ||
| try { | ||
| const { data } = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| }); | ||
| renderDashboard(data); | ||
| } catch (err) { | ||
| document.getElementById('error').textContent = err.message; | ||
| document.getElementById('error').style.display = 'block'; | ||
| } | ||
| })(); | ||
| </script> | ||
| ``` | ||
| Common errors: | ||
| - `"Unauthorized"` — invalid or expired API key | ||
| - `"Query failed"` — invalid measure/dimension names or query structure | ||
| - Network errors — API unreachable (CORS, connectivity) | ||
| ## Custom base URL | ||
| By default the SDK points to `https://app.bonnard.dev`. Override for self-hosted or preview deployments: | ||
| ```html | ||
| <script> | ||
| const bon = Bonnard.createClient({ | ||
| apiKey: 'bon_pk_...', | ||
| baseUrl: 'https://your-deployment.vercel.app', | ||
| }); | ||
| </script> | ||
| ``` | ||
| ## What's on `window.Bonnard` | ||
| The IIFE bundle exposes two exports: | ||
| | Export | Purpose | | ||
| |--------|---------| | ||
| | `Bonnard.createClient(config)` | Create an SDK client instance | | ||
| | `Bonnard.toCubeQuery(options)` | Convert `QueryOptions` to a Cube-native query object (useful for debugging) | | ||
| ## Field naming | ||
| All field names must be fully qualified with the view name: | ||
| ```javascript | ||
| // Correct | ||
| bon.query({ measures: ['orders.revenue'], dimensions: ['orders.city'] }); | ||
| // Wrong — will fail | ||
| bon.query({ measures: ['revenue'], dimensions: ['city'] }); | ||
| ``` | ||
| ## Discovering available fields | ||
| Use `explore()` to discover what views, measures, and dimensions are available: | ||
| ```javascript | ||
| const meta = await bon.explore(); | ||
| for (const view of meta.cubes) { | ||
| console.log(view.name); | ||
| console.log(' Measures:', view.measures.map(m => m.name)); | ||
| console.log(' Dimensions:', view.dimensions.map(d => d.name)); | ||
| } | ||
| ``` | ||
| ## Next steps | ||
| - [sdk.chartjs](sdk.chartjs) — Build a dashboard with Chart.js | ||
| - [sdk.echarts](sdk.echarts) — Build a dashboard with ECharts | ||
| - [sdk.apexcharts](sdk.apexcharts) — Build a dashboard with ApexCharts | ||
| - [sdk.query-reference](sdk.query-reference) — Full query API reference | ||
| - [sdk.authentication](sdk.authentication) — Auth patterns for multi-tenant apps |
| # Chart.js + Bonnard SDK | ||
| > Build HTML dashboards with Chart.js and the Bonnard SDK. No build step required. | ||
| Chart.js is the recommended chart library for HTML dashboards — smallest payload (~65KB gzip), most LLM training data, and excellent documentation. | ||
| ## Starter template | ||
| Copy this complete HTML file as a starting point. Replace `bon_pk_YOUR_KEY_HERE` with your publishable API key, and update the view/measure/dimension names to match your schema. | ||
| Use `explore()` to discover available views and fields — see [sdk.query-reference](sdk.query-reference). | ||
| ```html | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>Dashboard</title> | ||
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script> | ||
| <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script> | ||
| <style> | ||
| * { margin: 0; padding: 0; box-sizing: border-box; } | ||
| body { | ||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | ||
| background: #09090b; color: #fafafa; padding: 24px; | ||
| } | ||
| h1 { font-size: 24px; font-weight: 600; margin-bottom: 24px; } | ||
| .error { color: #ef4444; background: #1c0a0a; padding: 12px; border-radius: 8px; margin-bottom: 16px; display: none; } | ||
| .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 32px; } | ||
| .kpi { background: #18181b; border: 1px solid #27272a; border-radius: 12px; padding: 20px; } | ||
| .kpi-label { font-size: 14px; color: #a1a1aa; margin-bottom: 8px; } | ||
| .kpi-value { font-size: 32px; font-weight: 600; } | ||
| .kpi-value.loading { color: #3f3f46; } | ||
| .charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 24px; } | ||
| .chart-card { background: #18181b; border: 1px solid #27272a; border-radius: 12px; padding: 20px; } | ||
| .chart-title { font-size: 16px; font-weight: 500; margin-bottom: 16px; } | ||
| .chart-container { position: relative; height: 300px; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <h1>Dashboard</h1> | ||
| <div id="error" class="error"></div> | ||
| <div class="kpis"> | ||
| <div class="kpi"> | ||
| <div class="kpi-label">Revenue</div> | ||
| <div class="kpi-value loading" id="kpi-revenue">--</div> | ||
| </div> | ||
| <div class="kpi"> | ||
| <div class="kpi-label">Orders</div> | ||
| <div class="kpi-value loading" id="kpi-orders">--</div> | ||
| </div> | ||
| <div class="kpi"> | ||
| <div class="kpi-label">Avg Value</div> | ||
| <div class="kpi-value loading" id="kpi-avg">--</div> | ||
| </div> | ||
| </div> | ||
| <div class="charts"> | ||
| <div class="chart-card"> | ||
| <div class="chart-title">Revenue by City</div> | ||
| <div class="chart-container"><canvas id="bar-chart"></canvas></div> | ||
| </div> | ||
| <div class="chart-card"> | ||
| <div class="chart-title">Revenue Trend</div> | ||
| <div class="chart-container"><canvas id="line-chart"></canvas></div> | ||
| </div> | ||
| </div> | ||
| <script> | ||
| const bon = Bonnard.createClient({ | ||
| apiKey: 'bon_pk_YOUR_KEY_HERE', | ||
| }); | ||
| // --- Helpers --- | ||
| function showError(msg) { | ||
| const el = document.getElementById('error'); | ||
| el.textContent = msg; | ||
| el.style.display = 'block'; | ||
| } | ||
| function formatNumber(v) { | ||
| return new Intl.NumberFormat().format(v); | ||
| } | ||
| function formatCurrency(v) { | ||
| return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(v); | ||
| } | ||
| // Chart.js defaults for dark mode | ||
| Chart.defaults.color = '#a1a1aa'; | ||
| Chart.defaults.borderColor = '#27272a'; | ||
| // --- Load data --- | ||
| (async () => { | ||
| try { | ||
| // KPIs | ||
| const kpis = await bon.query({ | ||
| measures: ['orders.revenue', 'orders.count', 'orders.avg_value'], | ||
| }); | ||
| if (kpis.data.length > 0) { | ||
| const row = kpis.data[0]; | ||
| document.getElementById('kpi-revenue').textContent = formatCurrency(row['orders.revenue']); | ||
| document.getElementById('kpi-revenue').classList.remove('loading'); | ||
| document.getElementById('kpi-orders').textContent = formatNumber(row['orders.count']); | ||
| document.getElementById('kpi-orders').classList.remove('loading'); | ||
| document.getElementById('kpi-avg').textContent = formatCurrency(row['orders.avg_value']); | ||
| document.getElementById('kpi-avg').classList.remove('loading'); | ||
| } | ||
| // Bar chart — revenue by city | ||
| const byCity = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| dimensions: ['orders.city'], | ||
| orderBy: { 'orders.revenue': 'desc' }, | ||
| limit: 10, | ||
| }); | ||
| new Chart(document.getElementById('bar-chart'), { | ||
| type: 'bar', | ||
| data: { | ||
| labels: byCity.data.map(d => d['orders.city']), | ||
| datasets: [{ | ||
| label: 'Revenue', | ||
| data: byCity.data.map(d => d['orders.revenue']), | ||
| backgroundColor: '#3b82f6', | ||
| borderRadius: 4, | ||
| }], | ||
| }, | ||
| options: { | ||
| responsive: true, | ||
| maintainAspectRatio: false, | ||
| plugins: { legend: { display: false } }, | ||
| scales: { | ||
| y: { | ||
| ticks: { callback: v => formatCurrency(v) }, | ||
| grid: { color: '#27272a' }, | ||
| }, | ||
| x: { grid: { display: false } }, | ||
| }, | ||
| }, | ||
| }); | ||
| // Line chart — revenue trend | ||
| const trend = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| timeDimension: { | ||
| dimension: 'orders.created_at', | ||
| granularity: 'month', | ||
| dateRange: 'last 12 months', | ||
| }, | ||
| }); | ||
| new Chart(document.getElementById('line-chart'), { | ||
| type: 'line', | ||
| data: { | ||
| labels: trend.data.map(d => { | ||
| const date = new Date(d['orders.created_at']); | ||
| return date.toLocaleDateString('en-US', { month: 'short', year: '2-digit' }); | ||
| }), | ||
| datasets: [{ | ||
| label: 'Revenue', | ||
| data: trend.data.map(d => d['orders.revenue']), | ||
| borderColor: '#3b82f6', | ||
| backgroundColor: 'rgba(59, 130, 246, 0.1)', | ||
| fill: true, | ||
| tension: 0.3, | ||
| pointRadius: 3, | ||
| }], | ||
| }, | ||
| options: { | ||
| responsive: true, | ||
| maintainAspectRatio: false, | ||
| plugins: { legend: { display: false } }, | ||
| scales: { | ||
| y: { | ||
| ticks: { callback: v => formatCurrency(v) }, | ||
| grid: { color: '#27272a' }, | ||
| }, | ||
| x: { grid: { display: false } }, | ||
| }, | ||
| }, | ||
| }); | ||
| } catch (err) { | ||
| showError('Failed to load dashboard: ' + err.message); | ||
| } | ||
| })(); | ||
| </script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| ## Chart types | ||
| ### Bar chart | ||
| ```javascript | ||
| new Chart(ctx, { | ||
| type: 'bar', | ||
| data: { | ||
| labels: data.map(d => d['view.dimension']), | ||
| datasets: [{ | ||
| label: 'Revenue', | ||
| data: data.map(d => d['view.measure']), | ||
| backgroundColor: '#3b82f6', | ||
| borderRadius: 4, | ||
| }], | ||
| }, | ||
| options: { | ||
| responsive: true, | ||
| maintainAspectRatio: false, | ||
| plugins: { legend: { display: false } }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### Horizontal bar chart | ||
| ```javascript | ||
| new Chart(ctx, { | ||
| type: 'bar', | ||
| data: { /* same as above */ }, | ||
| options: { | ||
| indexAxis: 'y', | ||
| responsive: true, | ||
| maintainAspectRatio: false, | ||
| }, | ||
| }); | ||
| ``` | ||
| ### Line chart | ||
| ```javascript | ||
| new Chart(ctx, { | ||
| type: 'line', | ||
| data: { | ||
| labels: data.map(d => new Date(d['view.date']).toLocaleDateString()), | ||
| datasets: [{ | ||
| label: 'Revenue', | ||
| data: data.map(d => d['view.measure']), | ||
| borderColor: '#3b82f6', | ||
| tension: 0.3, | ||
| }], | ||
| }, | ||
| }); | ||
| ``` | ||
| ### Area chart (filled line) | ||
| ```javascript | ||
| datasets: [{ | ||
| label: 'Revenue', | ||
| data: data.map(d => d['view.measure']), | ||
| borderColor: '#3b82f6', | ||
| backgroundColor: 'rgba(59, 130, 246, 0.1)', | ||
| fill: true, | ||
| }] | ||
| ``` | ||
| ### Pie / doughnut chart | ||
| ```javascript | ||
| new Chart(ctx, { | ||
| type: 'doughnut', // or 'pie' | ||
| data: { | ||
| labels: data.map(d => d['view.dimension']), | ||
| datasets: [{ | ||
| data: data.map(d => d['view.measure']), | ||
| backgroundColor: ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6'], | ||
| }], | ||
| }, | ||
| }); | ||
| ``` | ||
| ### Multi-series line chart | ||
| ```javascript | ||
| // Query with a grouping dimension | ||
| const { data } = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| dimensions: ['orders.city'], | ||
| timeDimension: { dimension: 'orders.created_at', granularity: 'month' }, | ||
| }); | ||
| // Group data by city | ||
| const cities = [...new Set(data.map(d => d['orders.city']))]; | ||
| const dates = [...new Set(data.map(d => d['orders.created_at']))]; | ||
| new Chart(ctx, { | ||
| type: 'line', | ||
| data: { | ||
| labels: dates.map(d => new Date(d).toLocaleDateString()), | ||
| datasets: cities.map((city, i) => ({ | ||
| label: city, | ||
| data: dates.map(date => | ||
| data.find(d => d['orders.city'] === city && d['orders.created_at'] === date)?.['orders.revenue'] || 0 | ||
| ), | ||
| borderColor: ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6'][i % 5], | ||
| })), | ||
| }, | ||
| }); | ||
| ``` | ||
| ## Dark mode setup | ||
| Set Chart.js defaults before creating charts: | ||
| ```javascript | ||
| Chart.defaults.color = '#a1a1aa'; // label/tick color | ||
| Chart.defaults.borderColor = '#27272a'; // grid line color | ||
| ``` | ||
| ## Color palette | ||
| Recommended palette for multi-series charts: | ||
| ```javascript | ||
| const COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316']; | ||
| ``` | ||
| ## See also | ||
| - [sdk.browser](sdk.browser) — Browser / CDN quickstart | ||
| - [sdk.query-reference](sdk.query-reference) — Full query API | ||
| - [sdk.echarts](sdk.echarts) — ECharts alternative | ||
| - [sdk.apexcharts](sdk.apexcharts) — ApexCharts alternative |
| # ECharts + Bonnard SDK | ||
| > Build HTML dashboards with Apache ECharts and the Bonnard SDK. No build step required. | ||
| ECharts offers rich interactivity (tooltips, zoom, legend toggling), built-in dark theme, and extensive chart types. Larger payload than Chart.js (~160KB gzip) but more powerful. | ||
| ## Starter template | ||
| Copy this complete HTML file as a starting point. Replace `bon_pk_YOUR_KEY_HERE` with your publishable API key, and update the view/measure/dimension names to match your schema. | ||
| Use `explore()` to discover available views and fields — see [sdk.query-reference](sdk.query-reference). | ||
| ```html | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>Dashboard</title> | ||
| <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> | ||
| <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script> | ||
| <style> | ||
| * { margin: 0; padding: 0; box-sizing: border-box; } | ||
| body { | ||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | ||
| background: #09090b; color: #fafafa; padding: 24px; | ||
| } | ||
| h1 { font-size: 24px; font-weight: 600; margin-bottom: 24px; } | ||
| .error { color: #ef4444; background: #1c0a0a; padding: 12px; border-radius: 8px; margin-bottom: 16px; display: none; } | ||
| .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 32px; } | ||
| .kpi { background: #18181b; border: 1px solid #27272a; border-radius: 12px; padding: 20px; } | ||
| .kpi-label { font-size: 14px; color: #a1a1aa; margin-bottom: 8px; } | ||
| .kpi-value { font-size: 32px; font-weight: 600; } | ||
| .kpi-value.loading { color: #3f3f46; } | ||
| .charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 24px; } | ||
| .chart-card { background: #18181b; border: 1px solid #27272a; border-radius: 12px; padding: 20px; } | ||
| .chart-title { font-size: 16px; font-weight: 500; margin-bottom: 16px; } | ||
| .chart-container { height: 300px; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <h1>Dashboard</h1> | ||
| <div id="error" class="error"></div> | ||
| <div class="kpis"> | ||
| <div class="kpi"> | ||
| <div class="kpi-label">Revenue</div> | ||
| <div class="kpi-value loading" id="kpi-revenue">--</div> | ||
| </div> | ||
| <div class="kpi"> | ||
| <div class="kpi-label">Orders</div> | ||
| <div class="kpi-value loading" id="kpi-orders">--</div> | ||
| </div> | ||
| <div class="kpi"> | ||
| <div class="kpi-label">Avg Value</div> | ||
| <div class="kpi-value loading" id="kpi-avg">--</div> | ||
| </div> | ||
| </div> | ||
| <div class="charts"> | ||
| <div class="chart-card"> | ||
| <div class="chart-title">Revenue by City</div> | ||
| <div class="chart-container" id="bar-chart"></div> | ||
| </div> | ||
| <div class="chart-card"> | ||
| <div class="chart-title">Revenue Trend</div> | ||
| <div class="chart-container" id="line-chart"></div> | ||
| </div> | ||
| </div> | ||
| <script> | ||
| const bon = Bonnard.createClient({ | ||
| apiKey: 'bon_pk_YOUR_KEY_HERE', | ||
| }); | ||
| // --- Helpers --- | ||
| function showError(msg) { | ||
| const el = document.getElementById('error'); | ||
| el.textContent = msg; | ||
| el.style.display = 'block'; | ||
| } | ||
| function formatNumber(v) { | ||
| return new Intl.NumberFormat().format(v); | ||
| } | ||
| function formatCurrency(v) { | ||
| return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(v); | ||
| } | ||
| // Resize all charts on window resize | ||
| const charts = []; | ||
| window.addEventListener('resize', () => charts.forEach(c => c.resize())); | ||
| // --- Load data --- | ||
| (async () => { | ||
| try { | ||
| // KPIs | ||
| const kpis = await bon.query({ | ||
| measures: ['orders.revenue', 'orders.count', 'orders.avg_value'], | ||
| }); | ||
| if (kpis.data.length > 0) { | ||
| const row = kpis.data[0]; | ||
| document.getElementById('kpi-revenue').textContent = formatCurrency(row['orders.revenue']); | ||
| document.getElementById('kpi-revenue').classList.remove('loading'); | ||
| document.getElementById('kpi-orders').textContent = formatNumber(row['orders.count']); | ||
| document.getElementById('kpi-orders').classList.remove('loading'); | ||
| document.getElementById('kpi-avg').textContent = formatCurrency(row['orders.avg_value']); | ||
| document.getElementById('kpi-avg').classList.remove('loading'); | ||
| } | ||
| // Bar chart — revenue by city | ||
| const byCity = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| dimensions: ['orders.city'], | ||
| orderBy: { 'orders.revenue': 'desc' }, | ||
| limit: 10, | ||
| }); | ||
| const barChart = echarts.init(document.getElementById('bar-chart'), 'dark'); | ||
| charts.push(barChart); | ||
| barChart.setOption({ | ||
| tooltip: { trigger: 'axis', formatter: p => `${p[0].name}: ${formatCurrency(p[0].value)}` }, | ||
| xAxis: { type: 'category', data: byCity.data.map(d => d['orders.city']) }, | ||
| yAxis: { type: 'value', axisLabel: { formatter: v => formatCurrency(v) } }, | ||
| series: [{ | ||
| type: 'bar', | ||
| data: byCity.data.map(d => d['orders.revenue']), | ||
| itemStyle: { color: '#3b82f6', borderRadius: [4, 4, 0, 0] }, | ||
| }], | ||
| grid: { left: 80, right: 20, top: 20, bottom: 40 }, | ||
| }); | ||
| // Line chart — revenue trend | ||
| const trend = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| timeDimension: { | ||
| dimension: 'orders.created_at', | ||
| granularity: 'month', | ||
| dateRange: 'last 12 months', | ||
| }, | ||
| }); | ||
| const lineChart = echarts.init(document.getElementById('line-chart'), 'dark'); | ||
| charts.push(lineChart); | ||
| lineChart.setOption({ | ||
| tooltip: { trigger: 'axis', formatter: p => `${p[0].name}: ${formatCurrency(p[0].value)}` }, | ||
| xAxis: { | ||
| type: 'category', | ||
| data: trend.data.map(d => { | ||
| const date = new Date(d['orders.created_at']); | ||
| return date.toLocaleDateString('en-US', { month: 'short', year: '2-digit' }); | ||
| }), | ||
| }, | ||
| yAxis: { type: 'value', axisLabel: { formatter: v => formatCurrency(v) } }, | ||
| series: [{ | ||
| type: 'line', | ||
| data: trend.data.map(d => d['orders.revenue']), | ||
| smooth: true, | ||
| itemStyle: { color: '#3b82f6' }, | ||
| areaStyle: { color: 'rgba(59, 130, 246, 0.15)' }, | ||
| }], | ||
| grid: { left: 80, right: 20, top: 20, bottom: 40 }, | ||
| }); | ||
| } catch (err) { | ||
| showError('Failed to load dashboard: ' + err.message); | ||
| } | ||
| })(); | ||
| </script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| ## Chart types | ||
| ### Bar chart | ||
| ```javascript | ||
| const chart = echarts.init(el, 'dark'); | ||
| chart.setOption({ | ||
| xAxis: { type: 'category', data: data.map(d => d['view.dimension']) }, | ||
| yAxis: { type: 'value' }, | ||
| series: [{ type: 'bar', data: data.map(d => d['view.measure']) }], | ||
| }); | ||
| ``` | ||
| ### Horizontal bar chart | ||
| ```javascript | ||
| chart.setOption({ | ||
| xAxis: { type: 'value' }, | ||
| yAxis: { type: 'category', data: data.map(d => d['view.dimension']) }, | ||
| series: [{ type: 'bar', data: data.map(d => d['view.measure']) }], | ||
| }); | ||
| ``` | ||
| ### Line chart | ||
| ```javascript | ||
| chart.setOption({ | ||
| xAxis: { type: 'category', data: labels }, | ||
| yAxis: { type: 'value' }, | ||
| series: [{ type: 'line', data: values, smooth: true }], | ||
| }); | ||
| ``` | ||
| ### Area chart | ||
| ```javascript | ||
| series: [{ | ||
| type: 'line', | ||
| data: values, | ||
| smooth: true, | ||
| areaStyle: { color: 'rgba(59, 130, 246, 0.15)' }, | ||
| }] | ||
| ``` | ||
| ### Pie chart | ||
| ```javascript | ||
| chart.setOption({ | ||
| tooltip: { trigger: 'item' }, | ||
| series: [{ | ||
| type: 'pie', | ||
| radius: ['40%', '70%'], // doughnut — use '60%' for full pie | ||
| data: data.map(d => ({ | ||
| name: d['view.dimension'], | ||
| value: d['view.measure'], | ||
| })), | ||
| }], | ||
| }); | ||
| ``` | ||
| ### Multi-series line chart | ||
| ```javascript | ||
| const cities = [...new Set(data.map(d => d['orders.city']))]; | ||
| const dates = [...new Set(data.map(d => d['orders.created_at']))]; | ||
| chart.setOption({ | ||
| tooltip: { trigger: 'axis' }, | ||
| legend: { data: cities }, | ||
| xAxis: { type: 'category', data: dates.map(d => new Date(d).toLocaleDateString()) }, | ||
| yAxis: { type: 'value' }, | ||
| series: cities.map(city => ({ | ||
| name: city, | ||
| type: 'line', | ||
| smooth: true, | ||
| data: dates.map(date => | ||
| data.find(d => d['orders.city'] === city && d['orders.created_at'] === date)?.['orders.revenue'] || 0 | ||
| ), | ||
| })), | ||
| }); | ||
| ``` | ||
| ## Dark mode | ||
| ECharts has a built-in dark theme — pass `'dark'` as the second argument to `echarts.init()`: | ||
| ```javascript | ||
| const chart = echarts.init(document.getElementById('chart'), 'dark'); | ||
| ``` | ||
| The dark theme sets appropriate background, text, axis, and tooltip colors automatically. | ||
| ## Resize handling | ||
| ECharts charts don't auto-resize. Add a resize listener: | ||
| ```javascript | ||
| const charts = []; | ||
| // After creating each chart: | ||
| charts.push(chart); | ||
| // Global resize handler: | ||
| window.addEventListener('resize', () => charts.forEach(c => c.resize())); | ||
| ``` | ||
| ## Color palette | ||
| ```javascript | ||
| const COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316']; | ||
| // Apply to a series: | ||
| series: data.map((d, i) => ({ | ||
| // ... | ||
| itemStyle: { color: COLORS[i % COLORS.length] }, | ||
| })) | ||
| ``` | ||
| ## See also | ||
| - [sdk.browser](sdk.browser) — Browser / CDN quickstart | ||
| - [sdk.query-reference](sdk.query-reference) — Full query API | ||
| - [sdk.chartjs](sdk.chartjs) — Chart.js alternative (smaller payload) | ||
| - [sdk.apexcharts](sdk.apexcharts) — ApexCharts alternative |
| # SDK | ||
| > Build data apps and dashboards on top of your semantic layer. | ||
| The Bonnard SDK (`@bonnard/sdk`) is a lightweight TypeScript/JavaScript client for querying your deployed semantic layer. Zero dependencies, ~3KB minified — works in Node.js, browsers, and edge runtimes. | ||
| ## Two ways to use it | ||
| ### npm (TypeScript / Node.js / React) | ||
| ```bash | ||
| npm install @bonnard/sdk | ||
| ``` | ||
| ```typescript | ||
| import { createClient } from '@bonnard/sdk'; | ||
| const bon = createClient({ apiKey: 'bon_pk_...' }); | ||
| const { data } = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| dimensions: ['orders.city'], | ||
| }); | ||
| ``` | ||
| ### CDN (browser / HTML dashboards) | ||
| ```html | ||
| <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script> | ||
| <script> | ||
| const bon = Bonnard.createClient({ apiKey: 'bon_pk_...' }); | ||
| </script> | ||
| ``` | ||
| Exposes `window.Bonnard` with `createClient` and `toCubeQuery`. See [sdk.browser](sdk.browser) for the full browser quickstart. | ||
| ## Authentication | ||
| Two modes depending on your use case: | ||
| | Mode | Key type | Use case | | ||
| |------|----------|----------| | ||
| | **Publishable key** | `bon_pk_...` | Public dashboards, client-side apps — safe to expose in HTML | | ||
| | **Token exchange** | `bon_sk_...` → JWT | Multi-tenant / embedded analytics — server exchanges secret key for scoped token | | ||
| ```typescript | ||
| // Simple: publishable key | ||
| const bon = createClient({ apiKey: 'bon_pk_...' }); | ||
| // Multi-tenant: token exchange | ||
| const bon = createClient({ | ||
| fetchToken: async () => { | ||
| const res = await fetch('/my-backend/bonnard-token'); | ||
| const { token } = await res.json(); | ||
| return token; | ||
| }, | ||
| }); | ||
| ``` | ||
| The SDK automatically caches tokens and refreshes them 60 seconds before expiry. | ||
| See [sdk.authentication](sdk.authentication) for the full auth guide. | ||
| ## What you can query | ||
| | Method | Purpose | | ||
| |--------|---------| | ||
| | `query()` | JSON query — measures, dimensions, filters, time dimensions | | ||
| | `rawQuery()` | Pass a Cube-native query object directly | | ||
| | `sql()` | SQL with `MEASURE()` syntax | | ||
| | `explore()` | Discover available views, measures, and dimensions | | ||
| See [sdk.query-reference](sdk.query-reference) for the full API reference. | ||
| ## Building HTML dashboards | ||
| The SDK pairs with any chart library for single-file HTML dashboards. No build step required — load both from CDN and start querying. | ||
| | Library | CDN size (gzip) | Best for | | ||
| |---------|----------------|----------| | ||
| | **Chart.js** | ~65 KB | Default choice — most LLM training data, smallest payload | | ||
| | **ECharts** | ~160 KB | Rich interactivity, built-in dark theme | | ||
| | **ApexCharts** | ~130 KB | Best defaults out of box, SVG rendering | | ||
| Each guide includes a complete, copy-pasteable HTML starter template: | ||
| - [sdk.chartjs](sdk.chartjs) — Chart.js + SDK | ||
| - [sdk.echarts](sdk.echarts) — ECharts + SDK | ||
| - [sdk.apexcharts](sdk.apexcharts) — ApexCharts + SDK | ||
| ## See also | ||
| - [sdk.browser](sdk.browser) — Browser / CDN quickstart | ||
| - [sdk.query-reference](sdk.query-reference) — Full query API reference | ||
| - [sdk.authentication](sdk.authentication) — Auth patterns | ||
| - [security-context](security-context) — Row-level security for multi-tenant apps |
| # Query API Reference | ||
| > Complete reference for querying the Bonnard semantic layer via the SDK. | ||
| ## query() | ||
| Execute a JSON query against the semantic layer. All field names must be fully qualified (e.g. `orders.revenue`, not `revenue`). | ||
| ```javascript | ||
| const { data } = await bon.query({ | ||
| measures: ['orders.revenue', 'orders.count'], | ||
| dimensions: ['orders.city'], | ||
| filters: [ | ||
| { dimension: 'orders.status', operator: 'equals', values: ['completed'] }, | ||
| ], | ||
| timeDimension: { | ||
| dimension: 'orders.created_at', | ||
| granularity: 'month', | ||
| dateRange: ['2025-01-01', '2025-12-31'], | ||
| }, | ||
| orderBy: { 'orders.revenue': 'desc' }, | ||
| limit: 10, | ||
| }); | ||
| ``` | ||
| ### Parameters | ||
| | Parameter | Type | Required | Description | | ||
| |-----------|------|----------|-------------| | ||
| | `measures` | `string[]` | No | Numeric aggregations to compute (e.g. `['orders.revenue']`) | | ||
| | `dimensions` | `string[]` | No | Group-by columns (e.g. `['orders.city']`) | | ||
| | `filters` | `Filter[]` | No | Row-level filters | | ||
| | `timeDimension` | `TimeDimension` | No | Time-based grouping and date range | | ||
| | `orderBy` | `Record<string, 'asc' \| 'desc'>` | No | Sort order | | ||
| | `limit` | `number` | No | Max rows to return | | ||
| At least one of `measures` or `dimensions` is required. | ||
| ### Response | ||
| ```javascript | ||
| { | ||
| data: [ | ||
| { "orders.revenue": 125000, "orders.count": 340, "orders.city": "Berlin" }, | ||
| { "orders.revenue": 98000, "orders.count": 280, "orders.city": "Munich" }, | ||
| ], | ||
| annotation: { | ||
| measures: { | ||
| "orders.revenue": { title: "Revenue", type: "number" }, | ||
| "orders.count": { title: "Count", type: "number" }, | ||
| }, | ||
| dimensions: { | ||
| "orders.city": { title: "City", type: "string" }, | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
| - `data` — array of result rows, keyed by fully qualified field names | ||
| - `annotation` — optional metadata with field titles and types | ||
| ## Filters | ||
| ```javascript | ||
| filters: [ | ||
| { dimension: 'orders.status', operator: 'equals', values: ['completed'] }, | ||
| { dimension: 'orders.revenue', operator: 'gt', values: [1000] }, | ||
| ] | ||
| ``` | ||
| ### Filter operators | ||
| | Operator | Description | Example values | | ||
| |----------|-------------|---------------| | ||
| | `equals` | Exact match (any of values) | `['completed', 'shipped']` | | ||
| | `notEquals` | Exclude matches | `['cancelled']` | | ||
| | `contains` | Substring match | `['berlin']` | | ||
| | `gt` | Greater than | `[1000]` | | ||
| | `gte` | Greater than or equal | `[1000]` | | ||
| | `lt` | Less than | `[100]` | | ||
| | `lte` | Less than or equal | `[100]` | | ||
| ## Time dimensions | ||
| Group data by time periods and filter by date range. | ||
| ```javascript | ||
| timeDimension: { | ||
| dimension: 'orders.created_at', | ||
| granularity: 'month', | ||
| dateRange: ['2025-01-01', '2025-06-30'], | ||
| } | ||
| ``` | ||
| ### Granularities | ||
| | Granularity | Groups by | | ||
| |-------------|-----------| | ||
| | `day` | Calendar day | | ||
| | `week` | ISO week (Monday start) | | ||
| | `month` | Calendar month | | ||
| | `quarter` | Calendar quarter | | ||
| | `year` | Calendar year | | ||
| Omit `granularity` to filter by date range without time grouping. | ||
| ### Date range formats | ||
| ```javascript | ||
| // ISO date strings (tuple) | ||
| dateRange: ['2025-01-01', '2025-12-31'] | ||
| // Single string (relative) | ||
| dateRange: 'last 30 days' | ||
| dateRange: 'last 6 months' | ||
| dateRange: 'this year' | ||
| dateRange: 'last year' | ||
| dateRange: 'today' | ||
| dateRange: 'yesterday' | ||
| dateRange: 'last week' | ||
| dateRange: 'last month' | ||
| dateRange: 'last quarter' | ||
| ``` | ||
| ## Ordering and pagination | ||
| ```javascript | ||
| // Sort by revenue descending | ||
| orderBy: { 'orders.revenue': 'desc' } | ||
| // Multiple sort keys | ||
| orderBy: { 'orders.city': 'asc', 'orders.revenue': 'desc' } | ||
| // Limit results | ||
| limit: 10 | ||
| ``` | ||
| ## rawQuery() | ||
| Pass a Cube-native query object directly, bypassing the SDK's query format conversion. Use when you need features not exposed by `query()` (e.g. segments, offset). | ||
| ```javascript | ||
| const { data } = await bon.rawQuery({ | ||
| measures: ['orders.revenue'], | ||
| dimensions: ['orders.city'], | ||
| order: [['orders.revenue', 'desc']], | ||
| limit: 10, | ||
| offset: 20, | ||
| }); | ||
| ``` | ||
| ## sql() | ||
| Execute a SQL query using Cube's SQL API. Use `MEASURE()` to reference semantic layer measures. | ||
| ```javascript | ||
| const { data } = await bon.sql( | ||
| `SELECT city, MEASURE(revenue), MEASURE(count) | ||
| FROM orders | ||
| WHERE status = 'completed' | ||
| GROUP BY 1 | ||
| ORDER BY 2 DESC | ||
| LIMIT 10` | ||
| ); | ||
| ``` | ||
| Response includes an optional schema: | ||
| ```javascript | ||
| { | ||
| data: [ | ||
| { city: "Berlin", revenue: 125000, count: 340 }, | ||
| ], | ||
| schema: [ | ||
| { name: "city", type: "string" }, | ||
| { name: "revenue", type: "number" }, | ||
| { name: "count", type: "number" }, | ||
| ] | ||
| } | ||
| ``` | ||
| ## explore() | ||
| Discover available views, measures, dimensions, and segments. | ||
| ```javascript | ||
| const meta = await bon.explore(); | ||
| for (const view of meta.cubes) { | ||
| console.log(`${view.name}: ${view.description || ''}`); | ||
| for (const m of view.measures) { | ||
| console.log(` measure: ${m.name} (${m.type})`); | ||
| } | ||
| for (const d of view.dimensions) { | ||
| console.log(` dimension: ${d.name} (${d.type})`); | ||
| } | ||
| } | ||
| ``` | ||
| By default returns only views (`viewsOnly: true`). To include underlying cubes: | ||
| ```javascript | ||
| const meta = await bon.explore({ viewsOnly: false }); | ||
| ``` | ||
| ### Response shape | ||
| ```javascript | ||
| { | ||
| cubes: [ | ||
| { | ||
| name: "orders", | ||
| title: "Orders", | ||
| description: "Customer orders", | ||
| type: "view", | ||
| measures: [ | ||
| { name: "orders.revenue", title: "Revenue", type: "number" }, | ||
| { name: "orders.count", title: "Count", type: "number" }, | ||
| ], | ||
| dimensions: [ | ||
| { name: "orders.city", title: "City", type: "string" }, | ||
| { name: "orders.created_at", title: "Created At", type: "time" }, | ||
| ], | ||
| segments: [], | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
| ## toCubeQuery() | ||
| Utility function that converts SDK `QueryOptions` into a Cube-native query object. Useful for debugging or when you need to inspect the query before sending. | ||
| ```javascript | ||
| import { toCubeQuery } from '@bonnard/sdk'; | ||
| // or in browser: Bonnard.toCubeQuery(...) | ||
| const cubeQuery = Bonnard.toCubeQuery({ | ||
| measures: ['orders.revenue'], | ||
| dimensions: ['orders.city'], | ||
| orderBy: { 'orders.revenue': 'desc' }, | ||
| }); | ||
| console.log(JSON.stringify(cubeQuery, null, 2)); | ||
| // { | ||
| // "measures": ["orders.revenue"], | ||
| // "dimensions": ["orders.city"], | ||
| // "order": [["orders.revenue", "desc"]] | ||
| // } | ||
| ``` | ||
| ## Common patterns | ||
| ### KPI query (single row, no dimensions) | ||
| ```javascript | ||
| const { data } = await bon.query({ | ||
| measures: ['orders.revenue', 'orders.count', 'orders.avg_value'], | ||
| }); | ||
| const kpis = data[0]; | ||
| // { "orders.revenue": 1250000, "orders.count": 3400, "orders.avg_value": 367.6 } | ||
| ``` | ||
| ### Top N with dimension | ||
| ```javascript | ||
| const { data } = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| dimensions: ['orders.city'], | ||
| orderBy: { 'orders.revenue': 'desc' }, | ||
| limit: 10, | ||
| }); | ||
| ``` | ||
| ### Time series | ||
| ```javascript | ||
| const { data } = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| timeDimension: { | ||
| dimension: 'orders.created_at', | ||
| granularity: 'month', | ||
| dateRange: 'last 12 months', | ||
| }, | ||
| }); | ||
| // data[0]["orders.created_at"] is an ISO date string like "2025-01-01T00:00:00.000" | ||
| ``` | ||
| ### Filtered breakdown | ||
| ```javascript | ||
| const { data } = await bon.query({ | ||
| measures: ['orders.revenue'], | ||
| dimensions: ['orders.product_category'], | ||
| filters: [ | ||
| { dimension: 'orders.city', operator: 'equals', values: ['Berlin'] }, | ||
| { dimension: 'orders.revenue', operator: 'gt', values: [100] }, | ||
| ], | ||
| orderBy: { 'orders.revenue': 'desc' }, | ||
| }); | ||
| ``` | ||
| ## See also | ||
| - [sdk.browser](sdk.browser) — Browser / CDN quickstart | ||
| - [sdk.authentication](sdk.authentication) — Auth patterns | ||
| - [sdk.chartjs](sdk.chartjs) — Building dashboards with Chart.js |
| --- | ||
| name: bonnard-build-dashboard | ||
| description: Guide a user through building and deploying a data dashboard using the Bonnard SDK and Chart.js. Use when user says "build a dashboard", "create a chart", "visualize data", or wants to deploy an HTML dashboard. | ||
| allowed-tools: Bash(bon *) | ||
| --- | ||
| # Build & Deploy a Dashboard | ||
| This skill guides you through creating an HTML dashboard powered by the | ||
| Bonnard SDK and deploying it as a hosted dashboard. | ||
| ## Phase 1: Get the Starter Template | ||
| Fetch the SDK + Chart.js starter template: | ||
| ```bash | ||
| bon docs sdk.chartjs | ||
| ``` | ||
| This shows a working HTML template with `@bonnard/sdk` loaded from CDN and | ||
| a Chart.js example. Copy this as your starting point. | ||
| ## Phase 2: Explore Available Data | ||
| Discover what measures and dimensions are available to query: | ||
| ```bash | ||
| # List all views and their fields | ||
| bon docs schema | ||
| # Query a specific view to see what data looks like | ||
| bon query '{"measures": ["view_name.measure"], "dimensions": ["view_name.dimension"], "limit": 5}' | ||
| # Or use SQL format | ||
| bon query --sql "SELECT MEASURE(total_revenue), date FROM sales_performance LIMIT 5" | ||
| ``` | ||
| Ask the user what data they want to visualize. Match their request to | ||
| available views and measures. | ||
| ## Phase 3: Build the HTML File | ||
| Create an HTML file that: | ||
| 1. Loads `@bonnard/sdk` from CDN (see the template from Phase 1) | ||
| 2. Initializes the SDK with a publishable key placeholder | ||
| 3. Queries the semantic layer for the data the user wants | ||
| 4. Renders charts using Chart.js (or another chart library) | ||
| Key points: | ||
| - The SDK uses `Bonnard.query()` to fetch data from the deployed semantic layer | ||
| - Use the view names and measure/dimension names from Phase 2 | ||
| - The publishable key will be provided by the user or can be created with `bon keys create` | ||
| - Keep the HTML self-contained (inline CSS/JS, load libraries from CDN) | ||
| Save the file (e.g., `dashboard.html`). | ||
| ## Phase 4: Preview Locally | ||
| Let the user preview the file in their browser before deploying. | ||
| They'll need to set a valid publishable key in the HTML first. | ||
| ```bash | ||
| # Create a publishable key if needed | ||
| bon keys create --name "Dashboard" --type publishable | ||
| ``` | ||
| ## Phase 5: Deploy | ||
| Deploy the HTML file as a hosted dashboard on Bonnard: | ||
| ```bash | ||
| bon dashboard deploy dashboard.html | ||
| ``` | ||
| This will: | ||
| - Upload the HTML content | ||
| - Assign a slug (derived from filename, or use `--slug`) | ||
| - Print the public URL where the dashboard is accessible | ||
| Options: | ||
| - `--slug <slug>` — custom URL slug (default: derived from filename) | ||
| - `--title <title>` — dashboard title (default: from `<title>` tag) | ||
| ## Phase 6: View Live | ||
| Open the deployed dashboard in the browser: | ||
| ```bash | ||
| bon dashboard open dashboard | ||
| ``` | ||
| ## Iteration | ||
| To update the dashboard, edit the HTML file and redeploy: | ||
| ```bash | ||
| bon dashboard deploy dashboard.html | ||
| ``` | ||
| Each deploy increments the version. Use `bon dashboard list` to see all | ||
| deployed dashboards with their versions and URLs. | ||
| To remove a dashboard: | ||
| ```bash | ||
| bon dashboard remove dashboard | ||
| ``` |
Sorry, the diff of this file is not supported yet
@@ -33,3 +33,3 @@ --- | ||
| Supported types: `postgres`, `redshift`, `snowflake`, `bigquery`, `databricks`. | ||
| Supported types: `postgres`, `redshift`, `snowflake`, `bigquery`, `databricks`, `duckdb`. | ||
@@ -36,0 +36,0 @@ The demo option adds a read-only Contoso retail dataset with tables like |
@@ -68,3 +68,3 @@ --- | ||
| Supported types: `postgres`, `redshift`, `snowflake`, `bigquery`, `databricks`. | ||
| Supported types: `postgres`, `redshift`, `snowflake`, `bigquery`, `databricks`, `duckdb`. | ||
@@ -71,0 +71,0 @@ The connection will be tested automatically during `bon deploy`. |
@@ -8,3 +8,3 @@ # Bonnard Semantic Layer | ||
| ``` | ||
| Data Warehouse (Snowflake, Postgres, BigQuery, Databricks) | ||
| Data Warehouse (Snowflake, Postgres, BigQuery, Databricks, DuckDB) | ||
| ↓ | ||
@@ -77,2 +77,6 @@ Cubes (measures, dimensions, joins) | ||
| | `bon docs` | Browse documentation | | ||
| | `bon dashboard deploy <file>` | Deploy an HTML file as a hosted dashboard | | ||
| | `bon dashboard list` | List deployed dashboards with URLs | | ||
| | `bon dashboard remove <slug>` | Remove a deployed dashboard | | ||
| | `bon dashboard open <slug>` | Open a deployed dashboard in the browser | | ||
| | `bon metabase connect` | Connect to a Metabase instance (API key) | | ||
@@ -115,2 +119,3 @@ | `bon metabase analyze` | Generate analysis report for semantic layer planning | | ||
| For design principles: `/bonnard-design-guide` | ||
| For building dashboards with SDK + charts: `/bonnard-build-dashboard` | ||
@@ -117,0 +122,0 @@ ## Deployment & Change Tracking |
+2
-2
| { | ||
| "name": "@bonnard/cli", | ||
| "version": "0.2.15", | ||
| "version": "0.2.16", | ||
| "type": "module", | ||
@@ -12,3 +12,3 @@ "bin": { | ||
| "scripts": { | ||
| "build": "tsdown src/bin/bon.ts --format esm --out-dir dist/bin && cp -r src/templates dist/ && mkdir -p dist/docs/topics dist/docs/schemas && cp ./content/index.md dist/docs/_index.md && cp ./content/overview.md ./content/getting-started.md dist/docs/topics/ && cp ./content/modeling/*.md dist/docs/topics/ && cp ./content/dashboards/*.md dist/docs/topics/", | ||
| "build": "tsdown src/bin/bon.ts --format esm --out-dir dist/bin && cp -r src/templates dist/ && mkdir -p dist/docs/topics dist/docs/schemas && cp ./content/index.md dist/docs/_index.md && cp ./content/overview.md ./content/getting-started.md dist/docs/topics/ && cp ./content/modeling/*.md dist/docs/topics/ && cp ./content/sdk/*.md dist/docs/topics/", | ||
| "dev": "tsdown src/bin/bon.ts --format esm --out-dir dist/bin --watch", | ||
@@ -15,0 +15,0 @@ "test": "vitest run" |
+2
-2
@@ -101,3 +101,3 @@ <p align="center"> | ||
| ```bash | ||
| bon mcp setup # Configure MCP server | ||
| bon mcp # Show MCP server setup instructions | ||
| bon mcp test # Verify the connection | ||
@@ -176,3 +176,3 @@ ``` | ||
| | `bon query` | Run queries from the terminal (JSON or SQL) | | ||
| | `bon mcp setup` | Configure MCP server for agent access | | ||
| | `bon mcp` | Show MCP server setup instructions | | ||
| | `bon mcp test` | Test MCP connection | | ||
@@ -179,0 +179,0 @@ | `bon docs` | Browse or search documentation from the CLI | |
| # Components | ||
| > Chart and display components for rendering query results in dashboards. | ||
| ## Overview | ||
| Components are self-closing HTML-style tags that render query results as charts, tables, or KPI cards. Each component takes a `data` prop referencing a named query. | ||
| Choose the component that best fits your data: | ||
| - **BigValue** — single KPI number (total revenue, order count) | ||
| - **LineChart** — trends over time | ||
| - **BarChart** — comparing categories (vertical or horizontal) | ||
| - **AreaChart** — cumulative or stacked trends | ||
| - **PieChart** — proportional breakdown (best with 5-7 slices) | ||
| - **DataTable** — detailed rows for drilling into data | ||
| ## Syntax | ||
| ```markdown | ||
| <ComponentName data={query_name} prop="value" /> | ||
| ``` | ||
| - Components are self-closing (`/>`) | ||
| - `data` uses curly braces: `data={query_name}` | ||
| - Other props use quotes: `x="orders.city"` | ||
| - Boolean props can be shorthand: `horizontal` | ||
| ## Component Reference | ||
| ### BigValue | ||
| Displays a single KPI metric as a large number. | ||
| ```markdown | ||
| <BigValue data={total_revenue} value="orders.total_revenue" title="Revenue" /> | ||
| ``` | ||
| | Prop | Type | Required | Description | | ||
| |------|------|----------|-------------| | ||
| | `data` | query ref | Yes | Query name (should return a single row) | | ||
| | `value` | string | Yes | Fully qualified measure field name to display | | ||
| | `title` | string | No | Label above the value | | ||
| | `fmt` | string | No | Format preset or Excel code (e.g. `fmt="eur2"`, `fmt="$#,##0.00"`) | | ||
| ### LineChart | ||
| Renders a line chart, typically for time series. Supports multiple y columns and series splitting. | ||
| ```markdown | ||
| <LineChart data={monthly_revenue} x="orders.created_at" y="orders.total_revenue" title="Revenue Trend" /> | ||
| <LineChart data={trend} x="orders.created_at" y="orders.total_revenue,orders.count" /> | ||
| <LineChart data={revenue_by_type} x="orders.created_at" y="orders.total_revenue" series="orders.type" /> | ||
| ``` | ||
| | Prop | Type | Required | Description | | ||
| |------|------|----------|-------------| | ||
| | `data` | query ref | Yes | Query name | | ||
| | `x` | string | Yes | Field for x-axis (typically a time dimension) | | ||
| | `y` | string | Yes | Field(s) for y-axis. Comma-separated for multiple (e.g. `y="orders.total_revenue,orders.count"`) | | ||
| | `title` | string | No | Chart title | | ||
| | `series` | string | No | Column to split data into separate colored lines | | ||
| | `type` | string | No | `"stacked"` for stacked lines (default: no stacking) | | ||
| | `yFmt` | string | No | Format preset or Excel code for tooltip values (e.g. `yFmt="eur2"`) | | ||
| ### BarChart | ||
| Renders a vertical bar chart. Add `horizontal` for horizontal bars. Supports multi-series with stacked or grouped display. | ||
| ```markdown | ||
| <BarChart data={revenue_by_city} x="orders.city" y="orders.total_revenue" /> | ||
| <BarChart data={revenue_by_city} x="orders.city" y="orders.total_revenue" horizontal /> | ||
| <BarChart data={revenue_by_type} x="orders.created_at" y="orders.total_revenue" series="orders.type" /> | ||
| <BarChart data={revenue_by_type} x="orders.created_at" y="orders.total_revenue" series="orders.type" type="grouped" /> | ||
| ``` | ||
| | Prop | Type | Required | Description | | ||
| |------|------|----------|-------------| | ||
| | `data` | query ref | Yes | Query name | | ||
| | `x` | string | Yes | Field for category axis | | ||
| | `y` | string | Yes | Field(s) for value axis. Comma-separated for multiple (e.g. `y="orders.total_revenue,orders.count"`) | | ||
| | `title` | string | No | Chart title | | ||
| | `horizontal` | boolean | No | Render as horizontal bar chart | | ||
| | `series` | string | No | Column to split data into separate colored bars | | ||
| | `type` | string | No | `"stacked"` (default) or `"grouped"` for multi-series display | | ||
| | `yFmt` | string | No | Format preset or Excel code for tooltip values (e.g. `yFmt="usd"`) | | ||
| ### AreaChart | ||
| Renders a filled area chart. Supports series splitting and stacked areas. | ||
| ```markdown | ||
| <AreaChart data={monthly_revenue} x="orders.created_at" y="orders.total_revenue" /> | ||
| <AreaChart data={revenue_by_source} x="orders.created_at" y="orders.total_revenue" series="orders.source" type="stacked" /> | ||
| ``` | ||
| | Prop | Type | Required | Description | | ||
| |------|------|----------|-------------| | ||
| | `data` | query ref | Yes | Query name | | ||
| | `x` | string | Yes | Field for x-axis | | ||
| | `y` | string | Yes | Field(s) for y-axis. Comma-separated for multiple (e.g. `y="orders.total_revenue,orders.count"`) | | ||
| | `title` | string | No | Chart title | | ||
| | `series` | string | No | Column to split data into separate colored areas | | ||
| | `type` | string | No | `"stacked"` for stacked areas (default: no stacking) | | ||
| | `yFmt` | string | No | Format preset or Excel code for tooltip values (e.g. `yFmt="pct1"`) | | ||
| ### PieChart | ||
| Renders a pie/donut chart. | ||
| ```markdown | ||
| <PieChart data={by_status} name="orders.status" value="orders.count" title="Order Status" /> | ||
| ``` | ||
| | Prop | Type | Required | Description | | ||
| |------|------|----------|-------------| | ||
| | `data` | query ref | Yes | Query name | | ||
| | `name` | string | Yes | Field for slice labels | | ||
| | `value` | string | Yes | Field for slice values | | ||
| | `title` | string | No | Chart title | | ||
| ### DataTable | ||
| Renders query results as a sortable, paginated table. Click any column header to sort ascending/descending. | ||
| ```markdown | ||
| <DataTable data={top_products} /> | ||
| <DataTable data={top_products} columns="orders.category,orders.total_revenue,orders.count" /> | ||
| <DataTable data={top_products} rows="25" /> | ||
| <DataTable data={top_products} rows="all" /> | ||
| ``` | ||
| | Prop | Type | Required | Description | | ||
| |------|------|----------|-------------| | ||
| | `data` | query ref | Yes | Query name | | ||
| | `columns` | string | No | Comma-separated list of columns to show (default: all) | | ||
| | `title` | string | No | Table title | | ||
| | `fmt` | string | No | Column format map: `fmt="orders.total_revenue:eur2,orders.created_at:shortdate"` | | ||
| | `rows` | string | No | Rows per page. Default `10`. Use `rows="all"` to disable pagination. | | ||
| **Sorting:** Click a column header to sort ascending. Click again to sort descending. Null values always sort to the end. Numbers sort numerically, strings sort case-insensitively. | ||
| **Formatting:** Numbers right-align with tabular figures. Dates auto-detect and won't wrap. Use `fmt` for explicit formatting per column. | ||
| ## Layout | ||
| ### Auto BigValue Grouping | ||
| Consecutive `<BigValue>` components are automatically wrapped in a responsive grid — no `<Grid>` tag needed: | ||
| ```markdown | ||
| <BigValue data={total_revenue} value="orders.total_revenue" title="Revenue" /> | ||
| <BigValue data={order_count} value="orders.count" title="Orders" /> | ||
| <BigValue data={avg_order} value="orders.avg_order_value" title="Avg Order" /> | ||
| ``` | ||
| This renders as a 3-column row. The grid auto-sizes up to 4 columns based on the number of consecutive BigValues. For more control, use an explicit `<Grid>` tag. | ||
| ### Grid | ||
| Wrap components in a `<Grid>` tag to arrange them in columns: | ||
| ```markdown | ||
| <Grid cols="3"> | ||
| <BigValue data={total_orders} value="orders.count" title="Orders" /> | ||
| <BigValue data={total_revenue} value="orders.total_revenue" title="Revenue" /> | ||
| <BigValue data={avg_order} value="orders.avg_order_value" title="Avg Order" /> | ||
| </Grid> | ||
| ``` | ||
| | Prop | Type | Default | Description | | ||
| |------|------|---------|-------------| | ||
| | `cols` | string | `"2"` | Number of columns in the grid | | ||
| ### Layout Best Practices | ||
| **Use `##` for sections, not `#`.** The `#` heading renders very large and wastes vertical space. Use `##` for section titles and `###` for subsections. Reserve `#` for the dashboard title only (which is set in frontmatter, not in the body). | ||
| **Group related charts side by side.** Wrap pairs of charts in `<Grid cols="2">` to avoid long vertical scrolling: | ||
| ```markdown | ||
| <Grid cols="2"> | ||
| <BarChart data={by_channel} x="orders.channel" y="orders.total_revenue" title="By Channel" /> | ||
| <BarChart data={by_city} x="orders.city" y="orders.total_revenue" title="By City" horizontal /> | ||
| </Grid> | ||
| ``` | ||
| **Start each section with KPIs.** Place `<BigValue>` cards at the top of a section for at-a-glance metrics, then follow with charts for detail. | ||
| **Only Grid together components of similar height.** Don't mix a `<BigValue>` with a chart in the same `<Grid>` row — the grid stretches both cells to the tallest item, leaving the BigValue card with a large empty area. Instead, place KPIs in their own row (consecutive BigValues auto-group) and pair charts with other charts of similar size. | ||
| **Keep it compact.** A good dashboard fits key information in 2-3 screens of scrolling. Use Grids, concise titles, and avoid unnecessary headings between every chart. | ||
| ## Formatting | ||
| Values are auto-formatted by default — numbers get locale grouping (1,234.56), dates display as "13 Jan 2025", and nulls show as "—". Override with named presets for common currencies and percentages, or use raw Excel format codes for full control. | ||
| ### Format Presets | ||
| | Preset | Excel code | Example output | | ||
| |--------|-----------|---------------| | ||
| | `num0` | `#,##0` | 1,234 | | ||
| | `num1` | `#,##0.0` | 1,234.6 | | ||
| | `num2` | `#,##0.00` | 1,234.56 | | ||
| | `usd` | `$#,##0` | $1,234 | | ||
| | `usd2` | `$#,##0.00` | $1,234.56 | | ||
| | `eur` | `#,##0 "€"` | 1,234 € | | ||
| | `eur2` | `#,##0.00 "€"` | 1,234.56 € | | ||
| | `gbp` | `£#,##0` | £1,234 | | ||
| | `gbp2` | `£#,##0.00` | £1,234.56 | | ||
| | `chf` | `"CHF "#,##0` | CHF 1,234 | | ||
| | `chf2` | `"CHF "#,##0.00` | CHF 1,234.56 | | ||
| | `pct` | `0%` | 45% | | ||
| | `pct1` | `0.0%` | 45.1% | | ||
| | `pct2` | `0.00%` | 45.12% | | ||
| | `shortdate` | `d mmm yyyy` | 13 Jan 2025 | | ||
| | `longdate` | `d mmmm yyyy` | 13 January 2025 | | ||
| | `monthyear` | `mmm yyyy` | Jan 2025 | | ||
| Any string that isn't a preset name is treated as a raw Excel format code (ECMA-376). For example: `fmt="orders.total_revenue:$#,##0.00"`. | ||
| Note: Percentage presets (`pct`, `pct1`, `pct2`) multiply by 100 per Excel convention — 0.45 displays as "45%". | ||
| ### Usage Examples | ||
| ```markdown | ||
| <!-- BigValue with currency --> | ||
| <BigValue data={total_revenue} value="orders.total_revenue" title="Revenue" fmt="eur2" /> | ||
| <!-- DataTable with per-column formatting --> | ||
| <DataTable data={sales} fmt="orders.total_revenue:usd2,orders.created_at:shortdate,orders.margin:pct1" /> | ||
| <!-- Chart with formatted tooltips --> | ||
| <BarChart data={monthly} x="orders.created_at" y="orders.total_revenue" yFmt="usd" /> | ||
| <LineChart data={trend} x="orders.created_at" y="orders.growth" yFmt="pct1" /> | ||
| ``` | ||
| ## Field Names | ||
| All field names in component props must be **fully qualified** with the view or cube name — the same format used in query blocks. For example, use `value="orders.total_revenue"` not `value="total_revenue"`. | ||
| ## See Also | ||
| - [Queries](dashboards.queries) — query syntax and properties | ||
| - [Examples](dashboards.examples) — complete dashboard examples | ||
| - [Dashboards](dashboards) — overview and deployment |
| # Examples | ||
| > Complete dashboard examples showing common patterns. | ||
| ## Revenue Overview Dashboard | ||
| The most common dashboard pattern: KPI cards at the top for at-a-glance metrics, a time series chart for trends, and a bar chart with data table for category breakdown. | ||
| ```markdown | ||
| --- | ||
| title: Revenue Overview | ||
| description: Monthly revenue trends and breakdowns | ||
| --- | ||
| # Revenue Overview | ||
| ` ``query total_revenue | ||
| measures: [orders.total_revenue] | ||
| ` `` | ||
| ` ``query order_count | ||
| measures: [orders.count] | ||
| ` `` | ||
| ` ``query avg_order | ||
| measures: [orders.avg_order_value] | ||
| ` `` | ||
| <Grid cols="3"> | ||
| <BigValue data={total_revenue} value="orders.total_revenue" title="Total Revenue" /> | ||
| <BigValue data={order_count} value="orders.count" title="Orders" /> | ||
| <BigValue data={avg_order} value="orders.avg_order_value" title="Avg Order" /> | ||
| </Grid> | ||
| ## Monthly Trend | ||
| ` ``query monthly_revenue | ||
| measures: [orders.total_revenue] | ||
| timeDimension: | ||
| dimension: orders.created_at | ||
| granularity: month | ||
| dateRange: [2025-01-01, 2025-12-31] | ||
| ` `` | ||
| <LineChart data={monthly_revenue} x="orders.created_at" y="orders.total_revenue" title="Monthly Revenue" /> | ||
| ## By Category | ||
| ` ``query by_category | ||
| measures: [orders.total_revenue, orders.count] | ||
| dimensions: [orders.category] | ||
| orderBy: | ||
| orders.total_revenue: desc | ||
| ` `` | ||
| <BarChart data={by_category} x="orders.category" y="orders.total_revenue" title="Revenue by Category" /> | ||
| <DataTable data={by_category} /> | ||
| ``` | ||
| ## Sales Pipeline Dashboard | ||
| A status-focused dashboard using a pie chart for proportional breakdown, a horizontal bar chart for ranking, and filters to drill into a specific segment. | ||
| ```markdown | ||
| --- | ||
| title: Sales Pipeline | ||
| description: Order status breakdown and city analysis | ||
| --- | ||
| # Sales Pipeline | ||
| ` ``query by_status | ||
| measures: [orders.count] | ||
| dimensions: [orders.status] | ||
| ` `` | ||
| <PieChart data={by_status} name="orders.status" value="orders.count" title="Order Status" /> | ||
| ## Top Cities | ||
| ` ``query top_cities | ||
| measures: [orders.total_revenue, orders.count] | ||
| dimensions: [orders.city] | ||
| orderBy: | ||
| orders.total_revenue: desc | ||
| limit: 10 | ||
| ` `` | ||
| <BarChart data={top_cities} x="orders.city" y="orders.total_revenue" horizontal /> | ||
| <DataTable data={top_cities} /> | ||
| ## Completed Orders Over Time | ||
| ` ``query completed_trend | ||
| measures: [orders.total_revenue] | ||
| timeDimension: | ||
| dimension: orders.created_at | ||
| granularity: week | ||
| dateRange: [2025-01-01, 2025-06-30] | ||
| filters: | ||
| - dimension: orders.status | ||
| operator: equals | ||
| values: [completed] | ||
| ` `` | ||
| <AreaChart data={completed_trend} x="orders.created_at" y="orders.total_revenue" title="Completed Order Revenue" /> | ||
| ``` | ||
| ## Multi-Series Dashboard | ||
| When you need to compare segments side-by-side, use the `series` prop to split data by a dimension into colored segments. This example shows stacked bars, grouped bars, multi-line, and stacked area — all from the same data. | ||
| ```markdown | ||
| --- | ||
| title: Revenue by Channel | ||
| description: Multi-series charts showing revenue breakdown by sales channel | ||
| --- | ||
| # Revenue by Channel | ||
| ` ``query revenue_by_channel | ||
| measures: [orders.total_revenue] | ||
| dimensions: [orders.channel] | ||
| timeDimension: | ||
| dimension: orders.created_at | ||
| granularity: month | ||
| dateRange: [2025-01-01, 2025-12-31] | ||
| ` `` | ||
| ## Stacked Bar (default) | ||
| <BarChart data={revenue_by_channel} x="orders.created_at" y="orders.total_revenue" series="orders.channel" title="Revenue by Channel" /> | ||
| ## Grouped Bar | ||
| <BarChart data={revenue_by_channel} x="orders.created_at" y="orders.total_revenue" series="orders.channel" type="grouped" title="Revenue by Channel (Grouped)" /> | ||
| ## Multi-Line | ||
| ` ``query trend | ||
| measures: [orders.total_revenue, orders.count] | ||
| timeDimension: | ||
| dimension: orders.created_at | ||
| granularity: month | ||
| dateRange: [2025-01-01, 2025-12-31] | ||
| ` `` | ||
| <LineChart data={trend} x="orders.created_at" y="orders.total_revenue,orders.count" title="Revenue vs Orders" /> | ||
| ## Stacked Area by Channel | ||
| <AreaChart data={revenue_by_channel} x="orders.created_at" y="orders.total_revenue" series="orders.channel" type="stacked" title="Revenue by Channel" /> | ||
| ``` | ||
| ## Formatted Dashboard | ||
| Use format presets to display currencies, percentages, and number styles consistently across KPIs, charts, and tables. | ||
| ```markdown | ||
| --- | ||
| title: Sales Performance | ||
| description: Formatted revenue metrics and trends | ||
| --- | ||
| # Sales Performance | ||
| ` ``query totals | ||
| measures: [orders.total_revenue, orders.count, orders.avg_order_value] | ||
| ` `` | ||
| <Grid cols="3"> | ||
| <BigValue data={totals} value="orders.total_revenue" title="Revenue" fmt="eur2" /> | ||
| <BigValue data={totals} value="orders.count" title="Orders" fmt="num0" /> | ||
| <BigValue data={totals} value="orders.avg_order_value" title="Avg Order" fmt="eur2" /> | ||
| </Grid> | ||
| ## Revenue Trend | ||
| ` ``query monthly | ||
| measures: [orders.total_revenue] | ||
| timeDimension: | ||
| dimension: orders.created_at | ||
| granularity: month | ||
| dateRange: [2025-01-01, 2025-12-31] | ||
| ` `` | ||
| <LineChart data={monthly} x="orders.created_at" y="orders.total_revenue" title="Monthly Revenue" yFmt="eur" /> | ||
| ## Detail Table | ||
| ` ``query details | ||
| measures: [orders.total_revenue, orders.count] | ||
| dimensions: [orders.category] | ||
| orderBy: | ||
| orders.total_revenue: desc | ||
| ` `` | ||
| <DataTable data={details} fmt="orders.total_revenue:eur2,orders.count:num0" /> | ||
| ``` | ||
| ## Interactive Dashboard | ||
| Combine a DateRange picker and Dropdown filter to let viewers explore the data. Filter state syncs to the URL, so shared links preserve the exact filtered view. | ||
| ```markdown | ||
| --- | ||
| title: Interactive Sales | ||
| description: Sales dashboard with date and channel filters | ||
| --- | ||
| # Interactive Sales | ||
| <DateRange name="period" default="last-6-months" label="Time Period" /> | ||
| <Dropdown name="channel" dimension="orders.channel" data={channels} queries="trend,by_city" label="Channel" /> | ||
| ` ``query channels | ||
| dimensions: [orders.channel] | ||
| ` `` | ||
| ` ``query kpis | ||
| measures: [orders.total_revenue, orders.count] | ||
| ` `` | ||
| <Grid cols="2"> | ||
| <BigValue data={kpis} value="orders.total_revenue" title="Revenue" fmt="eur2" /> | ||
| <BigValue data={kpis} value="orders.count" title="Orders" fmt="num0" /> | ||
| </Grid> | ||
| ## Revenue Trend | ||
| ` ``query trend | ||
| measures: [orders.total_revenue] | ||
| timeDimension: | ||
| dimension: orders.created_at | ||
| granularity: month | ||
| ` `` | ||
| <LineChart data={trend} x="orders.created_at" y="orders.total_revenue" title="Monthly Revenue" yFmt="eur" /> | ||
| ## By City | ||
| ` ``query by_city | ||
| measures: [orders.total_revenue] | ||
| dimensions: [orders.city] | ||
| orderBy: | ||
| orders.total_revenue: desc | ||
| limit: 10 | ||
| ` `` | ||
| <BarChart data={by_city} x="orders.city" y="orders.total_revenue" title="Top Cities" yFmt="eur" /> | ||
| ``` | ||
| The `<DateRange>` automatically applies to all queries with a `timeDimension` (here: `trend`). The `<Dropdown>` filters `trend` and `by_city` by channel. The `channels` query populates the dropdown and is never filtered by it. | ||
| ## Compact Multi-Section Dashboard | ||
| A dashboard with multiple sections, side-by-side charts, and compact layout. Uses `##` headings (not `#`), `<Grid>` for horizontal grouping, and keeps all queries near the components that use them. | ||
| ```markdown | ||
| --- | ||
| title: Operations Overview | ||
| description: KPIs, trends, and breakdowns across channels and cities | ||
| --- | ||
| <DateRange name="period" default="last-30-days" label="Period" /> | ||
| ` ``query channels | ||
| dimensions: [orders.channel] | ||
| ` `` | ||
| <Dropdown name="channel" dimension="orders.channel" data={channels} queries="kpis,trend,by_city" label="Channel" /> | ||
| ## Key Metrics | ||
| ` ``query kpis | ||
| measures: [orders.total_revenue, orders.count, orders.avg_order_value] | ||
| ` `` | ||
| <BigValue data={kpis} value="orders.total_revenue" title="Revenue" fmt="eur" /> | ||
| <BigValue data={kpis} value="orders.count" title="Orders" fmt="num0" /> | ||
| <BigValue data={kpis} value="orders.avg_order_value" title="Avg Order" fmt="eur2" /> | ||
| ## Trends & Breakdown | ||
| ` ``query trend | ||
| measures: [orders.total_revenue] | ||
| timeDimension: | ||
| dimension: orders.created_at | ||
| granularity: week | ||
| ` `` | ||
| ` ``query by_channel | ||
| measures: [orders.total_revenue] | ||
| dimensions: [orders.channel] | ||
| orderBy: | ||
| orders.total_revenue: desc | ||
| ` `` | ||
| <Grid cols="2"> | ||
| <LineChart data={trend} x="orders.created_at" y="orders.total_revenue" title="Weekly Revenue" yFmt="eur" /> | ||
| <BarChart data={by_channel} x="orders.channel" y="orders.total_revenue" title="By Channel" yFmt="eur" /> | ||
| </Grid> | ||
| ## Top Cities | ||
| ` ``query by_city | ||
| measures: [orders.total_revenue, orders.count] | ||
| dimensions: [orders.city] | ||
| orderBy: | ||
| orders.total_revenue: desc | ||
| limit: 10 | ||
| ` `` | ||
| <Grid cols="2"> | ||
| <BarChart data={by_city} x="orders.city" y="orders.total_revenue" title="Revenue by City" horizontal yFmt="eur" /> | ||
| <DataTable data={by_city} fmt="orders.total_revenue:eur,orders.count:num0" /> | ||
| </Grid> | ||
| ``` | ||
| Key patterns: | ||
| - **`##` headings** for sections — compact, no oversized H1s | ||
| - **Consecutive `<BigValue>`** auto-groups into a row (no Grid needed) | ||
| - **`<Grid cols="2">`** pairs a chart with a table or two charts side by side | ||
| - **Queries defined before their Grid** — keeps the layout clean and components grouped | ||
| ## Tips | ||
| - **Start with KPIs**: Use `BigValue` at the top for key metrics — consecutive BigValues auto-group into a row | ||
| - **One query per chart**: Each component gets its own query — keep them focused | ||
| - **Use `##` headings**: Reserve `#` for the dashboard title (in frontmatter). Use `##` for sections | ||
| - **Use views**: Prefer view names over cube names when available | ||
| - **Name queries descriptively**: `monthly_revenue` is better than `q1` | ||
| - **Limit large datasets**: Add `limit` to dimension queries to avoid oversized charts | ||
| - **Time series**: Always use `timeDimension` with `granularity` for time-based charts | ||
| - **Multi-series**: Use `series="cube.column"` to split data by a dimension. For bars, default is stacked; use `type="grouped"` for side-by-side | ||
| - **Multiple y columns**: Use comma-separated values like `y="orders.revenue,orders.cases"` to show multiple measures on one chart | ||
| - **Side-by-side charts**: Wrap pairs in `<Grid cols="2">` to reduce vertical scrolling | ||
| ## See Also | ||
| - [Dashboards](dashboards) — overview and deployment | ||
| - [Queries](dashboards.queries) — query syntax and properties | ||
| - [Components](dashboards.components) — chart and display components |
| # Inputs | ||
| > Interactive filter inputs for dashboards — date range pickers and dropdown selectors. | ||
| ## Overview | ||
| Inputs add interactivity to dashboards. They render as a filter bar above the charts and re-execute queries when values change. Inspired by Evidence.dev's inputs pattern, adapted for the Cube semantic layer. | ||
| Two input types are available: | ||
| - **DateRange** — preset date range picker that overrides `timeDimension.dateRange` | ||
| - **Dropdown** — dimension value selector that adds/replaces filters | ||
| ## DateRange | ||
| Renders a date range preset picker. When changed, overrides `timeDimension.dateRange` on targeted queries. | ||
| ```markdown | ||
| <DateRange name="period" default="last-6-months" label="Time Period" /> | ||
| ``` | ||
| ### Props | ||
| | Prop | Type | Required | Description | | ||
| |------|------|----------|-------------| | ||
| | `name` | string | Yes | Unique input name | | ||
| | `default` | preset key | No | Initial preset (default: `last-6-months`) | | ||
| | `label` | string | No | Label shown above the picker | | ||
| | `queries` | string | No | Comma-separated query names to target | | ||
| ### Targeting | ||
| Targeting lets you control which queries a filter affects — useful when some charts should stay fixed while others respond to filter changes. | ||
| - **No `queries` prop** — applies to ALL queries that have a `timeDimension` | ||
| - **With `queries` prop** — only applies to the listed queries | ||
| ### Presets | ||
| | Key | Label | | ||
| |-----|-------| | ||
| | `last-7-days` | Last 7 Days | | ||
| | `last-30-days` | Last 30 Days | | ||
| | `last-3-months` | Last 3 Months | | ||
| | `last-6-months` | Last 6 Months | | ||
| | `last-12-months` | Last 12 Months | | ||
| | `month-to-date` | Month to Date | | ||
| | `year-to-date` | Year to Date | | ||
| | `last-year` | Last Year | | ||
| | `all-time` | All Time | | ||
| ## Dropdown | ||
| Renders a dropdown selector populated from a query's dimension values. Adds a filter on the specified dimension to targeted queries. | ||
| ```markdown | ||
| <Dropdown name="channel" dimension="orders.channel" data={channels} queries="main,trend" label="Channel" /> | ||
| ``` | ||
| ### Props | ||
| | Prop | Type | Required | Description | | ||
| |------|------|----------|-------------| | ||
| | `name` | string | Yes | Unique input name | | ||
| | `dimension` | string | Yes | Dimension to filter on | | ||
| | `data` | query ref | Yes | Query that provides the dropdown options | | ||
| | `queries` | string | Yes | Comma-separated query names to filter | | ||
| | `label` | string | No | Label shown above the dropdown | | ||
| | `default` | string | No | Initial selected value (default: All) | | ||
| ### Behavior | ||
| - Always includes an "All" option that removes the filter | ||
| - The dropdown's own `data` query is never filtered by itself (prevents circular dependencies) | ||
| - `queries` is required — the dropdown only filters explicitly listed queries | ||
| ## Examples | ||
| ### DateRange only | ||
| ```markdown | ||
| --- | ||
| title: Revenue Trends | ||
| --- | ||
| <DateRange name="period" default="last-6-months" label="Time Period" /> | ||
| ` ``query monthly_revenue | ||
| measures: [orders.total_revenue] | ||
| timeDimension: | ||
| dimension: orders.created_at | ||
| granularity: month | ||
| ` `` | ||
| <LineChart data={monthly_revenue} x="orders.created_at" y="orders.total_revenue" /> | ||
| ``` | ||
| The DateRange automatically applies to `monthly_revenue` because it has a `timeDimension`. No hardcoded `dateRange` needed in the query. | ||
| ### Dropdown with query binding | ||
| ```markdown | ||
| --- | ||
| title: Sales by Channel | ||
| --- | ||
| ` ``query channels | ||
| dimensions: [orders.channel] | ||
| ` `` | ||
| <Dropdown name="ch" dimension="orders.channel" data={channels} queries="main" label="Channel" /> | ||
| ` ``query main | ||
| measures: [orders.total_revenue] | ||
| dimensions: [orders.city] | ||
| ` `` | ||
| <BarChart data={main} x="orders.city" y="orders.total_revenue" /> | ||
| ``` | ||
| ### Combined inputs | ||
| ```markdown | ||
| --- | ||
| title: Sales Dashboard | ||
| --- | ||
| <DateRange name="period" default="last-6-months" label="Time Period" /> | ||
| <Dropdown name="channel" dimension="orders.channel" data={channels} queries="trend,by_city" label="Channel" /> | ||
| ` ``query channels | ||
| dimensions: [orders.channel] | ||
| ` `` | ||
| ` ``query trend | ||
| measures: [orders.total_revenue] | ||
| timeDimension: | ||
| dimension: orders.created_at | ||
| granularity: month | ||
| ` `` | ||
| <LineChart data={trend} x="orders.created_at" y="orders.total_revenue" /> | ||
| ` ``query by_city | ||
| measures: [orders.total_revenue] | ||
| dimensions: [orders.city] | ||
| ` `` | ||
| <BarChart data={by_city} x="orders.city" y="orders.total_revenue" /> | ||
| ``` | ||
| Both inputs work together: the DateRange scopes the time window on `trend` (which has a timeDimension), and the Dropdown filters both `trend` and `by_city` by channel. | ||
| ## Shareable URLs | ||
| Input values automatically sync to URL query params. When a user changes a filter, the URL updates to reflect the current state — for example: | ||
| ``` | ||
| /dashboards/sales?period=last-30-days&channel=Online | ||
| ``` | ||
| - **Default values are omitted** from the URL for clean links | ||
| - **Sharing a URL** preserves the current filter state — recipients see exactly the same filtered view | ||
| - **Refreshing the page** restores the active filters from the URL | ||
| - The **Copy Link** button in the dashboard header copies the full URL including filter params | ||
| DateRange inputs store the preset key (e.g. `last-30-days`), so shared URLs stay relative to today. Dropdown inputs store the selected value string. | ||
| ## See Also | ||
| - [Dashboards](dashboards) — overview and deployment | ||
| - [Components](dashboards.components) — chart and display components | ||
| - [Examples](dashboards.examples) — complete dashboard examples |
| # Dashboards | ||
| > Build interactive dashboards from markdown with embedded semantic layer queries. | ||
| ## Overview | ||
| Dashboards let your team track key metrics without leaving the Bonnard app. Define queries once in markdown, deploy them, and every viewer gets live, governed data — no separate BI tool needed. Filters, formatting, and layout are all declared in the same file. | ||
| A dashboard is a markdown file with YAML frontmatter, query blocks, and chart components. Write it as a `.md` file, deploy with `bon dashboard deploy`, and view it in the Bonnard web app. | ||
| ## Format | ||
| A dashboard file has three parts: | ||
| 1. **Frontmatter** — YAML metadata between `---` delimiters | ||
| 2. **Query blocks** — Named data queries in ` ```query ` code fences | ||
| 3. **Content** — Markdown text and chart component tags | ||
| ## Minimal Example | ||
| ```markdown | ||
| --- | ||
| title: Order Summary | ||
| description: Key metrics for the orders pipeline | ||
| --- | ||
| # Order Summary | ||
| ` ``query order_count | ||
| measures: [orders.count] | ||
| ` `` | ||
| <BigValue data={order_count} value="orders.count" title="Total Orders" /> | ||
| ` ``query by_status | ||
| measures: [orders.count] | ||
| dimensions: [orders.status] | ||
| ` `` | ||
| <BarChart data={by_status} x="orders.status" y="orders.count" /> | ||
| ``` | ||
| ## Frontmatter | ||
| The YAML frontmatter is required and must include `title`: | ||
| ```yaml | ||
| --- | ||
| title: Revenue Dashboard # Required | ||
| description: Monthly trends # Optional | ||
| slug: revenue-dashboard # Optional (derived from title if omitted) | ||
| --- | ||
| ``` | ||
| | Field | Required | Description | | ||
| |-------|----------|-------------| | ||
| | `title` | Yes | Dashboard title displayed in the viewer and listings | | ||
| | `description` | No | Short description shown in dashboard listings | | ||
| | `slug` | No | URL-safe identifier. Auto-derived from title if omitted | | ||
| ## Deployment | ||
| Deploy from the command line or via MCP tools. Each deploy auto-versions the dashboard so you can roll back if needed. | ||
| ```bash | ||
| # Deploy a single dashboard | ||
| bon dashboard deploy revenue.md | ||
| # Deploy all dashboards in a directory | ||
| bon dashboard deploy dashboards/ | ||
| # List deployed dashboards | ||
| bon dashboard list | ||
| # Remove a dashboard | ||
| bon dashboard remove revenue-dashboard | ||
| ``` | ||
| Via MCP tools, agents can use `deploy_dashboard` with the markdown content as a string. | ||
| ## Versioning | ||
| Every deployment auto-increments the version number and saves a snapshot. You can view version history and restore previous versions: | ||
| ```bash | ||
| # Via MCP tools: | ||
| # get_dashboard with version parameter to fetch a specific version | ||
| # deploy_dashboard with slug + restore_version to roll back | ||
| ``` | ||
| Restoring a version creates a new version (e.g. restoring v2 from v5 creates v6 with v2's content). Version history is never deleted — only `remove_dashboard` deletes all history. | ||
| ## Sharing | ||
| Dashboard viewers include a **Share** menu in the header with: | ||
| - **Copy link** — copies the current URL including any active filter state | ||
| - **Print to PDF** — opens the browser print dialog for PDF export | ||
| Filter state (DateRange presets, Dropdown selections) is encoded in URL query params, so shared links preserve the exact filtered view the sender was looking at. | ||
| ## Governance | ||
| Dashboard queries respect the same governance policies as all other queries. When a user views a dashboard: | ||
| - **View-level access** — users only see data from views their governance groups allow | ||
| - **Row-level filtering** — user attributes (e.g. region, department) automatically filter query results | ||
| - All org members see the same dashboard list, but may see different data depending on their governance context | ||
| ## See Also | ||
| - [Queries](dashboards.queries) — query syntax and properties | ||
| - [Components](dashboards.components) — chart and display components | ||
| - [Inputs](dashboards.inputs) — interactive filters | ||
| - [Examples](dashboards.examples) — complete dashboard examples |
| # Queries | ||
| > Define data queries in dashboard markdown using YAML code fences. | ||
| ## Overview | ||
| Each query fetches data from your semantic layer and makes it available to chart components. Queries use the same measures and dimensions defined in your cubes and views — field names stay consistent whether you're querying from a dashboard, MCP, or the API. | ||
| Query blocks have a unique name and map to a `QueryOptions` shape. Components reference them using `data={query_name}`. All field names must be fully qualified with the cube or view name (e.g. `orders.count`, `orders.created_at`). | ||
| ## Syntax | ||
| Query blocks use fenced code with the `query` language tag followed by a name: | ||
| ````markdown | ||
| ```query revenue_trend | ||
| measures: [orders.total_revenue] | ||
| timeDimension: | ||
| dimension: orders.created_at | ||
| granularity: month | ||
| dateRange: [2025-01-01, 2025-12-31] | ||
| ``` | ||
| ```` | ||
| ## Query Properties | ||
| | Property | Type | Required | Description | | ||
| |----------|------|----------|-------------| | ||
| | `measures` | string[] | No | Fully qualified measures to aggregate (e.g. `[orders.count, orders.total_revenue]`) | | ||
| | `dimensions` | string[] | No | Fully qualified dimensions to group by (e.g. `[orders.status, orders.city]`) | | ||
| | `filters` | Filter[] | No | Row-level filters | | ||
| | `timeDimension` | object | No | Time-based grouping and date range | | ||
| | `orderBy` | object | No | Sort specification (e.g. `{orders.total_revenue: desc}`) | | ||
| | `limit` | number | No | Maximum rows to return | | ||
| ### timeDimension | ||
| | Property | Type | Required | Description | | ||
| |----------|------|----------|-------------| | ||
| | `dimension` | string | Yes | Fully qualified time dimension name (e.g. `orders.created_at`) | | ||
| | `granularity` | string | No | `day`, `week`, `month`, `quarter`, or `year` | | ||
| | `dateRange` | string[] | No | `[start, end]` in `YYYY-MM-DD` format | | ||
| ### filters | ||
| Each filter is an object with: | ||
| | Property | Type | Description | | ||
| |----------|------|-------------| | ||
| | `dimension` | string | Dimension to filter on | | ||
| | `operator` | string | `equals`, `notEquals`, `contains`, `gt`, `gte`, `lt`, `lte` | | ||
| | `values` | array | Values to filter by | | ||
| ## Examples | ||
| ### Simple aggregation | ||
| ````markdown | ||
| ```query total_orders | ||
| measures: [orders.count] | ||
| ``` | ||
| ```` | ||
| ### Grouped by dimension | ||
| ````markdown | ||
| ```query revenue_by_city | ||
| measures: [orders.total_revenue] | ||
| dimensions: [orders.city] | ||
| orderBy: | ||
| orders.total_revenue: desc | ||
| limit: 10 | ||
| ``` | ||
| ```` | ||
| ### Time series | ||
| ````markdown | ||
| ```query monthly_revenue | ||
| measures: [orders.total_revenue] | ||
| timeDimension: | ||
| dimension: orders.created_at | ||
| granularity: month | ||
| dateRange: [2025-01-01, 2025-12-31] | ||
| ``` | ||
| ```` | ||
| ### With filters | ||
| ````markdown | ||
| ```query completed_orders | ||
| measures: [orders.count, orders.total_revenue] | ||
| dimensions: [orders.category] | ||
| filters: | ||
| - dimension: orders.status | ||
| operator: equals | ||
| values: [completed] | ||
| ``` | ||
| ```` | ||
| ## Rules | ||
| - Query names must be valid identifiers (letters, numbers, `_`, `$`) | ||
| - Query names must be unique within a dashboard | ||
| - All field names must be fully qualified with the cube or view name (e.g. `orders.count`, not `count`) | ||
| - Components reference queries by name: `data={query_name}` | ||
| ## See Also | ||
| - [Components](dashboards.components) — chart and display components | ||
| - [Dashboards](dashboards) — overview and deployment | ||
| - [Querying](querying) — query format reference |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
441274
6.6%75
5.63%4818
3.06%9
50%