VideoBI / Developers / Rendering with ECharts

Rendering with Apache ECharts

VideoBI draws its own dashboards with Apache ECharts. Follow the same rules and your pages will read like ours: the same numbers, formats, colors and warnings.

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];
ColumnMeaning
bucketStart 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_configThe 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.
valueThe metric. null means no data: draw a gap, never zero.
coverage_pctHow much of the time the cameras could see, 0–1. Below 0.9, say the number is less certain.
compare_value, deltaOnly 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.

FormatValueShown as
percent0.87387.3% (changes in points: +2.1 pts)
integer1284.41,284
clock214 (seconds)03:34
duration951:35
currency1840.5$1.8K (under $1,000: $184 or $18.40)
decimal1, decimal24.2564.3, 4.26
score10082.683

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 like Downtown · 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

vizDraw it as
line, areaECharts line series, no smoothing, gaps for nulls; area adds a light fill. display.target is a dashed markLine.
barECharts 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.
heatmapECharts heatmap with a visualMap from light grey to blue.
statA 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.
gaugeA half-circle (ECharts gauge, 180° to 0°) filled in the threshold color.
table, pivot, listHTML tables or lists of the rows, values colored by threshold.
image_carousel, clip_carousel, snapshotImages 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).

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