Building with an AI agent? Give it the Markdown reference: this guide in full detail, with the ECharts option for each chart type.
1. Lay out the grid
Dashboards sit on a grid of dashboard.columns (12) columns. Each widget's layout gives its position and size in cells. VideoBI uses rows 56 px tall with a 12 px gap, so a widget h rows tall is h × 56 + (h − 1) × 12 px. On phones, stack the widgets in order of y, then x.
container.style.cssText = "display:grid;grid-template-columns:repeat(12,1fr);grid-auto-rows:56px;gap:12px";
el.style.gridColumn = `${x + 1} / span ${w}`;
el.style.gridRow = `${y + 1} / span ${h}`;2. Read the rows
A metric widget's data is a small table: columns (name and type) and rows, each an array in column order. Read cells by column name.
const col = Object.fromEntries(data.columns.map((c, i) => [c.name, i]));
const value = row[col.value];| Column | Meaning |
|---|---|
bucket | Start of the time bucket (hour, day, week…), as an ISO instant. One bucket for grain period. |
location_name, zone_name, zone_role, hour_of_day, day_of_week, counter_config | The dimensions from query.group_by, in order. Show names, never the _id columns. day_of_week 0 is Sunday; hour_of_day is local, 0–23. |
value | The metric. null means no data: draw a gap, never zero. |
coverage_pct | How much of the time the cameras could see, 0–1. Below 0.9, say the number is less certain. |
compare_value, delta | Only when query.compare_to is set: the prior period's (or last year's) value and the change. |
3. Format numbers
Use display.format, falling back to metric.format. Ratios arrive as 0–1.
| Format | Value | Shown as |
|---|---|---|
percent | 0.873 | 87.3% (changes in points: +2.1 pts) |
integer | 1284.4 | 1,284 |
clock | 214 (seconds) | 03:34 |
duration | 95 | 1:35 |
currency | 1840.5 | $1.8K (under $1,000: $184 or $18.40) |
decimal1, decimal2 | 4.256 | 4.3, 4.26 |
score100 | 82.6 | 83 |
4. Pick categories and series
- Over time (grain
hour,day,week…): the x axis is the buckets, in order. Each combination of dimensions is one series, named likeDowntown · Drive-thru. - One period (grain
period): the x axis is the first dimension (hours or weekdays first, in their natural order); any other dimensions are series. - Use a category axis, not a time axis. Don't stack series. Charts don't draw
compare_value: comparisons belong in big numbers and tables.
5. Draw each widget
viz | Draw it as |
|---|---|
line, area | ECharts line series, no smoothing, gaps for nulls; area adds a light fill. display.target is a dashed markLine. |
bar | ECharts bar, side by side. Ranked bars (one series of locations or zones) go horizontal: grey bars, the worst one in the accent color, with target and average lines. |
heatmap | ECharts heatmap with a visualMap from light grey to blue. |
stat | A big number: the rows collapsed with display.aggregate, the change against compare_value (▲ or ▼, green when good for metric.direction), and a Watch or Act chip from the thresholds. |
gauge | A half-circle (ECharts gauge, 180° to 0°) filled in the threshold color. |
table, pivot, list | HTML tables or lists of the rows, values colored by threshold. |
image_carousel, clip_carousel, snapshot | Images and clips from data.assets or data.snapshot. Links work for 10 minutes; fetch again for fresh ones. |
Others (brief, scorecard, live boards) | Their own data key instead of rows: show text or a table, or link to VideoBI. |
A widget whose data is {"error": {"message"}} shows that message in its frame; the rest of the dashboard draws normally.
A line chart, as VideoBI draws it
chart.setOption({
color: ["#3568C4", "#E08A2E", "#3FA37A", "#B0578F", "#5C6E8E", "#D9B43C", "#4CA6C6", "#A8624B"],
tooltip: { trigger: "axis", valueFormatter: (v) => formatValue(v, format) },
grid: { left: 4, right: 20, top: 12, bottom: 30, containLabel: true },
legend: series.length > 1 ? { bottom: 0 } : undefined,
xAxis: { type: "category", data: labels, boundaryGap: false },
yAxis: { type: "value", axisLabel: { formatter: (v) => formatValue(v, format) } },
series: series.map((s, i) => ({
name: s.name, type: "line", data: s.values, connectNulls: false, showSymbol: false,
markLine: target != null && i === 0
? { symbol: "none", data: [{ yAxis: target, lineStyle: { type: "dashed" } }] } : undefined,
})),
}, { notMerge: true });6. Thresholds and colors
display.thresholds is a list like [{"lt": 0.8, "level": "act"}, {"lt": 0.9, "level": "watch"}], checked in order; the first match wins, and no match means ok. Whether a change is good depends on metric.direction (higher_is_better, lower_is_better or neutral).
| Level | Text | Fill |
|---|---|---|
| ok | #0F6B3A | #DCF3E6 |
| watch | #7A5200 | #FDEFC7 |
| act | #A11B1B | #FBDCDC |
Always pair a color with words ("Act", "▲ 2.1 pts", "Target 80%"), so the dashboard reads without color.
Next
The Markdown reference has the full rules: every display hint, the ranked-bar and heatmap options, stats, tables, media and a minimal renderer. The API reference covers fetching.