Chart & Table Response Spec
This document is the single source of truth for how the backend returns widget data. It covers every chart type, every chartConfig shape, the rows format, table column definitions, and how responses differ across endpoints.
Widget Types
Every widget has a type field:
| type | description |
|---|---|
chart | Visual chart — rendered using chartType and chartConfig |
table | Tabular data — rendered using columns |
Chart Types
When type is "chart", the chartType field determines the visualization:
| chartType | Use Case | Max Rows |
|---|---|---|
bar | Comparisons between categories (e.g., consumption by section, TC count by zone) | 10 |
line | Trends over time (e.g., monthly consumption, collection trends) | 10 |
pie | Composition/proportion (e.g., tariff distribution, billing status breakdown) | 10 |
donut | Same as pie, with a hollow center | 10 |
stackedBar | Category comparisons broken down by a sub-dimension (e.g., consumption by section stacked by tariff type) | 10 |
scatterPlot | Correlation between two numeric variables (e.g., installed capacity vs consumption) | 50 |
When type is "table", chartType is null.
chartConfig Shapes
The chartConfig object tells the frontend how to map rows data to visual elements. Its shape varies by chartType. Every field value exactly matches a column alias key in each rows[] object. Every label value is a human-readable display name for the UI.
1. Bar / Line — Single Metric
Two axes, one metric per data point.
{
"xAxis": { "field": "Zone", "label": "Zone" },
"yAxis": { "field": "TC Count", "label": "TC Count" }
}
How to detect: chartConfig.yAxis exists and chartConfig.series does not exist.
Rendering: Each row produces one data point. Read row[xAxis.field] for the X position, row[yAxis.field] for the Y value.
Example rows:
[
{ "Zone": "Pune", "TC Count": 142 },
{ "Zone": "Nashik", "TC Count": 98 },
{ "Zone": "Nagpur", "TC Count": 67 }
]
2. Bar / Line — Multiple Metrics (Series)
One X axis, multiple named Y values per data point. Used when comparing two or more metrics on the same axis (e.g., Section 195 loss % vs MSEDCL average loss %).
{
"xAxis": { "field": "Month", "label": "Audit Month" },
"series": [
{ "field": "Section 195 Loss", "label": "Section 195 Loss (%)" },
{ "field": "MSEDCL Avg Loss", "label": "MSEDCL Avg Loss (%)" }
],
"yAxisLabel": "Loss (%)"
}
How to detect: chartConfig.series exists (array of { field, label }).
Rendering: Each row produces one X position with multiple Y values. Read row[xAxis.field] for the X position, then row[series[i].field] for each series value. Each series entry becomes a separate line/bar with its own legend entry using series[i].label. Use yAxisLabel as the Y-axis title — it describes the shared unit across all series (e.g., "Loss (%)", "Consumption (kWh)"). All series in a multi-metric chart share the same unit, so a single axis title applies.
Example rows:
[
{ "Month": "2509", "Section 195 Loss": 4.2, "MSEDCL Avg Loss": 6.1 },
{ "Month": "2510", "Section 195 Loss": 3.8, "MSEDCL Avg Loss": 5.9 },
{ "Month": "2511", "Section 195 Loss": 4.0, "MSEDCL Avg Loss": 6.0 }
]
3. Stacked Bar
Category comparisons with a sub-dimension breakdown. Each bar is split into segments by the stackField.
{
"xAxis": { "field": "Section", "label": "Section" },
"yAxis": { "field": "Consumption", "label": "Consumption (kWh)" },
"stackField": { "field": "Tariff Type", "label": "Tariff Type" }
}
How to detect: chartType is "stackedBar". chartConfig.stackField is always present (backend enforces this — retries if AI omits it).
Rendering: Group rows by xAxis.field. Within each group, stackField.field determines the segment and yAxis.field determines the segment size. Each unique stackField value becomes a legend entry.
Example rows:
[
{ "Section": "Pune Urban", "Tariff Type": "HT Industrial", "Consumption": 45000 },
{ "Section": "Pune Urban", "Tariff Type": "LT Commercial", "Consumption": 32000 },
{ "Section": "Pune Urban", "Tariff Type": "LT Domestic", "Consumption": 28000 },
{ "Section": "Nashik Rural", "Tariff Type": "HT Industrial", "Consumption": 12000 },
{ "Section": "Nashik Rural", "Tariff Type": "LT Commercial", "Consumption": 8500 },
{ "Section": "Nashik Rural", "Tariff Type": "LT Domestic", "Consumption": 15000 }
]
4. Pie / Donut
Composition charts. Each row is one slice.
{
"labelField": { "field": "Tariff", "label": "Tariff Category" },
"valueField": { "field": "Count", "label": "Consumer Count" }
}
How to detect: chartConfig.labelField exists.
Rendering: Each row is one slice. Read row[labelField.field] for the slice label and row[valueField.field] for the slice value. Best with 6-8 slices maximum (AI is instructed to group smaller values into "Others" beyond that).
Example rows:
[
{ "Tariff": "HT Industrial", "Count": 85 },
{ "Tariff": "LT Commercial", "Count": 142 },
{ "Tariff": "LT Domestic", "Count": 310 },
{ "Tariff": "Agricultural", "Count": 67 },
{ "Tariff": "Others", "Count": 23 }
]
5. Scatter Plot
Correlation between two numeric variables. Uses the same shape as single-metric bar/line.
{
"xAxis": { "field": "Installed Capacity", "label": "Installed Capacity (kVA)" },
"yAxis": { "field": "Consumption", "label": "Consumption (kWh)" }
}
How to detect: chartType is "scatterPlot".
Rendering: Each row is one point. Read row[xAxis.field] for X, row[yAxis.field] for Y. Both values are numeric. Can have up to 50 data points (unlike other charts which cap at 10).
Example rows:
[
{ "Installed Capacity": 100, "Consumption": 45000 },
{ "Installed Capacity": 250, "Consumption": 98000 },
{ "Installed Capacity": 50, "Consumption": 18000 }
]
chartConfig Detection Logic (Frontend)
Use this decision tree to determine how to render a chart:
if chartType == "pie" or "donut":
use labelField + valueField
else if chartType == "stackedBar":
use xAxis + yAxis + stackField
else if chartConfig.series exists:
use xAxis + series (multi-metric)
else:
use xAxis + yAxis (single metric)
Table Response
When type is "table", chartType and chartConfig are both null. The widget uses columns instead.
columns
An ordered array of column definitions. The array order determines the display order left-to-right.
[
{ "field": "Section Code", "label": "Section" },
{ "field": "Consumer Count", "label": "No. of Consumers" },
{ "field": "Total Consumption", "label": "Consumption (kWh)" }
]
field— key to read from eachrows[]object (matches SQL column alias exactly)label— human-readable column header for the UI
rows (tables)
Same structure as chart rows — an array of objects where keys match the columns[].field values.
[
{ "Section Code": "PUNE001", "Consumer Count": 4520, "Total Consumption": 890000 },
{ "Section Code": "NASH003", "Consumer Count": 2310, "Total Consumption": 456000 }
]
Pagination
Tables support server-side pagination:
totalCount— total number of matching rows in DuckDB (not the length ofrows)pageandperPagequery params control the page (perPage enum: 10, 25, 50 — see getWidgetData endpoint)pageData—{ current, last, perPage, allowedPerPage }on table responses,nullon chart responses- The batch endpoint (getWidgetsData) returns a fixed 10-row preview per table widget
rows Format (General)
Across all widget types:
rowsis an array of plain objects- Keys are the SQL column aliases chosen by the AI (human-readable strings like
"Consumption (kWh)", not raw DB column names) - Values are the native DuckDB types coerced to JSON: strings, numbers, booleans, or null
- BigInt values are cast to INTEGER in SQL to avoid JSON serialization issues
- The
fieldvalues inchartConfig(orcolumnsfor tables) always match keys inrows[]exactly — use these to look up values
Related LLD Docs
- Start Conversation —
POST /v1/conversations - Send Message —
POST /v1/conversations/:conversationId/messages - Get Widget Data —
GET /v1/widgets/:widgetId/data - Get Widgets Data (Batch) —
GET /v1/widgets/data?canvasId=...
Response Shapes by Endpoint
POST /v1/conversations (startConversation)
POST /v1/conversations/:conversationId/messages (sendMessage)
Both conversation endpoints return the same response shape:
{
"success": true,
"message": null,
"data": {
"type": "clarification" | "chart" | "table",
"conversationId": "<string>",
"message": "<string>",
// Present only when type is "clarification"
// The AI's follow-up question
"widget": {
"id": "<string>",
"title": "<string>",
"type": "chart" | "table",
"chartType": "line" | "bar" | "pie" | "donut" | "stackedBar" | "scatterPlot" | null,
"chartConfig": { ... } | null,
"columns": [{ "field": "<string>", "label": "<string>" }] | null,
"position": 0
},
// Present only when type is "chart" or "table"
// null when type is "clarification"
"rows": [{ ... }],
// Present only when type is "chart" or "table"
"totalCount": 42
// Present only when type is "table"
}
}
Frontend logic:
- Check
data.type - If
"clarification"— showdata.messagein the chat UI, prompt user for follow-up - If
"chart"— render chart usingdata.widget.chartType,data.widget.chartConfig, anddata.rows - If
"table"— render table usingdata.widget.columnsanddata.rows, usedata.totalCountfor pagination
GET /v1/widgets/:widgetId/data (getWidgetData)
Single widget data refresh. Used when the user opens/refreshes a specific widget.
{
"success": true,
"message": null,
"data": {
"widgetId": "<string>",
"canvasId": "<string>",
"conversationId": "<string>" | null,
"title": "<string>",
"type": "chart" | "table",
"chartType": "line" | "bar" | "pie" | "donut" | "stackedBar" | "scatterPlot" | null,
"chartConfig": { ... } | null,
"columns": [{ "field": "<string>", "label": "<string>" }] | null,
"position": 0,
"rows": [{ ... }],
"totalCount": 42,
"sql": "SELECT ..."
// sql is only present for SUPER_ADMIN users
}
}
Query params (tables only):
limit— integer, 1-500, default 50offset— integer, min 0, default 0
GET /v1/widgets/data?canvasId=... (getWidgetsData)
Batch endpoint — fetches data for all widgets in a canvas. Used when loading a full canvas view.
{
"success": true,
"message": null,
"data": {
"canvasId": "<string>",
"widgets": [
{
"widgetId": "<string>",
"conversationId": "<string>" | null,
"title": "<string>",
"type": "chart" | "table",
"chartType": "line" | "bar" | "pie" | "donut" | "stackedBar" | "scatterPlot" | null,
"chartConfig": { ... } | null,
"columns": [{ "field": "<string>", "label": "<string>" }] | null,
"position": 0,
"success": true,
"rows": [{ ... }],
"totalCount": 42,
"sql": "SELECT ..."
// sql is only present for SUPER_ADMIN users
},
{
"widgetId": "<string>",
"conversationId": "<string>" | null,
"title": "<string>",
"type": "chart",
"chartType": "bar",
"chartConfig": { ... },
"columns": null,
"position": 1,
"success": false,
"error": "Unable to load this widget's data"
// rows and totalCount are absent when success is false
}
]
}
}
Key differences from single widget endpoint:
- Table widgets get a fixed 10-row preview (no pagination params)
- Per-widget failures are returned inline with
success: falseanderror— the overall HTTP response is still 200 - Widgets are ordered by
positionascending
Null/Absent Field Rules
| Field | When null | When absent |
|---|---|---|
chartType | type is "table" | Never absent on widget objects |
chartConfig | type is "table" | Never absent on widget objects |
columns | type is "chart" | May be absent (treated as null) |
totalCount | Never null when present | Absent when type is "chart" (conversation endpoints) |
widget (conversation endpoints) | type is "clarification" | Never absent |
message (conversation endpoints) | type is "chart" or "table" | Never absent |
sql (data endpoints) | Never null when present | Absent for non-SUPER_ADMIN users |
rows | Never null when present | Absent when widget success is false (batch endpoint) |
Empty States
rowscan be an empty array[]— this means the SQL executed successfully but returned no matching data. Frontend should show an empty state (e.g., "No data available for this query")data.widgetscan be an empty array[](batch endpoint) — the canvas has no widgets yet
Explanation Fields
All widget endpoints (listWidgets, getWidgetData, getWidgetsData) include two explanation fields on every widget object:
latestExplanation(string, nullable): plain-language description of the chart, up to 5000 chars. Displayed in an "Explain" popup.latestExplanationGeneratedAt(ISO timestamp, nullable): when the explanation was generated.
Both are populated for type: "chart" widgets only; always null for type: "table".
Version Note
The widget model has a schemaVersion field (currently 0.1). This is not exposed in API responses but is stored internally. If the chartConfig shape changes in the future, schemaVersion will be incremented and documented here.