Skip to main content

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:

typedescription
chartVisual chart — rendered using chartType and chartConfig
tableTabular data — rendered using columns

Chart Types

When type is "chart", the chartType field determines the visualization:

chartTypeUse CaseMax Rows
barComparisons between categories (e.g., consumption by section, TC count by zone)10
lineTrends over time (e.g., monthly consumption, collection trends)10
pieComposition/proportion (e.g., tariff distribution, billing status breakdown)10
donutSame as pie, with a hollow center10
stackedBarCategory comparisons broken down by a sub-dimension (e.g., consumption by section stacked by tariff type)10
scatterPlotCorrelation 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 each rows[] 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 of rows)
  • page and perPage query params control the page (perPage enum: 10, 25, 50 — see getWidgetData endpoint)
  • pageData{ current, last, perPage, allowedPerPage } on table responses, null on chart responses
  • The batch endpoint (getWidgetsData) returns a fixed 10-row preview per table widget

rows Format (General)

Across all widget types:

  • rows is 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 field values in chartConfig (or columns for tables) always match keys in rows[] exactly — use these to look up values

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:

  1. Check data.type
  2. If "clarification" — show data.message in the chat UI, prompt user for follow-up
  3. If "chart" — render chart using data.widget.chartType, data.widget.chartConfig, and data.rows
  4. If "table" — render table using data.widget.columns and data.rows, use data.totalCount for 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 50
  • offset — 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: false and error — the overall HTTP response is still 200
  • Widgets are ordered by position ascending

Null/Absent Field Rules

FieldWhen nullWhen absent
chartTypetype is "table"Never absent on widget objects
chartConfigtype is "table"Never absent on widget objects
columnstype is "chart"May be absent (treated as null)
totalCountNever null when presentAbsent 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 presentAbsent for non-SUPER_ADMIN users
rowsNever null when presentAbsent when widget success is false (batch endpoint)

Empty States

  • rows can 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.widgets can 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.