WelisaWelisa DocGen
v1.1.0 welisa.com

Charts#

Render nine chart styles inline in generated documents — bar, column, pie, donut, pivot, stacked, clustered, line and area. Charts are produced as real PNG images by a pure-Apex rasteriser (no external services, no browser needed), so they render in every output format and from every entry point: the runner, Flows, bulk jobs, scheduled Apex. In interactive generation of PowerPoint and Excel templates over very large datasets the browser renders them instead, so the heap ceiling no longer applies.

Two authoring paths#

  1. {Chart:relationship:field:style:opts} — a one-line tag. The engine aggregates the data with a SOQL GROUP BY (constant cost regardless of row count — verified at 30,000 child rows), rasterises the chart and embeds the image. Works in all template types. Use this for 95 % of cases.
  2. {#ChartBucket:relationship:field}…{/ChartBucket} — a hand-authored loop where you write the chart markup yourself with {key} / {count} / {percent} placeholders. Only when you need a custom layout (a sparkline inside a table cell).

On a Canvas template the Chart block renders a live preview on the artboard and writes the same tag.

The {Chart:…} tag#

{Chart:RELATIONSHIP:FIELD:STYLE:OPTS}
PositionRequiredMeaning
RELATIONSHIPyesChild relationship name on the parent record (one hop)
FIELDyesField on the child to group by — use the API name
STYLEnoOne of nine styles (default bar)
OPTSnokey=value&key=value modifiers, any order
StyleVisualCross-tab?Best for
barHorizontal bar per bucket, label left, count + % rightnoOne dimension, long labels
columnVertical bar per bucketnoOne dimension, short labels
piePie with right-side legendnoShare of total, ≤ 8 slices
donutPie with a centre holenoSame, lighter visual weight
pivotCross-tab table with a Total columnrequiredA numeric matrix readout
stackedHorizontal stacked bar, segments by groupByrequired"How does each row split across the second dimension?"
clusteredVertical clustered barsrequiredSide-by-side comparison
linePolyline through (bucket, count); multi-series with groupByoptionalTrend; ordering matters
areaLine with a filled area below each seriesoptionalTrend plus accumulated volume

Worked examples against a survey with a Survey_Responses__r child relationship:

{Chart:Survey_Responses__r:Selected_Answer__c:bar:title=Commute mode}
{Chart:Survey_Responses__r:Selected_Answer__c:pie:title=Commute mode share}
{Chart:Survey_Responses__r:Selected_Answer__c:stacked:groupBy=Location__c&colSort=Amsterdam,Utrecht&title=Location mix per mode}
{Chart:Survey_Responses__r:Selected_Answer__c:line:groupBy=Location__c&colSort=Amsterdam,Utrecht&title=Trend by location}
{Chart:Survey_Responses__r:Selected_Answer__c:pivot:groupBy=Location__c&title=Mode by location (table)}

Modifiers#

ModifierApplies toExampleEffect
title=alltitle=Open cases by priorityHeader drawn above the chart; also the image's alt text
width= / height=allwidth=420&height=240Logical pixels. Defaults: 540 (stacked/clustered), 500 (bar/column/line/area), 360 (pie/donut); bar height grows with bucket count
fontSize=allfontSize=16Axis-label size in pixels (default 12); titles scale with it. Raise it when the chart lands in a small frame. On a Canvas chart use the Label size box
where=allwhere=Status__c='Open'SOQL fragment appended to the chart's WHERE; identifiers only, sanitised; forces server-side aggregation
groupBy=stacked / clustered / pivot / line / areagroupBy=Department__cThe cross-tab dimension. Required for stacked, clustered and pivot; optional for line and area
colSort=with groupBycolSort=Sales,Service,FinanceColumn order: named values first, remaining values alphabetical, Total last (pivot)
colors=allcolors=#384494,#e49007,#0f7a4aOverride the 8-colour palette; cycles by index; six-digit hex
split=bucket stylessplit=;Multi-select delimiter: each respondent counts towards every value they picked (percentages exceed 100 % by design)
scale=raster pathscale=2Supersample multiplier for a sharper PNG (default 1 or 2; 4 is the practical ceiling)
htmlRender=HTML browser preview onlyhtmlRender=svgInline SVG instead of <img> — browser only; the PDF engine drops SVG

Modifiers compose freely. Values containing & or = are not supported (use where= for operators).

Inside a loop the chart's relationship resolves against the iterating record — one chart per question, no where= needed:

{#Survey_Questions__r}
<h2>Q{Display_Order__c}: {Question_Text__c}</h2>
{Chart:Survey_Responses__r:Selected_Answer__c:bar}
{/Survey_Questions__r}
Tip

Labels too small? Label sizes are canvas pixels, so their printed size depends on how far the image is scaled to fit its frame. On a Word or PowerPoint template the image is stretched to the shape you drew, and width= does not change that — raising it sharpens the picture while making text smaller relative to the frame. fontSize= is the knob.

Errors are visible, never silent: a malformed tag renders an inline error block in HTML output (red border, the tag verbatim) and a [Chart error: …] placeholder in Word.

Output-format matrix#

TemplateOutputHow the chart gets there
HTMLPDF<img> referencing a Salesforce file, fetched server-side by the PDF engine
HTMLbrowser preview<img> (default) or inline <svg> (opt-in)
WordDOCXPNG embedded in word/media/
WordPDFsame embedding, then converted
PowerPointPPTXPNG picture on the slide
ExcelXLSXPNG embedded in the sheet (text placeholder in bulk)
CanvasPDF<img>; live preview on the artboard
Flow / batch / Apexanyserver-side rasteriser — no browser, no callouts
Important

In HTML → PDF output, use the CSS-bar styles — bar, pivot, clustered, stacked. column, pie, donut, line and area need inline SVG on the HTML path, and the PDF engine drops inline SVG: they look right in a browser preview and come out blank in the PDF. For those shapes in a PDF, Word is the better source, because the PNG pipeline carries every style. HTML is the better source for cross-tab tables (pivot never rasterises — a cross-tab is a table, not a chart shape).

Large datasets#

For PowerPoint and Excel templates with thousands of child rows, interactive generation lets the browser do the work: the runner pages the records down in chunks, aggregates them into buckets, renders the chart on a real canvas and embeds the PNG — verified to 30,000 child records. You do not opt in and do not change your tags. Flow, batch and Apex entry points still use the server-side rasteriser. Non-groupable fields (Long Text Area) are refused with a message before the platform throws; group by Text, Picklist, Number, Date or Checkbox fields.

Each generated chart produces a transient file that the runner removes after the document downloads or saves; a daily cleanup job (DocGen Chart CV Reaper) sweeps stragglers.

Query Configuration setup for charts#

How you configure the query decides whether a chart works at 300 rows, 30,000 rows or fails. The engine picks one of four resolution paths automatically:

  1. In-memory — the child collection is already loaded (small relationship in the query), no where=/groupBy=. Fastest, zero extra SOQL.
  2. SOQL fallback (recommended above 2,000 rows) — the relationship is not in the query, or where=/groupBy= is present. One GROUP BY aggregate, constant cost.
  3. Parent-level — chart outside any loop; same behaviour as above.
  4. Large-dataset parent — chart targeting a relationship in a large-dataset template; server-side aggregation.

Author's rule of thumb for the chart's target relationship:

  • ✅ Do not pre-load the relationship the chart aggregates. Leave it out of the Query Configuration; the chart aggregates it server-side.
  • ✅ Do pre-load the relationship the chart's parent loop iterates — but without a nested subquery for the chart's target underneath it.
  • ❌ Do not pre-load the target and chart against it above 2,000 rows — that triggers the large-dataset path looking for a loop body that is not there. The engine throws an actionable error if it detects this.

Worked example — a survey with 30,000 responses and one chart per question inside a question loop:

Name, (SELECT Id, Question_Text__c, Display_Order__c FROM Survey_Questions__r ORDER BY Display_Order__c ASC)

Id in the question subquery is required (the chart scopes its aggregate by each question's Id); there is no nested Survey_Responses__r subquery. CPU stays under 500 ms and SOQL under 10 whether the survey has 425 or 30,000 responses.

Security and governor budget#

Charts run in user mode: object permissions, field-level security and sharing are enforced at the database layer. A user without read access to the child object sees empty buckets, never a leaked aggregate. Every dynamic identifier is validated against the schema and where= fragments go through the same keyword block-list as the query builder — injection is structurally impossible.

Chart aggregates are capped at 50 SOQL queries per transaction; past the budget the remaining charts render a single "Chart limit reached" bucket rather than nothing. Per-chart CPU is tracked too; the rasteriser tunes the default scale= so an eight-chart document fits the synchronous Apex limit. In bulk jobs keep batch size 1 for chart-heavy templates.

Hand-authored {#ChartBucket} loops#

For a custom layout, drop down to the underlying section tag. Aggregation is identical — same modifiers, same SOQL fallback at scale.

<table>
{#ChartBucket:Survey_Responses__r:Selected_Answer__c}
<tr>
  <td>{key_label}</td>
  <td><div style="background:{color};width:{percent}%;height:14px;"></div></td>
  <td>{count} ({percent}%)</td>
</tr>
{/ChartBucket}
</table>

Buckets sort by count descending, then key alphabetically; blanks collapse into one "Not Specified" bucket. Body fields: {key}, {key_label} (picklist label when available), {count}, {percent} (one decimal), {percent_int}, {max_percent} (largest bucket = 100), {index}, {color} (#hex), {color_hex} (without #, for Word shading). Add {:else}No responses yet. before the closing tag for an empty fallback. Modifiers (colors=, where=, split=, groupBy=, colSort=) go in a third colon segment.

Pivot bodies with groupBy= expose a {#cols}…{/cols} sub-loop per bucket (plus a Total column) with the standard fields and two pivot-only ones, {percent_of_row} and {percent_of_row_int}. Because HTML container auto-expansion duplicates the nearest open <tr> for a nested loop, use <div> with display: table-row / table-cell for pivot bodies, not <tr>/<td>.

In Word, hand-authored cross-tabs with {#cols} inside a table row hit the same row-expansion limitation and bar widths are compressed by Word's percentage normalisation — prefer {Chart:…:stacked} or {Chart:…:clustered}, which embed PNGs.

Paste-ready AI prompt for a chart template#

You are writing a Welisa DocGen HTML template that renders charts with the {Chart:...} tag.
The template is a single HTML file rendered to PDF by a CSS 2.1 engine.

Hard constraints:
- Layout with <table>/<tr>/<td> or <div> with display:table/table-row/table-cell.
- Do NOT use flex, grid, gap, linear-gradient, calc(), CSS variables, border-radius, rgba().
- Do NOT use <svg> — the PDF engine drops it. In HTML→PDF use only the styles bar, pivot, clustered, stacked.
- One <style> in <head> with @page { size: 8.27in 11.69in; margin: 1.5cm; }.

Chart tag: {Chart:RELATIONSHIP:FIELD:STYLE:OPTS}
  RELATIONSHIP = child relationship name (e.g. Survey_Responses__r); FIELD = API name of the field to bucket by;
  STYLE = bar | column | pie | donut | pivot | stacked | clustered | line | area;
  OPTS = key=value&key=value: title=, width=, height=, fontSize=, where=, groupBy= (required for stacked/clustered/pivot),
         colSort=, colors=#hex,#hex, split=, scale=.

Data shape: ${MY_DATA_DESCRIPTION}
(Example: "Survey__c parent. Child relationship Survey_Responses__r. Bucket by Selected_Answer__c.
 Cross-tab dimension Location__c with values Amsterdam, Utrecht.")

Produce: a title row with {Name}, then one section per chart style with a heading, a one-line description,
and the {Chart:...} tag in <div class="chart">, each section on its own page. Return the complete HTML only.