RenderChart: drawing from a cached table, and how it reaches the person
RenderChart (Jube.AskJooby/Tools/Charting/RenderChartTool.cs) is the one AskJooby tool that draws a picture. It
takes a data_pointer – never rows the model typed out itself – and produces a chart as an SVG file attached
directly to the turn’s reply. The model never sees the picture it drew: it gets back a one-line confirmation of what
was plotted, and is told to describe it in words rather than attempt to reproduce it. This page covers the tool
itself, the eight chart types it supports, and precisely how a chart gets from a tool call to the person’s screen.
For the backend SVG library that actually draws the picture (Jube.Charting: axes, series, hover, no JavaScript),
see docs/AnalyticalMethodology/Charting.md, which this page does not repeat.
What RenderChart takes, and what it does with it
public async Task<string> RenderChart(
string dataPointer, string chartType, string categoryColumn, string valueColumns,
string? title = null, int? bins = null)
dataPointer– a pointer already minted byRunSqlQuery,GetDemoDataset,CacheAssembledTable, or carried forward from an earlier turn of the same conversation. Seedocs/AskJooby/DataPointerCache.mdfor the full pointer mechanic;RenderChartonly ever reads a cached table throughcache.GetAsync(dataPointer)and never sees the query, specialist, or reasoning that produced it.chartType– one ofcolumn,bar,line,area,pie,donut,scatter,histogram(the exact listAskJoobyChartRenderer.SupportedChartTypesexposes,Jube.Service/AskJoobyCharting/AskJoobyChartRenderer.cs). An unrecognised value is rejected before anything is drawn, naming the real list.categoryColumn– the column labelling each point along the bottom (a day, a rule name, a status). Forscatterthis is the numeric x-axis instead of a label axis. Ignored, and left empty, forhistogram, which has no category axis at all – it bins the raw values of the value column(s) itself.valueColumns– one numeric column, or several comma-separated ones. Several columns overlay several series on one chart (several lines, several histograms side by side) – the tool’s own description gives the example of comparing a long baseline window against a recent one by eye.title– optional, becomes both the chart’s own title and (sanitised: letters, digits,-,_and spaces only, collapsed and trimmed, capped at 60 characters) the base of the exported file name,"chart"falling back when it is blank.bins– histogram only, and optional even there. Left unset,Jube.Charting’s own histogram builder chooses bin edges from the data, which the tool’s description calls “almost always better” than a caller-supplied count – the one case worth overriding it is a reporting threshold that a default binning would straddle and hide.
The eight chart types, and what each actually needs
AskJoobyChartRenderer.Render dispatches on chartType before ever touching an axis:
column(vertical bars) andbar(horizontal) – the two safest defaults for “a count or total per category.”barreads better when category names are long, since the labels run along the axis rather than underneath short bars.lineandarea– for something ordered over time or sequence; both accept severalvalueColumnsas separate series sharing one category axis.pieanddonut– a handful of parts of one whole. Both take only the first name invalueColumns(RenderPiereadsvalueColumns[0]) – a pie chart has exactly one value series by construction, so naming more than one column here draws a chart from only the first and silently ignores the rest.scatter– plots each named value column againstcategoryColumnas the numeric x-axis, so for a scatter chart specificallycategoryColumnmust itself resolve to a numeric column, not a label.histogram– the one chart type that reads the raw values of its value column(s) rather than one point per row, works out its own bin edges (or usesbinsif given), and needs nocategoryColumn. It tolerates far more rows than the other seven: up toMaximumHistogramValues(200,000) raw values, against aMaximumCategoriesceiling of 2,000 rows for every other chart type – a table over that per-type ceiling is refused outright with an instruction to aggregate in SQL or filter down first, rather than drawing an unreadable chart of (for example) raw transactions where a chart of counts per day was wanted.
Every chart type resolves its named columns against the cached table’s real column names (case-insensitively) before drawing anything, and fails with the table’s actual column list if a name does not match – so a chart never silently plots the wrong column or an empty one because of a typo. A value column where every row’s value fails to parse as a number is rejected outright, naming the column, rather than drawing a flat line at zero.
Every chart renders at a fixed 760×420 size, in ThemeMode.Light, non-interactive (Interactive(false)), with
styles inlined onto the SVG itself (ChartStyleMode.Inline) rather than referencing an external stylesheet – this
matters because the SVG is handed to the person as a standalone file, with no guarantee the page it is viewed in
carries the same CSS a live Jube page would.
The model never sees the picture – it only gets told what was drawn
RenderChart’s own description is explicit and repeated: “You do not get the picture back and cannot see it; once
this returns, just say what the chart shows and let it appear on your reply.” The tool’s return value is a one-line
summary – row/column counts and which columns were plotted, or, for a histogram, how many raw values were binned
– followed by an instruction to describe the chart in words and never attempt to reproduce it as SVG, ASCII art, or
chart markup. This is also why the tool’s description tells the model never to read the cached rows first “just in
case”: RenderChart draws straight from the cache, so the picture always matches the data regardless of whether the
model inspected it, and reading the rows first would only cost tokens for no benefit. InspectDataSchema exists for
the one case that still matters – not knowing the column names to pass as categoryColumn/valueColumns.
How the SVG actually reaches the person: the attachment mechanic
A rendered chart is attached to the turn’s reply, and a chart drawn by a spawned Joobrik is not redrawn by the agent that spawned it. The mechanic has four parts, each in a different layer of the stack:
- Drawing produces bytes, not a file reference.
renderer.Render(table, specification)returns the finished SVG as abyte[]– nothing is written to disk by the renderer itself. - The bytes are persisted as a conversation file.
RenderCharthands those bytes toIFileExportClient(exportClient.ExportAsync(svg, safeName, "image/svg+xml")), whose concrete implementation,AskJoobyFileExportClient(Jube.Service/AskJoobyExport/AskJoobyFileExportClient.cs), inserts one row into theAskJoobyFiletable – the raw bytes, the sanitised name, the content type, and theAskJoobyConversationIdit belongs to – and returns that row’s integer id. This is the exact same export pathExportCachedTableToCsvuses for a CSV download; a rendered chart and an exported CSV are both just rows inAskJoobyFile, distinguished only byContentType/Extension. - The file is recorded against the whole turn, not the one tool call.
RenderChartcallsexportSink.Record(fileId, safeName)on theIExportSink(concrete typeExportSink,Jube.AskJooby/Tools/Export/Interfaces/IExportSink.cs) it was constructed with. OneExportSinkinstance is created once per turn, inAskJoobyConversationService, and threaded intoRunContext.ExportSinkfor every agent and every spawned sub-agent (Joobrik) that runs during that turn –ExportSink.Recordis lock-guarded specifically because several agents, including ones running concurrently viaSpawnAgents, can call it during the same turn. This is exactly whyRenderChart’s own description says attachment “is turn-wide, so a chart drawn by a spawned Joobrik reaches them without the agent that spawned it redrawing it” – and why its closing instruction is “if one reports back that it has already drawn something, do not draw it again”: redrawing the same chart would just produce a second, redundant attachment, not a missing one. - Everything recorded on the sink is attached to the saved reply, and the frontend renders
.svginline. Once the turn’s model loop finishes,AskJoobyConversationServicereadsexportSink.Files, turns eachExportedFile(Id, Name)into anAskJoobyMessageAttachmentDto, and both saves them against the assistant message (fileRepository.AttachToMessageAsync) and streams them down as part of that message’s payload. The browser side (AskJoobyWidget.js/AskJooby.js, functionPrependAttachments/AskJoobyWidgetPrependAttachments) checks each attachment’s file name: anything matching/\.svg$/iis fetched fromGET /api/AskJoobyConversation/Files/{id}and rendered inline as an<img>(RenderSvgAttachment); everything else becomes a plain download chip (📎 <name>) linking to the same endpoint. A rendered chart is therefore a.svg-namedAskJoobyFilerow that happens to render inline because of its extension, not because of anythingRenderChartitself does differently fromExportCachedTableToCsv– the branch lives entirely in the frontend.
So the full chain for one RenderChart call is: SVG bytes → AskJoobyFile row (backend database) →
ExportSink.Record (turn-scoped, shared across every agent and Joobrik in the turn) → AskJoobyMessageAttachmentDto
on the saved assistant message → streamed to the browser → rendered inline because the name ends in .svg. Nothing
in this chain requires the model that called RenderChart to still be running, or to be the same agent that started
the turn – which is exactly the property the tool’s own description leans on.
Cross-reference
docs/AnalyticalMethodology/Charting.md– theJube.ChartingSVG library itself: axes, series, hover behaviour, the “no JavaScript” design. This page does not repeat that material.docs/AskJooby/DataPointerCache.md– what adata_pointeris and how a table gets cached in the first place.docs/AskJooby/SqlQueryTools.md–RunSqlQuery, the most common way a pointer reachesRenderChart.
