The sibling of Handlebars chart templates for the Power BI engine. Two libraries, both shared:
| Dashboard-Template | Visual-Template | |
|---|---|---|
| What it is | A complete Power BI theme document: palette, semantic colours, text classes, per-type defaults in visualStyles | How one visual type looks (lineChart, card, tableEx, …), with colours and fonts as tokens |
| Where it goes | <name> in the .pbip / .pbit | Merged into that theme under visualStyles.<type>."*", and optionally into each visual.json |
| Built-ins | The 20 RASTER themes, seeded from * | * |
| Who can add one | Everyone, in Libraries → Visuals and themes | Same |
| Who can edit it | Its author. Built-ins answer 409 "copy it instead" | Same |
| Who can use it | Everyone, once it is Public; its author alone while it is Private | Same |
| Limit | 10 per user per kind | 10 per user per kind |
Private or Public. A template you create starts Private — yours alone to see and to name as a theme default or a draft override. Publish it and everyone can use it; it stays yours to edit. Taking a published one private again never breaks an artifact that already used it: visibility decides who may choose a template, and the export path resolves names blind, exactly as it already does for a disabled theme.
A blank theme= deals a published theme at random, never anybody's private one.
Everything here is free: authoring a template runs no AI, choosing one on a draft runs no agent, and applying it at export is a JSON merge. The engine never reads the design notes.
1. How a look reaches the artifact
- The export resolves the draft's (or the one-shot run's) Dashboard-Template.
- For each emitted visual type it picks a Visual-Template: the draft's own choice first, else the theme's default, else none. An empty-string choice switches a theme default off.
- Theme channel — the template's
visualStylesblock, tokens substituted, replaces the matching formatting-object arrays undervisualStyles.<type>."*"in the theme. Replace, not merge:legend: [{show:false}]means exactly that. A theme with nothing applied is written byte-for-byte as stored. - Visual channel — the template's
objectsandvisualContainerObjectsare written into everyvisual.jsonof that type, with{{field.<Role>[n]}}resolved to that visual's ownqueryRefs. An entry whose field token has no projection (a second series on a one-measure chart) is dropped; the rest applies. The writer's title text always wins. - The job row records the theme (
style_name) and the effective map (visual_templates).
A Visual-Template name that no longer resolves is skipped with a warning; the export never fails because a template went missing.
A run on a model the user uploaded (the upload-formats page, §4c) takes a Dashboard-Template and Visual-Templates exactly as a run from data files does: the theme goes into the new report's StaticResources, the visual channel into its visual.json files, and the user's model is not touched.
With a design
A run or a draft drawn with a design (HTML dashboard §5a) has no Dashboard-Template: its style names a palette from the colours library, and the theme is composed at export — a neutral base (base, RASTER 01's structure with every colour a token), the design's fragment over it (its own powerbi.json, or the one generated from its structure, fonts and chart style), every token resolved from the palette, and the palette's eight series as dataColors. The token table below reads the palette then: accent is its accent, dataColors.N its series, canvas its background, surfaceAlt its surface2, line its border, good/neutral/bad its positive/neutral/negative. Visual-Templates apply on top exactly as above; the design's own defaults (visuals.powerbi) sit between the draft's choice and the Dashboard-Template's. The job and the draft record the palette as their theme and the design beside it.
2. Tokens
| Token | Resolves to |
|---|---|
{{accent}} | dataColors[0] |
{{dataColors.N}} (0…7) | dataColors[N] — no fallback beyond the palette; the entry is dropped |
{{text}}, {{textSecondary}}, {{textMuted}} | foreground, foregroundNeutralSecondary, foregroundNeutralTertiary |
{{surface}}, {{surfaceAlt}}, {{canvas}}, {{line}} | background, backgroundLight, backgroundNeutral, secondaryBackground |
{{tableAccent}}, {{good}}, {{neutral}}, {{bad}} | the same keys |
{{font.callout}} … {{font.label}} | textClasses.<x>.fontFace |
{{size.callout}} … {{size.label}} | textClasses.<x>.fontSize (a number stays a number) |
{{field.<Role>[n]}} | the queryRef of projection n in that role — objects only |
{{field.<Role>[n].entity}}, {{field.<Role>[n].property}} | that projection's table and column/measure name, for a PBIR Measure(...) expression |
Missing keys fall back (textMuted → foreground, canvas → background, tableAccent → accent, good/neutral/bad → fixed defaults, fonts → Segoe UI, sizes → 10); the theme detail endpoint marks those as inherited. A string that is a token takes the typed value; a string that contains one ("'{{accent}}'" in a PBIR literal) is substituted textually. In the objects channel Power BI's own ThemeDataColor(ColorId, Percent) expression is also fine — Power BI resolves it against the exported palette.
3. The Visual-Template body
{
"visualStyles": { "lineStyles": [ { "strokeWidth": 3, "lineChartType": "smooth" } ],
"legend": [ { "show": false } ] },
"objects": { "lineStyles": [
{ "properties": { "lineStyle": { "expr": { "Literal": { "Value": "'dashed'" } } } },
"selector": { "metadata": "{{field.Y[1]}}" } } ] },
"visualContainerObjects": { "background": [ { "properties": { "show": { "expr": { "Literal": { "Value": "false" } } } } } ] }
}
- Up to those three keys, at least one present.
visualStylesis theme grammar (plain values,{"solid":{"color":…}}); the other two are PBIR grammar ({properties, selector?}entries, typed literals"3D","76L","'smooth'"— seeformat-findings). - The engine checks shape and tokens only. Property names are not validated: the vendored schema types them as a free bag, and Power BI ignores what it does not know.
visualTypeis one the writer emits:card,kpi,lineChart,areaChart,clusteredColumnChart,clusteredBarChart,pieChart,funnel,scatterChart,tableEx,pivotTable(the Matrix) — or one of the theme-only typesslicer,advancedSlicerVisual,actionButton, which the writer never places but the theme still styles for the visuals a user adds in Desktop (the theme channel only;objectsnever fires for them).GET /api/v1/powerbi/visual-typesserves both with anemittedflag. Notecard, notcardVisual— the writer still emits the older type; a switch there is a migration of every stored map.- Per-state styling (buttons, button slicers) uses the theme's
$identries:default,selection:selected,selection:unselected,hover, … - Design notes are Markdown for people, in the manner of
next-level-grafik§11: what the look is, what may vary, what must not.
4. The repository path (built-ins)
Theme files: identity = file stem, stored verbatim, description and keywords from the CATALOG in PowerBiDashboardTemplateSeeder (derived from raster-20-stile-DE.md). Visual files: {displayName, description, whenToUse, keywords, visualType, body, designNotes}, identity = file stem. Both seeders: insert, update on checksum change, never touch enabled, never overwrite an authored row, log names only. PowerBiTemplateAssetsTest fails CI when a shipped visual body leaves a token unresolved in any shipped theme, hard-codes a hex colour, or lacks whenToUse/keywords. Renaming a file orphans every draft and theme default that pins it — change displayName instead.
5. Previews are honest
The browser draws a swatch and a CSS sample tile from the tokens, a small SVG sketch of what a Visual-Template asks for (stroke, area, markers, axis, fonts), and the substituted JSON a template will be told. It does not render Power BI visuals. Open the .pbip in Desktop to see the look; Desktop lists the theme under View → Themes by its display name.
6. Endpoints
Engine (:8086, X-User-Id optional on reads, required on writes): GET|POST /api/v1/powerbi/themes, GET|PUT|DELETE /api/v1/powerbi/themes/{name}, GET|POST /api/v1/powerbi/visual-templates[?visualType=], GET|PUT|DELETE /api/v1/powerbi/visual-templates/{name}, GET /api/v1/powerbi/visual-types, PUT /api/v1/drafts/{id}/style {themeName, visualTemplates}, and the one-shot visualTemplates multipart part. Studio proxies all of them under the same paths, stamps the author from the JWT, and charges nothing.
DASH:Gen