references/authoring.md
# Authoring Workflows & Examples
Read this for concrete PBIR authoring workflows: pages, visuals, layout
templates, themes, drillthrough, interactions, and visual-type routing.
Related references:
- [formatting.md](formatting.md) — value encoding, colors, selectors, VCOs, and formatting properties.
- [expressions.md](expressions.md) — field expressions, filters, and sort definitions.
> **Before editing existing reports:** inventory the report with
> `powerbi-report-author preview-pages <path-to-.Report-dir>` and
> `powerbi-report-author preview-visuals <path-to-.Report-dir>`.
> Add `--with-derived` when you need counts or cheap computed flags.
>
> **Examples are structural patterns.** Property names may vary by visual type.
> Always confirm exact properties with
> `powerbi-report-author formatting describe-object <type> <object>` before
> applying formatting.
## Contents
- [Names & Format Versions](#names--format-versions)
- [Add a New Page](#add-a-new-page)
- [Add a Visual to a Page](#add-a-visual-to-a-page)
- [Common Layout Templates](#common-layout-templates)
- [Change Theme](#change-theme)
- [Visual Type References](#visual-type-references)
- [Drillthrough Page](#drillthrough-page)
- [Visual Interactions](#visual-interactions)
## Names & Format Versions
### ID Generation
| Element | Format | Scope |
|---|---|---|
| Visual `name` | 20 lowercase hex chars (e.g. `f4214b297bb2c1e49dfe`) | Unique within a page |
| Page `name` | Modern: bare 20 hex chars. Traditional: `ReportSection` + 24 hex chars. The first page may be just `ReportSection` (no suffix). | Unique across the report |
| Filter `name` | `Filter` + 24 lowercase hex chars (e.g. `Filter1a2b3c4d5e6f7890a1b2c3d4`) | Unique across the entire report definition — `powerbi-report-author validate` flags duplicates with `PBIR_FILTER_NAME_DUPLICATE_*` |
```javascript
// Node.js
const crypto = require('crypto');
const visualId = crypto.randomBytes(10).toString('hex'); // 20 hex chars
const pageId = 'ReportSection' + crypto.randomBytes(12).toString('hex'); // ReportSection + 24 hex
const filterId = 'Filter' + crypto.randomBytes(12).toString('hex'); // Filter + 24 hex
```
### Format Versions
These are file-content versions (not `$schema` URLs) and are constants for the
current PBIR format:
- `version.json` → `"version": "2.0.0"`
- `definition.pbir` → `"version": "4.0"`
For `$schema` URL versioning rules and the file layout, see
[SKILL.md § PBIR File Layout](../SKILL.md#pbir-file-layout). When creating a
new file of any type, copy the `$schema` URL from an existing file of the same
type in the same report.
## Add a New Page
1. Generate a unique page name (e.g. `ReportSection` + 24 hex chars)
2. Create the directory: `definition/pages/<pageName>/`
3. Create `page.json`:
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/page/2.1.0/schema.json",
"name": "<pageName>",
"displayName": "My New Page",
"displayOption": "FitToPage",
"height": 720,
"width": 1280
}
```
4. Create `visuals/` subdirectory (can be empty initially)
5. Add the page name to `pages.json` → `pageOrder` array at the desired position
**Note**: For the `$schema` URL, copy from an existing `page.json` in the same report.
## Add a Visual to a Page
1. Generate a unique visual name (20 hex chars)
2. Create `definition/pages/<pageName>/visuals/<visualName>/visual.json`
3. Populate with the visual definition:
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<visualName>",
"position": {
"x": 0, "y": 0, "z": 1000,
"height": 300, "width": 400,
"tabOrder": 1000
},
"visual": {
"visualType": "<type>",
"query": {
"queryState": {
"<RoleName>": {
"projections": [
{
"field": { /* expression — see references/expressions.md */ },
"queryRef": "<Entity>.<Property>",
"nativeQueryRef": "<Property>"
}
]
}
}
}
}
}
```
**⚠️ All role projections (Rows, Columns, Values, etc.) must be inside `queryState`, not directly under `query`.** Placing them under `query` causes a schema validation error.
**Position guidelines** (1280×720 canvas):
- `z`: Stacking order. Higher = on top. Increment by 1000 for each visual.
- `tabOrder`: Keyboard navigation order. Usually matches `z`.
- Keep visuals within canvas bounds: `x + width ≤ 1280`, `y + height ≤ 720`.
## Common Layout Templates
**Full-width KPI row + detail chart:**
```text
KPI Card 1: x=20, y=20, w=290, h=120
KPI Card 2: x=330, y=20, w=290, h=120
KPI Card 3: x=640, y=20, w=290, h=120
KPI Card 4: x=950, y=20, w=290, h=120
Main Chart: x=20, y=160, w=1240, h=540
```
**2×2 Grid:**
```text
Top-Left: x=20, y=20, w=610, h=340
Top-Right: x=650, y=20, w=610, h=340
Bottom-Left: x=20, y=380, w=610, h=320
Bottom-Right: x=650, y=380, w=610, h=320
```
**Header + sidebar + main:**
```text
Header: x=0, y=0, w=1280, h=80
Sidebar: x=0, y=80, w=280, h=640
Main: x=300, y=80, w=960, h=640
```
## Change Theme
In `report.json`, update the `themeCollection.customTheme`:
```json
{
"customTheme": {
"name": "<CustomThemeName>-<guid>.json",
"reportVersionAtImport": {
"visual": "2.6.0",
"report": "3.1.0",
"page": "2.3.0"
},
"type": "RegisteredResources"
}
}
```
Also ensure the theme file exists in `StaticResources/RegisteredResources/`
and is listed in the `resourcePackages` array with matching `name` and `path`.
On every theme edit, follow the [GUID cache-busting procedure in theming.md](theming.md#theme-name-guid-convention-cache-busting)
— rotate the GUID suffix (keep `<CustomThemeName>` stable), update all
`report.json` references, and reload Desktop. Only change `<CustomThemeName>`
if the user explicitly requests a rename.
> ⚠️ **`type` must be `"RegisteredResources"`** — not `"SharedResources"`.
> Using the wrong type causes the theme to silently fail.
>
> Inside `report.json`, both `customTheme.name` and the matching
> `resourcePackages[].items[].name` MUST include the `.json` extension and
> equal the item's `path` (e.g., all three are `"<ThemeName>.json"`). Using
> the bare theme name causes the published report to incorrectly apply the theme
> on Power BI service. The `name` field **inside the theme JSON file itself**
> stays as the bare theme name (no `.json` suffix). See `theming.md` for
> details.
## Visual Type References
Use these focused references for visual-specific templates, formatting rules,
and known rendering pitfalls.
| Intent | Read |
|---|---|
| Bar, column, and line charts | [cartesian.md](cartesian.md) |
| Cards and KPI callouts | [card.md](card.md) |
| Tables and matrices | [table.md](table.md) |
| Slicers and slicer selections | [slicers.md](slicers.md) |
| Image visuals | [image.md](image.md) |
| Shapes, dividers, and containers | [shape.md](shape.md) |
| Maps | [map.md](map.md) |
| Static or dynamic textboxes | [textbox.md](textbox.md) |
## Drillthrough Page
A drillthrough page adds `pageBinding` and drillthrough filters to `page.json`:
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/page/2.1.0/schema.json",
"name": "<pageName>",
"displayName": "Detail Drillthrough",
"displayOption": "FitToPage",
"height": 720,
"width": 1280,
"filterConfig": {
"filters": [
{
"name": "Filter<24hex>",
"field": {
"Column": {
"Expression": { "SourceRef": { "Entity": "<Table>" } },
"Property": "<DrillthroughColumn>"
}
},
"type": "Categorical",
"howCreated": "Drillthrough"
}
]
},
"pageBinding": {
"name": "Pod",
"type": "Drillthrough",
"parameters": [
{
"name": "Param_Filter<24hex>",
"boundFilter": "Filter<24hex>",
"fieldExpr": {
"Column": {
"Expression": { "SourceRef": { "Entity": "<Table>" } },
"Property": "<DrillthroughColumn>"
}
}
}
]
}
}
```
**Key points**:
- Each drillthrough field needs both a filter entry (with `"howCreated": "Drillthrough"`)
and a matching `pageBinding.parameters` entry with `boundFilter` referencing the filter name.
- The `pageBinding.name` is typically `"Pod"`.
- `pageBinding.type` is `"Drillthrough"` (or `"Tooltip"` for tooltip pages).
## Visual Interactions
Control how visuals cross-filter/highlight each other on a page.
Add `visualInteractions` array to `page.json`:
```json
{
"visualInteractions": [
{
"source": "<sourceVisualName>",
"target": "<targetVisualName>",
"type": "NoFilter"
}
]
}
```
| Type | Effect |
|------|--------|
| `"NoFilter"` | Source visual does NOT filter target |
| `"DataFilter"` | Source cross-filters target (shows subset) |
| `"HighlightFilter"` | Source highlights matching data in target |
By default (no entry), visuals cross-filter each other. Only add entries to
**override** the default behavior.
references/card.md
# Card Visual Authoring Guide
Cards (`cardVisual`) display one or more headline metrics. `card` and `multiRowCard`
(both legacy) are deprecated — always use `cardVisual`.
- [Single-Value Template](#single-value-template)
- [Multi-Value Template](#multi-value-template)
- [Key Formatting Rules](#key-formatting-rules)
- [Multi-Value Formatting](#multi-value-formatting)
- [When to Consolidate vs. Keep Separate](#when-to-consolidate-vs-keep-separate)
- [Theme Approach](#theme-approach)
- [Discovering Properties](#discovering-properties)
- [References](#references)
---
## Single-Value Template
> ⚠️ **Role name is `Data`, not `Fields`.** The only valid `queryState` key for
> `cardVisual` is `"Data"`. Using `"Fields"` (the legacy `card` role name) causes
> the visual to render empty — PBI Desktop cannot resolve the binding. The
> validator catches this as `Unknown role "Fields"` and `Required role "Data" missing`.
The `Data` role accepts one or more measures. For a single headline KPI:
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 24, "y": 56, "z": 1000, "height": 80, "width": 296, "tabOrder": 1000 },
"visual": {
"visualType": "cardVisual",
"query": {
"queryState": {
"Data": {
"projections": [{
"field": { "Measure": { "Expression": { "SourceRef": { "Entity": "<Table>" } }, "Property": "<Measure>" } },
"queryRef": "<Table>.<Measure>",
"nativeQueryRef": "<Measure>"
}]
}
}
}
}
}
```
---
## Multi-Value Template
> **Default for multiple related KPIs:** When the user asks for 2–5 related
> KPIs on the same row (e.g. "Sales, Profit, Units, Gross Margin"), create
> **one** multi-value `cardVisual` with all measures as projections in `Data`.
> Do **not** create separate single-value cards unless the user explicitly needs
> per-card styling differences (see [When to Consolidate vs. Keep Separate](#when-to-consolidate-vs-keep-separate)).
Add multiple projections to the `Data` role. PBI renders them as a horizontal
row of callouts inside one visual. Use this instead of placing multiple
single-value cards side by side when they share the same container styling.
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 24, "y": 56, "z": 1000, "height": 120, "width": 900, "tabOrder": 1000 },
"visual": {
"visualType": "cardVisual",
"query": {
"queryState": {
"Data": {
"projections": [
{
"field": { "Measure": { "Expression": { "SourceRef": { "Entity": "<Table>" } }, "Property": "<Measure1>" } },
"queryRef": "<Table>.<Measure1>",
"nativeQueryRef": "<Measure1>"
},
{
"field": { "Measure": { "Expression": { "SourceRef": { "Entity": "<Table>" } }, "Property": "<Measure2>" } },
"queryRef": "<Table>.<Measure2>",
"nativeQueryRef": "<Measure2>"
},
{
"field": { "Measure": { "Expression": { "SourceRef": { "Entity": "<Table>" } }, "Property": "<Measure3>" } },
"queryRef": "<Table>.<Measure3>",
"nativeQueryRef": "<Measure3>"
}
]
}
}
}
}
}
```
> **Sizing tip:** Multi-value cards need more width. Allow ~250–300 px per
> callout. For 3 measures a width of 900 px works well. Height of 100–120 px
> accommodates value + label without clipping.
---
## Key Formatting Rules
### Instance selectors required
Most `cardVisual` formatting objects require `selector: { "id": "default" }`.
Without it, properties silently fail to apply.
Objects that need the `id` selector: `value`, `label`, `accentBar`, `outline`,
`padding`, `spacing`, `divider`, `fillCustom`, `shadowCustom`, `glowCustom`,
`image`, `layout`, `referenceLabelTitle`, `referenceLabelValue`,
`referenceLabelDetail`.
Objects that do **NOT** need a selector: `cardCalloutArea`, `referenceLabel`,
`referenceLabelLayout`.
### Remove the internal border
The `outline` object controls the internal rectangular border inside the card.
To remove it (recommended):
```json
"outline": [{
"properties": { "show": { "expr": { "Literal": { "Value": "false" } } } },
"selector": { "id": "default" }
}]
```
> ⚠️ This does **NOT** cascade from theme `visualStyles` — must be set
> per-visual. The outer container border (VCO `border`) is separate and
> does cascade from theme.
### Override the category label text
By default the card shows the raw measure name from the model. Override
with `label.text`:
```json
"label": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"text": { "expr": { "Literal": { "Value": "'Total Revenue'" } } }
},
"selector": { "id": "default" }
}]
```
### Font sizing and clipping — MANDATORY pre-check
> **BLOCKING REQUIREMENT.** Before creating or resizing any cardVisual, compute
> `required_height` and `required_width` using the formulas below. If either
> exceeds the card's dimensions, adjust or reduce font sizes.
#### Step 1: Resolve effective values from cascade
Before computing, inspect these sources in priority order (first match wins):
1. **Visual file** (`visual.json` → `objects` and `visualContainerObjects`)
2. **Custom theme** → `visualStyles.cardVisual.*` (type-specific)
3. **Custom theme** → `visualStyles.*.*` (global wildcard)
4. **Custom theme** → `textClasses.callout.fontSize` (for value default)
5. **Base theme** → `visualStyles.cardVisual.*` (content padding, spacing)
Check the **base theme** (`StaticResources/SharedResources/BaseThemes/*.json`)
for the content area default properties:
- `visualStyles.cardVisual.*.padding.paddingUniform` → content inner padding
- `visualStyles.cardVisual.*.layout.paddingUniform` → content outer padding
- `visualStyles.cardVisual.*.spacing.verticalSpacing` → gap between value and label
These follow the same cascade as other properties (visual `objects` →
custom theme `cardVisual.*` → custom theme `*.*` → base theme).
Check the **custom theme** JSON for:
- `textClasses.callout.fontSize` → this is the default `value_fontSize`
- `visualStyles.*.*.padding.top/bottom` → VCO padding override
- `visualStyles.*.*.border.show` + `.width` → border contribution
- `visualStyles.cardVisual.*.spacing.verticalSpacing` → verticalSpacing override
Check the **visual** JSON for any per-card overrides on:
- `objects.value.fontSize`, `objects.label.fontSize`
- `visualContainerObjects.padding.top/bottom`
- `visualContainerObjects.border.show/width`
- `visualContainerObjects.title.show/fontSize`
- `visualContainerObjects.spacing.spaceBelowTitleArea`
Only after resolving all effective values, proceed to Step 2.
#### Step 2: Card visual anatomy (top to bottom)
```
┌──────────────────────────────────────────────────────────┐
│ VCO border (top) │ border_width
├──────────────────────────────────────────────────────────┤
│ VCO padding (top) │ padding_top
├──────────────────────────────────────────────────────────┤
│ Title text (if shown) │ render(title_fontSize)
│ Space below title │ spaceBelowTitleArea
├──────────────────────────────────────────────────────────┤
│ Content padding (top) │ content_padding_top
│ Value text: "283K" │ render(value_fontSize)
│ verticalSpacing │ spacing.verticalSpacing
│ Label text: "Sum of Profit" │ render(label_fontSize)
│ Content padding (bottom) │ content_padding_bottom
├──────────────────────────────────────────────────────────┤
│ VCO padding (bottom) │ padding_bottom
├──────────────────────────────────────────────────────────┤
│ VCO border (bottom) │ border_width
└──────────────────────────────────────────────────────────┘
```
Key facts:
- **Label ALWAYS renders** even with `label.show=false`. Allocate for ≥12pt.
- **Content padding** comes from two objects, each with uniform/individual modes:
- `objects.padding`: if `paddingIndividual=true` use per-side values, else `paddingUniform`
- `objects.layout`: if `paddingIndividual=true` use `topOuterMargin`/`bottomOuterMargin`, else `paddingUniform`
- **VCO padding** (`visualContainerObjects.padding`): `top`/`bottom`/`left`/`right`
- **verticalSpacing**: from `objects.spacing` or `visualContainerObjects.spacing`
- **calloutSize** (`objects.layout.calloutSize`): percentage that may scale the
content area. Runtime default is unverified.
#### Step 3: Compute height
```
render(fs) = ceil(fs × 1.5)
# Resolve content padding from cascade:
# - padding object: paddingUniform (or paddingTop/paddingBottom if paddingIndividual=true)
# - layout object: paddingUniform (or topOuterMargin/bottomOuterMargin if paddingIndividual=true)
content_padding_top = padding_obj_top + layout_obj_top
content_padding_bottom = padding_obj_bottom + layout_obj_bottom
required_height = border_width × 2
+ padding_top + padding_bottom
+ (render(title_fontSize) + spaceBelowTitleArea) × title_visible
+ content_padding_top + content_padding_bottom
+ render(value_fontSize)
+ verticalSpacing
+ render(effective_label_fontSize)
+ accentBar_width × accentBar_top_or_bottom
effective_label_fontSize = max(explicit_label_fontSize, 12)
Constraint: required_height ≤ position.height
```
#### Step 4: Width considerations
Width overflow shows ellipsis ("...") rather than clipping — less severe than
height clipping. Properties that consume horizontal space:
- VCO padding left/right (`visualContainerObjects.padding`)
- Content padding left/right (from `objects.padding` and `objects.layout`, same uniform/individual logic as vertical)
- Border width (left + right)
- Accent bar width (if positioned left or right)
If values are truncated with ellipsis, increase `position.width` or reduce
`value_fontSize`.
Use **5 chars** when display format is unknown. Width overflow shows ellipsis.
#### Default values (cascade resolution)
Priority: visual `objects` → custom theme `cardVisual.*` → custom theme `*.*` → base theme.
| Variable | Source | Notes |
|----------|--------|-------|
| `value_fontSize` | `textClasses.callout.fontSize` | Override via `objects.value.fontSize` (id selector) |
| `label_fontSize` | `textClasses.label.fontSize` | Override via `objects.label.fontSize` (id selector) |
| `padding_top/bottom/left/right` | `visualContainerObjects.padding` | VCO padding around the whole visual |
| `padding` object | `objects.padding` (id selector) | `paddingUniform` or individual `paddingTop/Bottom/Left/Right` |
| `layout` object | `objects.layout` (id selector) | `paddingUniform` or individual `topOuterMargin/bottomOuterMargin/leftOuterMargin/rightOuterMargin` |
| `verticalSpacing` | `objects.spacing` (id selector) | Gap between value and label |
| `spaceBelowTitleArea` | `visualContainerObjects.spacing` | Gap below title area |
| `calloutSize` | `objects.layout.calloutSize` | Percentage; runtime default unverified |
| `title_fontSize` | `visualContainerObjects.title` | Title font size when title is shown |
| `border_width` | `visualContainerObjects.border` | Only contributes when `border.show=true` |
| `accentBar_width` | `objects.accentBar` (id selector) | Only contributes when `accentBar.show=true` |
> Always read the report's base theme file
> (`StaticResources/SharedResources/BaseThemes/*.json`) for authoritative
> default values. Do not assume hardcoded constants.
#### Worked examples
These examples assume base theme values: `padding.paddingUniform=12`,
`layout.paddingUniform=12`, `verticalSpacing=2`, VCO padding top/bottom=8.
Always verify these values from the actual base theme file.
**Example 1 — theme callout=36, card h=100 w=180 (FAILS):**
```
content_padding = (12+12) × 2 = 48
required_height = 0 + 8+8 + 0 + 48 + ceil(36×1.5) + 2 + ceil(12×1.5) + 0
= 16 + 48 + 54 + 2 + 18 = 138
→ 138 > 100 → CLIPS!
Fix: set value.fontSize=20 → 16+48+30+2+18 = 114, use h=120
```
**Example 2 — with title and accent bar:**
```
title_area = render(title_fontSize) + spaceBelowTitleArea
required_height = border + VCO_padding + title_area + content_padding
+ render(value) + verticalSpacing + render(label) + accentBar
```
**Example 3 — custom VCO padding=4, border=2:**
```
required_height = 2×2 + 4+4 + 0 + 48 + ceil(32×1.5) + 2 + ceil(14×1.5) + 0
= 4 + 8 + 48 + 48 + 2 + 21 = 131
→ use h ≥ 131
```
#### Quick-reference safe dimensions
Example assuming `padding.paddingUniform=12`, `layout.paddingUniform=12`,
VCO padding=8, `verticalSpacing=2`, label=12pt, no title, no border:
| value.fontSize | Min height |
|----------------|------------|
| 20 | 114 |
| 24 | 120 |
| 28 | 126 |
| 32 | 132 |
| 36 | 138 |
| 40 | 144 |
| 45 | 152 |
With title: add `render(title_fontSize) + spaceBelowTitleArea` to height.
With custom VCO padding=4: subtract **8px** from height.
### Accent bar
Adds a colored edge bar. Match color to the card's accent from the palette:
```json
"accentBar": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"position": { "expr": { "Literal": { "Value": "'Left'" } } },
"width": { "expr": { "Literal": { "Value": "4D" } } },
"color": { "solid": { "color": { "expr": { "Literal": { "Value": "'#0072B2'" } } } } }
},
"selector": { "id": "default" }
}]
```
> **Tip:** When using an accent bar, set VCO `padding` to all zeros so the
> bar spans the full card height. Set `layout` outer margins to all zeros
> so the accent bar is flush against the card border. Set `padding`
> `topMargin: 0L`, `bottomMargin: 0L` (content sits tight against top),
> `leftMargin: 12L` (breathing room from the bar), `rightMargin: 8L`.
---
## Multi-Value Formatting
These formatting objects only take effect when the card has **2 or more**
measures in the `Data` role. On single-value cards they validate but have
no visible effect.
### cardCalloutArea
Controls per-callout tile styling — padding, corner radius, background fill.
Does **not** need an `id` selector.
```json
"cardCalloutArea": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"paddingUniform": { "expr": { "Literal": { "Value": "8L" } } },
"rectangleRoundedCurve": { "expr": { "Literal": { "Value": "6L" } } },
"backgroundFillColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#F5F5F5'" } } } } },
"backgroundTransparency": { "expr": { "Literal": { "Value": "0D" } } }
}
}]
```
### layout (gridlines between callouts)
Draws vertical separator lines between each callout tile. Use the `layout`
object — `cardVisual` does have a `grid` object that validates, but it does
**not** render the inter-callout separators in PBI Desktop. The working path
is `layout` with `style: "Table"` plus `customizeLines: true`, which unlocks
the `gridline*` properties below.
```json
"layout": [{
"properties": {
"style": { "expr": { "Literal": { "Value": "'Table'" } } },
"customizeLines": { "expr": { "Literal": { "Value": "true" } } },
"gridlineWidth": { "expr": { "Literal": { "Value": "1D" } } },
"gridlineColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#E0E0E0'" } } } } },
"gridlineTransparency": { "expr": { "Literal": { "Value": "0D" } } },
"gridlineStyle": { "expr": { "Literal": { "Value": "'solid'" } } }
},
"selector": { "id": "default" }
}]
```
### divider
Horizontal divider line between value and label within each callout. Property
names are prefixed with `divider*` (the unprefixed `width/color/style/...`
belong to the visual-container `divider` object, not `cardVisual`'s own).
```json
"divider": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"dividerWidth": { "expr": { "Literal": { "Value": "1D" } } },
"dividerColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#E0E0E0'" } } } } },
"dividerTransparency": { "expr": { "Literal": { "Value": "0D" } } },
"dividerLineStyle": { "expr": { "Literal": { "Value": "'solid'" } } },
"dividerIgnorePadding": { "expr": { "Literal": { "Value": "true" } } }
},
"selector": { "id": "default" }
}]
```
> **Note:** `value` and `label` formatting (font, size, color, alignment)
> applies uniformly to all callouts — you cannot style individual callouts
> differently within the same multi-value card.
---
## When to Consolidate vs. Keep Separate
**Default: always use one multi-value `cardVisual`** for multiple KPIs. Put all
measures as projections in the `Data` role — this is exactly what `cardVisual`
is designed for. Do **not** create separate single-value cards per metric unless
the user explicitly needs per-card styling differences (see below).
**Only keep separate single-value cards** when the user specifically requires:
- Per-card accent bar colors (each card gets its own accent color)
- Different background colors or conditional formatting per metric
- Different font sizes per metric (e.g., one hero card larger than the rest)
- Individual card click/drill-through behavior
---
## Theme Approach
These `cardVisual` defaults can be applied report-wide via theme
`visualStyles` (plain JSON, not PBIR `expr` wrappers):
```json
"cardVisual": {
"*": {
"value": [{ "bold": true, "$id": "default" }],
"label": [{ "show": true, "$id": "default" }],
"cardCalloutArea": [{ "paddingUniform": 0 }],
"border": [{ "show": true, "color": { "solid": { "color": "#E8E8E8" } }, "radius": 8 }],
"title": [{ "show": false }],
"spacing": [{ "verticalSpacing": -6 }],
"padding": [{ "top": 0, "bottom": 0, "left": 0, "right": 0 }]
}
}
```
> ⚠️ `outline`, `accentBar`, `layout`, visual-level `padding` (with `leftMargin` etc.),
> `value.fontColor`, and `label.text` do **NOT** cascade from theme — they
> must be set per-visual.
>
> **VCO mixing caveat**: If you set ANY VCO property per-visual (background,
> border, visualHeader), also set `padding` per-visual in the same
> `visualContainerObjects` block. Otherwise PBI may reset padding to its
> default (~5px) instead of inheriting from the theme.
**Layout outer margins** — To eliminate the gap between the card border and
content (so the accent bar sits flush), set `layout` with `id: "default"`:
```json
"layout": [{
"properties": {
"topOuterMargin": { "expr": { "Literal": { "Value": "0L" } } },
"bottomOuterMargin": { "expr": { "Literal": { "Value": "0L" } } },
"leftOuterMargin": { "expr": { "Literal": { "Value": "0L" } } },
"rightOuterMargin": { "expr": { "Literal": { "Value": "0L" } } },
"paddingUniform": { "expr": { "Literal": { "Value": "0L" } } }
},
"selector": { "id": "default" }
}]
```
---
## Discovering Properties
```bash
# List all formatting objects for cardVisual
powerbi-report-author formatting list-objects cardVisual
# Inspect a specific object
powerbi-report-author formatting describe-object cardVisual value
powerbi-report-author formatting describe-object cardVisual accentBar
powerbi-report-author formatting describe-object cardVisual outline
powerbi-report-author formatting describe-object cardVisual referenceLabel
# Search across all objects for a property
powerbi-report-author formatting search cardVisual "padding|margin"
```
---
## References
- [formatting.md § Selectors](formatting.md#selectors-targeting-specific-data) — id selector pattern
- [theming.md § Visual Styles](theming.md#6-visual-styles-visualstyles) — theme defaults
references/cartesian.md
# Cartesian Visuals (Bar, Column, Line)
> Examples use illustrative `<table>.<measure>` identifiers — substitute your own.
<!-- TOC -->
- [Visual Type Families](#visual-type-families)
- [Column/Bar Family](#columnbar-family)
- [Line Family](#line-family)
- [Roles & Cardinality](#roles--cardinality)
- [Query Patterns](#query-patterns)
- [Y Binding — Measure vs Aggregation](#y-binding--measure-vs-aggregation)
- [Multiple Y Measures](#multiple-y-measures)
- [Category Drill Hierarchy](#category-drill-hierarchy)
- [Date Hierarchy Binding](#date-hierarchy-binding)
- [Sort Definition](#sort-definition)
- [Formatting Patterns](#formatting-patterns)
- [Per-Series Metadata Selector Pattern](#per-series-metadata-selector-pattern)
- [dataPoint — Color Assignment](#datapoint--color-assignment)
- [labels — Data Labels](#labels--data-labels)
- [legend](#legend)
- [categoryAxis / valueAxis](#categoryaxis--valueaxis)
- [Invert Axis (`invertAxis`)](#invert-axis-invertaxis)
- [Log Scale (`logAxisScale`)](#log-scale-logaxisscale)
- [layout — Gap & Series Order](#layout--gap--series-order)
- [ribbonBands — Stacked Charts](#ribbonbands--stacked-charts)
- [totals — Stacked Charts](#totals--stacked-charts)
- [zoom — Slider Controls](#zoom--slider-controls)
- [lineStyles — Line Specific](#linestyles--line-specific)
- [markers — Marker Styling](#markers--marker-styling)
- [seriesLabels — End-of-Line Labels](#serieslabels--end-of-line-labels)
- [y2Axis — Secondary Axis](#y2axis--secondary-axis)
- [smallMultiplesLayout — Rows Role](#smallmultipleslayout--rows-role)
- [Minimal Examples](#minimal-examples)
- [Bar Chart (Minimal)](#bar-chart-minimal)
- [Clustered Bar Chart (with Per-Series Color)](#clustered-bar-chart-with-per-series-color)
- [Complete Examples](#complete-examples)
- [Clustered Bar Chart with Per-Measure Colors](#clustered-bar-chart-with-per-measure-colors)
- [Stacked Column Chart with Ribbons and Totals](#stacked-column-chart-with-ribbons-and-totals)
- [Line Chart with Y2 Secondary Axis and Per-Series Styling](#line-chart-with-y2-secondary-axis-and-per-series-styling)
<!-- /TOC -->
Use these visual types for axis-based charts that plot data against category
and value axes. All share the `Category` + `Y` role pattern but differ in
orientation, stacking, and line/marker support.
> **Schema version rule:** Copy the `$schema` URL from an existing `visual.json` in the same report.
> If no reference exists, fall back to `https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json`.
> When editing existing visuals, **preserve the existing schema version** —
> do not upgrade unless the task explicitly requires it.
## Visual Type Families
### Column/Bar Family
| PBIR `visualType` | Orientation | Stacking |
|---|---|---|
| `columnChart` | Vertical | Stacked |
| `barChart` | Horizontal | Stacked |
| `clusteredColumnChart` | Vertical | Clustered (side-by-side) |
| `clusteredBarChart` | Horizontal | Clustered (side-by-side) |
### Line Family
| PBIR `visualType` | Fill |
|---|---|
| `lineChart` | Line only |
## Roles & Cardinality
Run `powerbi-report-author catalog describe <type>` to discover the exact
roles, display names, kind (Grouping/Measure), and cardinality for each visual
type:
```bash
powerbi-report-author catalog describe barChart
powerbi-report-author catalog describe lineChart
```
Example output for `catalog describe lineChart`:
```json
{
"requiredRoles": ["Category", "Y"],
"optionalRoles": ["Series", "Y2", "Rows", "Tooltips"],
"maxPerRole": { "Series": 1 },
"roles": {
"Category": { "displayName": "Axis", "kind": "Grouping" },
"Series": { "displayName": "Legend", "kind": "Grouping" },
"Y": { "displayName": "Values", "kind": "Measure" },
"Y2": { "displayName": "Secondary values", "kind": "Measure" },
"Rows": { "displayName": "Small multiples", "kind": "Grouping" },
"Tooltips": { "displayName": "Tooltips", "kind": "Measure" }
},
"formattingObjects": [
"categoryAxis", "dataPoint", "labels", "legend", "lineStyles",
"markers", "plotArea", "seriesLabels", "smallMultiplesLayout",
"valueAxis", "y2Axis", "zoom", ...
]
}
```
**Key differences:** Line charts have `Y2` (secondary axis) but no
`Gradient`. Column/bar charts have `Gradient` but no `Y2`.
## Query Patterns
### Y Binding — Measure vs Aggregation
The `Y` role accepts both expression types:
- **`Measure`** — for authored semantic-model measures (DAX). Use when the
field is a measure defined in TMDL:
```json
"field": {
"Measure": {
"Expression": { "SourceRef": { "Entity": "<Table>" } },
"Property": "<MeasureName>"
}
}
```
`queryRef`: `"<Table>.<Measure>"`, `nativeQueryRef`: `"<Measure>"`
- **`Aggregation`** — for raw columns with an aggregation function. Use when
aggregating a column directly (Sum, Avg, Count, etc.):
```json
"field": {
"Aggregation": {
"Expression": {
"Column": {
"Expression": { "SourceRef": { "Entity": "<Table>" } },
"Property": "<Column>"
}
},
"Function": 0
}
}
```
`queryRef`: `"Sum(<Table>.<Column>)"`, `nativeQueryRef`: `"Sum of <Column>"`
Aggregation Function values: `0`=Sum, `1`=Avg, `2`=Count, `3`=Min, `4`=Max,
`5`=CountNonNull, `6`=Median, `7`=StandardDeviation, `8`=Variance
> **⚠️ nativeQueryRef format:** Aggregation projections require
> `"Sum of <Column>"` (not just `"<Column>"`). Using the raw column name
> causes blank visuals with no error. See `references/expressions.md` for details.
### Multiple Y Measures
There are two ways to get multiple series (lines/bars) in a chart:
1. **Series role** — put a grouping column (e.g., `Sub-Category`) in the
`Series` role. Power BI splits one measure into multiple series based on
the column's distinct data values. Each unique value becomes a separate
line/bar color, and the legend should mirror that series identity.
2. **Multiple Y projections** — add multiple measures (e.g., Sales, Profit)
to the `Y` role. Each measure becomes its own series. No `Series` role
needed.
> **Clustered bar/column rule:** if you want distinct colors for each bar
> group, use per-series `dataPoint.fill` selectors or let the theme `dataColors`
> palette assign series colors. Do **not** use `defaultColor` on clustered
> charts — it forces every bar and legend entry to the same color.
To use approach 2, add multiple projections to `Y`:
```json
"Y": {
"projections": [
{
"field": { "Aggregation": { "Expression": { "Column": { "Expression": { "SourceRef": { "Entity": "Sales" } }, "Property": "Revenue" } }, "Function": 0 } },
"queryRef": "Sum(Sales.Revenue)",
"nativeQueryRef": "Sum of Revenue"
},
{
"field": { "Aggregation": { "Expression": { "Column": { "Expression": { "SourceRef": { "Entity": "Sales" } }, "Property": "Cost" } }, "Function": 0 } },
"queryRef": "Sum(Sales.Cost)",
"nativeQueryRef": "Sum of Cost"
}
]
}
```
### Category Drill Hierarchy
When multiple fields are added to the `Category` role (e.g., Year → Quarter →
Month), they form a **drill hierarchy**. The chart initially shows only the
top level. Users can then drill down through the levels interactively.
The `active` property controls which levels are **currently visible** when
the report loads — it saves the drill state:
- **Top level only** (default): set `active: true` on the first projection only
- **Drilled down to a level**: set `active: true` on all levels up to and
including the visible level
```json
"Category": {
"projections": [
{
"field": { "Column": { "Expression": { "SourceRef": { "Entity": "Product" } }, "Property": "Category" } },
"queryRef": "Product.Category",
"nativeQueryRef": "Category",
"active": true
},
{
"field": { "Column": { "Expression": { "SourceRef": { "Entity": "Product" } }, "Property": "SubCategory" } },
"queryRef": "Product.SubCategory",
"nativeQueryRef": "SubCategory",
"active": true
}
]
}
```
In this example both levels have `active: true`, so the chart loads showing
data drilled down to SubCategory.
> **Cartesian charts only.** The `active` property is specific to drill
> hierarchies on cartesian Category projections. Do **not** set `active` on
> tableEx or pivotTable projections — it triggers drill behavior and causes
> columns to disappear (see SKILL.md anti-patterns).
### Date Hierarchy Binding
When a date column has a `variation` in the semantic model (auto date
hierarchy), the Category binding uses `PropertyVariationSource` → `Hierarchy`
→ `HierarchyLevel` nesting. Each level is a separate projection:
```json
"Category": {
"projections": [
{
"field": {
"HierarchyLevel": {
"Expression": {
"Hierarchy": {
"Expression": {
"PropertyVariationSource": {
"Expression": { "SourceRef": { "Entity": "<Table>" } },
"Name": "Variation",
"Property": "<DateColumn>"
}
},
"Hierarchy": "Date Hierarchy"
}
},
"Level": "Year"
}
},
"queryRef": "<Table>.<DateColumn>.Variation.Date Hierarchy.Year",
"nativeQueryRef": "<DateColumn> Year",
"active": true
},
{
"field": {
"HierarchyLevel": {
"Expression": {
"Hierarchy": {
"Expression": {
"PropertyVariationSource": {
"Expression": { "SourceRef": { "Entity": "<Table>" } },
"Name": "Variation",
"Property": "<DateColumn>"
}
},
"Hierarchy": "Date Hierarchy"
}
},
"Level": "Quarter"
}
},
"queryRef": "<Table>.<DateColumn>.Variation.Date Hierarchy.Quarter",
"nativeQueryRef": "<DateColumn> Quarter",
"active": false
}
]
}
```
Standard date hierarchy levels: `Year`, `Quarter`, `Month`, `Day`.
Set `active: true` on the starting drill level, `false` on deeper levels.
### Sort Definition
Add `sortDefinition` at the `query` level (sibling of `queryState`) to set
the default sort order:
```json
"query": {
"queryState": { /* ... */ },
"sortDefinition": {
"sort": [
{
"field": {
"Aggregation": {
"Expression": {
"Column": {
"Expression": { "SourceRef": { "Entity": "<Table>" } },
"Property": "<Column>"
}
},
"Function": 0
}
},
"direction": "Descending"
}
],
"isDefaultSort": true
}
}
```
Direction values: `"Ascending"` or `"Descending"`.
## Formatting Patterns
Discover formatting objects and properties with the CLI:
```bash
powerbi-report-author formatting list-objects <visualType>
powerbi-report-author formatting describe-object <visualType> <object>
powerbi-report-author formatting search <visualType> <regex>
```
> **Property names vary by visual type.** Always run
> `powerbi-report-author formatting describe-object <type> <object>` to confirm
> exact names.
### Per-Series Metadata Selector Pattern
When a chart has multiple Y measures, use metadata selectors to target
formatting to a specific measure. The selector `metadata` value **must match**
the projection's `queryRef` (not `nativeQueryRef`):
```json
"dataPoint": [
{
"properties": {
"fill": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 9, "Percent": -0.25 } } } } },
"fillTransparency": { "expr": { "Literal": { "Value": "18D" } } }
},
"selector": {
"metadata": "Sum(OrderBreakdown.Profit)"
}
}
]
```
This pattern applies to `dataPoint`, `labels`, `lineStyles`, and
`ribbonBands`. Each section below notes when metadata selectors are needed.
> **⚠️ Pitfall:** The selector `metadata` value must match the `queryRef` string
> (e.g., `"Sum(OrderBreakdown.Profit)"`), not the `nativeQueryRef`
> (e.g., `"Sum of Profit"`).
### dataPoint — Color Assignment
> **⚠️ Multi-visual pages:** When a page has multiple charts sharing the same
> measures, define a **measure→color mapping before creating any visuals** and
> apply it consistently to every chart. Without this, the same measure gets
> different colors on different visuals. See
> [color-strategy.md § Cross-Visual Measure-Color Consistency](color-strategy.md#pattern-cross-visual-measure-color-consistency)
> for the full pattern.
Use metadata selectors to set per-measure colors. **Always use `Literal` hex
values** (not `ThemeDataColor`) for explicit color assignments — `ThemeDataColor`
with metadata selectors can silently resolve to wrong colors (white, black).
Run `powerbi-report-author formatting describe-object <type> dataPoint` for
exact property names per visual type.
> **⚠️ Background contrast:** Always choose bar/line/point colors that contrast
> with the page and VCO background. If the canvas or card background is white,
> avoid light or desaturated colors. Pick saturated, mid-to-dark hues.
```json
"dataPoint": [
{
"properties": {
"fill": { "solid": { "color": { "expr": { "Literal": { "Value": "'#2E86AB'" } } } } }
},
"selector": {
"metadata": "Sum(OrderBreakdown.Sales)"
}
},
{
"properties": {
"fill": { "solid": { "color": { "expr": { "Literal": { "Value": "'#E6553A'" } } } } }
},
"selector": {
"metadata": "Sum(OrderBreakdown.Profit)"
}
}
]
```
> **Note:** Property names differ between visual types (e.g., `fillTransparency`
> on bar/column vs `transparency` on lineChart). Always verify with
> `powerbi-report-author formatting describe-object <type> dataPoint`.
### labels — Data Labels
Basic labels — enable for all series (from barChart reference visual):
```json
"labels": [
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } }
}
},
{
"properties": {
"enableTitleDataLabel": { "expr": { "Literal": { "Value": "true" } } },
"titleBold": { "expr": { "Literal": { "Value": "true" } } },
"enableBackground": { "expr": { "Literal": { "Value": "true" } } },
"backgroundColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 2, "Percent": 0.4 } } } } },
"labelContentLayout": { "expr": { "Literal": { "Value": "'MultiLine'" } } },
"horizontalAlignment": { "expr": { "Literal": { "Value": "'center'" } } }
},
"selector": {
"metadata": "Sum(OrderBreakdown.Profit)"
}
}
]
```
The first entry (no selector) enables labels globally. The second entry uses
a metadata selector to customize labels for a specific measure — adding title,
bold, background, and multi-line layout.
**Dynamic label title/detail** — bind label content to aggregation expressions
using a `dataViewWildcard` selector (from clusteredBarChart reference visual):
```json
"labels": [
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"labelPosition": { "expr": { "Literal": { "Value": "'InsideCenter'" } } },
"labelOverflow": { "expr": { "Literal": { "Value": "true" } } },
"optimizeLabelDisplay": { "expr": { "Literal": { "Value": "true" } } },
"labelContainerMaxWidth": { "expr": { "Literal": { "Value": "174D" } } },
"enableTitleDataLabel": { "expr": { "Literal": { "Value": "true" } } },
"titleContentType": { "expr": { "Literal": { "Value": "'Custom'" } } },
"enableDetailDataLabel": { "expr": { "Literal": { "Value": "true" } } },
"detailColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 2, "Percent": 0.6 } } } } },
"detailTransparency": { "expr": { "Literal": { "Value": "20D" } } },
"enableBackground": { "expr": { "Literal": { "Value": "true" } } },
"backgroundColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 2, "Percent": 0 } } } } },
"backgroundTransparency": { "expr": { "Literal": { "Value": "40D" } } },
"labelContentLayout": { "expr": { "Literal": { "Value": "'MultiLine'" } } }
}
},
{
"properties": {
"dynamicLabelTitle": {
"expr": {
"Aggregation": {
"Expression": { "Column": { "Expression": { "SourceRef": { "Entity": "ListOfOrders" } }, "Property": "State" } },
"Function": 3
}
}
},
"dynamicLabelDetail": {
"expr": {
"Aggregation": {
"Expression": { "Column": { "Expression": { "SourceRef": { "Entity": "ListOfOrders" } }, "Property": "Ship Mode" } },
"Function": 3
}
}
}
},
"selector": {
"data": [{ "dataViewWildcard": { "matchingOption": 1 } }],
"highlightMatching": 1
}
}
]
```
The `dataViewWildcard` selector with `matchingOption: 1` applies to all data
point instances. `Function: 3` is Min aggregation. Run
`powerbi-report-author formatting describe-object <type> labels` for all
available properties.
### legend
Bar/column example (from clusteredBarChart reference visual):
```json
"legend": [{
"properties": {
"position": { "expr": { "Literal": { "Value": "'TopCenter'" } } },
"titleText": { "expr": { "Literal": { "Value": "'Sales for Categories'" } } }
}
}]
```
Line chart example with marker rendering (from lineChart reference visual):
```json
"legend": [{
"properties": {
"legendMarkerRendering": { "expr": { "Literal": { "Value": "'lineAndMarker'" } } },
"labelColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 6, "Percent": -0.25 } } } } },
"titleText": { "expr": { "Literal": { "Value": "'Line Chart'" } } },
"bold": { "expr": { "Literal": { "Value": "false" } } },
"italic": { "expr": { "Literal": { "Value": "true" } } },
"underline": { "expr": { "Literal": { "Value": "true" } } }
}
}]
```
Run `powerbi-report-author formatting describe-object <type> legend` for all
available properties and valid enum values.
### categoryAxis / valueAxis
categoryAxis example (from clusteredBarChart reference visual):
```json
"categoryAxis": [{
"properties": {
"fontFamily": { "expr": { "Literal": { "Value": "'Georgia'" } } },
"labelColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": 0 } } } } },
"titleText": { "expr": { "Literal": { "Value": "'Category'" } } },
"titleColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 6, "Percent": 0 } } } } },
"innerPadding": { "expr": { "Literal": { "Value": "26L" } } },
"maxMarginFactor": { "expr": { "Literal": { "Value": "24L" } } }
}
}]
```
valueAxis example (from clusteredBarChart reference visual):
```json
"valueAxis": [{
"properties": {
"start": { "expr": { "Literal": { "Value": "0D" } } },
"labelDisplayUnits": { "expr": { "Literal": { "Value": "1000D" } } },
"labelPrecision": { "expr": { "Literal": { "Value": "2L" } } },
"gridlineStyle": { "expr": { "Literal": { "Value": "'dashed'" } } },
"gridlineColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": 0 } } } } },
"gridlineThickness": { "expr": { "Literal": { "Value": "4D" } } }
}
}]
```
Run `powerbi-report-author formatting describe-object <type> categoryAxis` and
`powerbi-report-author formatting describe-object <type> valueAxis` for all
available properties and valid enum values.
#### Invert Axis (`invertAxis`)
Use `invertAxis` to reverse the order of values on an axis.
- On **categoryAxis**: reverses the category order (e.g., alphabetical Z→A
instead of A→Z, or bottom-to-top instead of top-to-bottom on bar charts).
- On **valueAxis**: reverses the numeric direction (e.g., values grow
right-to-left instead of left-to-right on bar charts).
> **When the user asks to "invert" a bar or column chart**, apply
> `invertAxis: true` to **both** `categoryAxis` and `valueAxis` unless they
> explicitly specify only one axis. Setting `categoryAxis` alone only reorders
> the categories; setting `valueAxis` alone only flips the value direction.
> Both together fully inverts the chart.
```json
"categoryAxis": [{
"properties": {
"invertAxis": { "expr": { "Literal": { "Value": "true" } } }
}
}],
"valueAxis": [{
"properties": {
"invertAxis": { "expr": { "Literal": { "Value": "true" } } }
}
}]
```
#### Log Scale (`logAxisScale`)
> **⚠️ Confirm with the user before setting `logAxisScale: true`** if any bound
> measure or column can produce zero or negative values. Logarithms of zero or
> negative numbers are mathematically undefined. PBI Desktop silently falls
> back to linear scale with a warning:
> *"The axis changed to a linear scale to accommodate both positive and negative values."*
>
> **Workflow before enabling `logAxisScale`:**
> 1. Inspect the columns/measures bound to the axis — can they produce zero or
> negative values? (e.g., Profit, Discount, Net Change often go negative)
> 2. If negatives are possible, **warn the user before applying log scale**.
> Explain that log scale is mathematically undefined for zero/negative values
> and PBI will silently fall back to linear. Use the `ask_user` tool to
> present alternatives and let the user choose:
> - Filter out zero/negative values (visual-level or page-level filter)
> - Switch to a different column/measure that is always positive (e.g., Sales, Quantity)
> - Use a DAX measure with `ABS()` (requires semantic model change)
> - Keep linear scale with `labelDisplayUnits` for readability instead
> 3. Apply `logAxisScale: true` only after the user resolves the negative values
> via one of the alternatives above, or confirms that all values are positive.
```json
"valueAxis": [{
"properties": {
"logAxisScale": { "expr": { "Literal": { "Value": "true" } } }
}
}]
```
### layout — Gap & Series Order
Clustered charts example:
```json
"layout": [{
"properties": {
"seriesOrderSorted": { "expr": { "Literal": { "Value": "true" } } },
"seriesOrderReversed": { "expr": { "Literal": { "Value": "false" } } },
"clusteredGapSize": { "expr": { "Literal": { "Value": "16D" } } }
}
}]
```
Stacked charts (`barChart`, `columnChart`) use different properties
(e.g., `stackedGapSize` instead of `clusteredGapSize`). Run
`powerbi-report-author formatting describe-object <type> layout` to discover
available properties for each visual type.
### ribbonBands — Stacked Charts
Ribbon connectors link same-series segments across categories. Available on
`barChart` and `columnChart`.
```json
"ribbonBands": [
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } }
}
},
{
"properties": {
"fillTransparency": { "expr": { "Literal": { "Value": "7D" } } },
"borderShow": { "expr": { "Literal": { "Value": "true" } } },
"borderColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": 0.2 } } } } },
"borderSize": { "expr": { "Literal": { "Value": "4D" } } }
},
"selector": {
"metadata": "Sum(OrderBreakdown.Profit)"
}
}
]
```
First entry: static `show` toggle. Subsequent entries: per-measure styling
via metadata selectors. Run
`powerbi-report-author formatting describe-object <type> ribbonBands` for
all available properties.
### totals — Stacked Charts
Total labels on stacked bars/columns. Available on `barChart` and
`columnChart`.
Example with background and per-instance color:
```json
"totals": [
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"backgroundColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 4, "Percent": -0.5 } } } } },
"backgroundTransparency": { "expr": { "Literal": { "Value": "68D" } } }
}
},
{
"properties": {
"color": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 4, "Percent": -0.25 } } } } }
},
"selector": {
"data": [{ "dataViewWildcard": { "matchingOption": 1 } }]
}
}
]
```
Run `powerbi-report-author formatting describe-object <type> totals` for all
available properties.
### zoom — Slider Controls
Example:
```json
"zoom": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"showOnValueAxis": { "expr": { "Literal": { "Value": "true" } } },
"showLabels": { "expr": { "Literal": { "Value": "true" } } },
"showTooltip": { "expr": { "Literal": { "Value": "true" } } }
}
}]
```
Run `powerbi-report-author formatting describe-object <type> zoom` for all
available properties.
### lineStyles — Line Specific
For line charts. Set line width, style, interpolation, and markers. Use
metadata selectors for per-series styling. Run
`powerbi-report-author formatting describe-object lineChart lineStyles` for the
full property list and valid enum values.
**Visual types with `lineStyles`:** areaChart, lineChart, stackedAreaChart,
lineStackedColumnComboChart, lineClusteredColumnComboChart,
hundredPercentStackedAreaChart.
**Key properties:**
| Property | Type | Description |
|----------|------|-------------|
| `strokeShow` | bool | Show/hide the line |
| `strokeWidth` | numeric (D) | Line width in pixels |
| `strokeColor` | fill/color | Line color |
| `strokeTransparency` | numeric (D) | Transparency (0–100) |
| `lineStyle` | enum | `'solid'`, `'dashed'`, `'dotted'`, `'custom'` |
| `strokeDashCap` | enum | `'none'`, `'round'`, `'square'` |
| `strokeLineJoin` | enum | Line join style |
| `showMarker` | bool | Show data-point markers |
| `markerShape` | enum | `'circle'`, `'square'`, `'diamond'`, `'triangle'`, `'x'`, `'shortDash'`, `'longDash'`, `'plus'` |
| `markerSize` | numeric (D) | Marker size in pixels |
| `markerColor` | fill/color | Marker fill color |
| `lineChartType` | enum | Interpolation: `'linear'`, `'smooth'`, `'step'` |
```json
"lineStyles": [
{
"properties": {
"strokeWidth": { "expr": { "Literal": { "Value": "5D" } } },
"lineChartType": { "expr": { "Literal": { "Value": "'step'" } } },
"interpolationStep": { "expr": { "Literal": { "Value": "'after'" } } },
"showMarker": { "expr": { "Literal": { "Value": "true" } } },
"markerShape": { "expr": { "Literal": { "Value": "'diamond'" } } },
"markerSize": { "expr": { "Literal": { "Value": "9D" } } },
"strokeLineJoin": { "expr": { "Literal": { "Value": "'bevel'" } } }
},
"selector": {
"metadata": "Sum(OrderBreakdown.Profit)"
}
},
{
"properties": {
"lineStyle": { "expr": { "Literal": { "Value": "'dashed'" } } },
"strokeWidth": { "expr": { "Literal": { "Value": "4D" } } },
"lineChartType": { "expr": { "Literal": { "Value": "'smooth'" } } },
"interpolationSmooth": { "expr": { "Literal": { "Value": "'cardinal'" } } },
"strokeTransparency": { "expr": { "Literal": { "Value": "11D" } } }
},
"selector": {
"metadata": "Sum(OrderBreakdown.Quantity)"
}
},
{
"properties": {
"areaShow": { "expr": { "Literal": { "Value": "true" } } },
"showMarker": { "expr": { "Literal": { "Value": "true" } } }
}
}
]
```
A static entry (no selector) sets defaults for all series. Per-series entries
with metadata selectors override specific measures.
### markers — Marker Styling
Controls marker appearance independently of `lineStyles.showMarker`. Available
on all line/area visual types **plus scatterChart**.
| Property | Type | Description |
|----------|------|-------------|
| `transparency` | numeric (D) | Marker transparency (0–100) |
| `rotation` | numeric (D) | Rotation angle |
| `borderShow` | bool | Show marker border |
| `borderWidth` | numeric (D) | Border width |
| `borderColorMatchFill` | bool | Match border to fill color |
| `borderColor` | fill/color | Marker border color |
| `borderTransparency` | numeric (D) | Border transparency |
### seriesLabels — End-of-Line Labels
Labels at the end of each line series (lineChart only). Run
`powerbi-report-author formatting describe-object lineChart seriesLabels` for
all available properties.
### y2Axis — Secondary Axis
When the `Y2` role is populated (lineChart only), format the secondary axis.
Example:
```json
"y2Axis": [{
"properties": {
"secLabelColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 3, "Percent": 0.2 } } } } },
"secTitleColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": 0.2 } } } } },
"secLogAxisScale": { "expr": { "Literal": { "Value": "false" } } }
}
}]
```
> **Note:** All `y2Axis` properties are prefixed with `sec`. Run
> `powerbi-report-author formatting describe-object lineChart y2Axis` for the
> full list.
### smallMultiplesLayout — Rows Role
When the `Rows` role is populated, the chart splits into a grid of small
multiples:
```json
"smallMultiplesLayout": [{
"properties": {
"rowCount": { "expr": { "Literal": { "Value": "9L" } } },
"columnCount": { "expr": { "Literal": { "Value": "4L" } } },
"gridPadding": { "expr": { "Literal": { "Value": "2D" } } },
"gridLineWidth": { "expr": { "Literal": { "Value": "2D" } } },
"backgroundColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 4, "Percent": 0.4 } } } } }
}
}]
```
Run `powerbi-report-author formatting describe-object <type> smallMultiplesLayout`
for all available properties.
For VCO formatting (title, border, background, etc.), see
[formatting.md](formatting.md).
## Minimal Examples
### Bar Chart (Minimal)
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "a1b2c3d4e5f6a7b8c9d0",
"position": { "x": 20, "y": 20, "z": 1000, "height": 300, "width": 500, "tabOrder": 1000 },
"visual": {
"visualType": "barChart",
"query": {
"queryState": {
"Category": {
"projections": [{
"field": { "Column": { "Expression": { "SourceRef": { "Entity": "product" } }, "Property": "ProductName" } },
"queryRef": "product.ProductName",
"nativeQueryRef": "ProductName",
"active": true
}]
},
"Y": {
"projections": [{
"field": { "Measure": { "Expression": { "SourceRef": { "Entity": "metrics" } }, "Property": "Sales" } },
"queryRef": "metrics.Sales",
"nativeQueryRef": "Sales"
}]
}
}
}
}
}
```
### Clustered Bar Chart (with Per-Series Color)
Shows both `objects` (chart formatting) and `visualContainerObjects` (container formatting).
**⚠️ `visualContainerObjects` goes INSIDE `visual`, as a sibling of `objects` — NOT as a top-level sibling of `visual`.**
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "e1f2a3b4c5d6e7f8a9b0",
"position": { "x": 20, "y": 20, "z": 1000, "height": 400, "width": 600, "tabOrder": 1000 },
"visual": {
"visualType": "clusteredBarChart",
"query": {
"queryState": {
"Category": {
"projections": [{
"field": { "Column": { "Expression": { "SourceRef": { "Entity": "product" } }, "Property": "Category" } },
"queryRef": "product.Category",
"nativeQueryRef": "Category",
"active": true
}]
},
"Y": {
"projections": [{
"field": { "Measure": { "Expression": { "SourceRef": { "Entity": "metrics" } }, "Property": "Revenue" } },
"queryRef": "metrics.Revenue",
"nativeQueryRef": "Revenue"
}]
}
}
},
"objects": {
"categoryAxis": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"fontSize": { "expr": { "Literal": { "Value": "11D" } } }
}
}],
"valueAxis": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"gridlineStyle": { "expr": { "Literal": { "Value": "'dotted'" } } }
}
}],
"dataPoint": [{
"properties": {
"fill": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 0, "Percent": 0 } } } } }
}
}],
"labels": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"fontSize": { "expr": { "Literal": { "Value": "9D" } } },
"color": { "solid": { "color": { "expr": { "Literal": { "Value": "'#333333'" } } } } }
}
}]
},
"visualContainerObjects": {
"title": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"text": { "expr": { "Literal": { "Value": "'Revenue by Category'" } } },
"fontSize": { "expr": { "Literal": { "Value": "14D" } } },
"fontColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#333333'" } } } } }
}
}],
"background": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"color": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FFFFFF'" } } } } },
"transparency": { "expr": { "Literal": { "Value": "0D" } } }
}
}],
"border": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"color": { "solid": { "color": { "expr": { "Literal": { "Value": "'#E0E0E0'" } } } } },
"radius": { "expr": { "Literal": { "Value": "5D" } } }
}
}],
"dropShadow": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"preset": { "expr": { "Literal": { "Value": "'BottomRight'" } } },
"color": { "solid": { "color": { "expr": { "Literal": { "Value": "'#000000'" } } } } },
"transparency": { "expr": { "Literal": { "Value": "80D" } } },
"position": { "expr": { "Literal": { "Value": "'Outer'" } } }
}
}],
"visualHeader": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "false" } } }
}
}],
"padding": [{
"properties": {
"top": { "expr": { "Literal": { "Value": "5D" } } },
"bottom": { "expr": { "Literal": { "Value": "5D" } } },
"left": { "expr": { "Literal": { "Value": "5D" } } },
"right": { "expr": { "Literal": { "Value": "5D" } } }
}
}]
}
}
}
```
## Complete Examples
### Clustered Bar Chart with Per-Measure Colors
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 20, "y": 20, "z": 0, "height": 700, "width": 1000, "tabOrder": 0 },
"visual": {
"visualType": "clusteredBarChart",
"query": {
"queryState": {
"Category": {
"projections": [{
"field": { "Column": { "Expression": { "SourceRef": { "Entity": "OrderBreakdown" } }, "Property": "Category" } },
"queryRef": "OrderBreakdown.Category",
"nativeQueryRef": "Category",
"active": true
}]
},
"Y": {
"projections": [
{
"field": { "Aggregation": { "Expression": { "Column": { "Expression": { "SourceRef": { "Entity": "OrderBreakdown" } }, "Property": "Profit" } }, "Function": 0 } },
"queryRef": "Sum(OrderBreakdown.Profit)",
"nativeQueryRef": "Sum of Profit"
},
{
"field": { "Aggregation": { "Expression": { "Column": { "Expression": { "SourceRef": { "Entity": "OrderBreakdown" } }, "Property": "Sales" } }, "Function": 0 } },
"queryRef": "Sum(OrderBreakdown.Sales)",
"nativeQueryRef": "Sum of Sales"
}
]
}
},
"sortDefinition": {
"sort": [{
"field": { "Aggregation": { "Expression": { "Column": { "Expression": { "SourceRef": { "Entity": "OrderBreakdown" } }, "Property": "Profit" } }, "Function": 0 } },
"direction": "Descending"
}],
"isDefaultSort": true
}
},
"objects": {
"categoryAxis": [{
"properties": {
"fontFamily": { "expr": { "Literal": { "Value": "'Georgia'" } } },
"labelColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": 0 } } } } },
"titleText": { "expr": { "Literal": { "Value": "'Category'" } } },
"titleColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 6, "Percent": 0 } } } } },
"innerPadding": { "expr": { "Literal": { "Value": "26L" } } }
}
}],
"valueAxis": [{
"properties": {
"start": { "expr": { "Literal": { "Value": "0D" } } },
"labelDisplayUnits": { "expr": { "Literal": { "Value": "1000D" } } },
"labelPrecision": { "expr": { "Literal": { "Value": "2L" } } },
"gridlineStyle": { "expr": { "Literal": { "Value": "'dashed'" } } },
"gridlineColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": 0 } } } } },
"gridlineThickness": { "expr": { "Literal": { "Value": "4D" } } }
}
}],
"legend": [{
"properties": {
"position": { "expr": { "Literal": { "Value": "'TopCenter'" } } },
"titleText": { "expr": { "Literal": { "Value": "'Sales for Categories'" } } }
}
}],
"zoom": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"showLabels": { "expr": { "Literal": { "Value": "true" } } },
"showOnValueAxis": { "expr": { "Literal": { "Value": "true" } } }
}
}],
"dataPoint": [{
"properties": {
"fill": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 9, "Percent": -0.25 } } } } },
"fillTransparency": { "expr": { "Literal": { "Value": "18D" } } },
"borderShow": { "expr": { "Literal": { "Value": "true" } } },
"borderColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": 0.2 } } } } },
"borderSize": { "expr": { "Literal": { "Value": "7D" } } }
},
"selector": { "metadata": "Sum(OrderBreakdown.Profit)" }
}],
"layout": [{
"properties": {
"seriesOrderSorted": { "expr": { "Literal": { "Value": "true" } } },
"clusteredGapSize": { "expr": { "Literal": { "Value": "16D" } } }
}
}],
"labels": [
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"labelPosition": { "expr": { "Literal": { "Value": "'InsideCenter'" } } },
"enableTitleDataLabel": { "expr": { "Literal": { "Value": "true" } } },
"enableBackground": { "expr": { "Literal": { "Value": "true" } } },
"labelContentLayout": { "expr": { "Literal": { "Value": "'MultiLine'" } } }
}
}
]
},
"visualContainerObjects": {
"title": [{
"properties": {
"text": { "expr": { "Literal": { "Value": "'Profit and Sum of Sales by Category'" } } },
"fontColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 7, "Percent": -0.25 } } } } },
"background": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": 0.6 } } } } },
"alignment": { "expr": { "Literal": { "Value": "'center'" } } }
}
}],
"border": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"color": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 8, "Percent": 0.4 } } } } },
"width": { "expr": { "Literal": { "Value": "3D" } } },
"radius": { "expr": { "Literal": { "Value": "5D" } } }
}
}]
},
"drillFilterOtherVisuals": true
}
}
```
### Stacked Column Chart with Ribbons and Totals
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "972da330c2a28c851c90",
"position": {
"x": 198.91,
"y": 51.22,
"z": 0,
"height": 668.20,
"width": 1006.46,
"tabOrder": 0
},
"visual": {
"visualType": "columnChart",
"query": {
"queryState": {
"Category": {
"projections": [
{
"field": { "Column": { "Expression": { "SourceRef": { "Entity": "OrderBreakdown" } }, "Property": "Category" } },
"queryRef": "OrderBreakdown.Category",
"nativeQueryRef": "Category",
"active": true
}
]
},
"Y": {
"projections": [
{
"field": { "Aggregation": { "Expression": { "Column": { "Expression": { "SourceRef": { "Entity": "OrderBreakdown" } }, "Property": "Discount" } }, "Function": 0 } },
"queryRef": "Sum(OrderBreakdown.Discount)",
"nativeQueryRef": "Sum of Discount"
},
{
"field": { "Aggregation": { "Expression": { "Column": { "Expression": { "SourceRef": { "Entity": "OrderBreakdown" } }, "Property": "Product Name" } }, "Function": 5 } },
"queryRef": "CountNonNull(OrderBreakdown.Product Name)",
"nativeQueryRef": "Product Name"
}
]
}
},
"sortDefinition": {
"sort": [
{
"field": { "Aggregation": { "Expression": { "Column": { "Expression": { "SourceRef": { "Entity": "OrderBreakdown" } }, "Property": "Discount" } }, "Function": 0 } },
"direction": "Descending"
}
],
"isDefaultSort": true
}
},
"objects": {
"categoryAxis": [
{
"properties": {
"labelColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": 0.2 } } } } },
"maxMarginFactor": { "expr": { "Literal": { "Value": "29L" } } },
"fontSize": { "expr": { "Literal": { "Value": "11D" } } },
"titleColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 3, "Percent": -0.25 } } } } },
"concatenateLabels": { "expr": { "Literal": { "Value": "false" } } },
"titleBold": { "expr": { "Literal": { "Value": "true" } } },
"titleUnderline": { "expr": { "Literal": { "Value": "true" } } },
"preferredCategoryWidth": { "expr": { "Literal": { "Value": "35D" } } }
}
}
],
"valueAxis": [
{
"properties": {
"start": { "expr": { "Literal": { "Value": "0D" } } },
"invertAxis": { "expr": { "Literal": { "Value": "true" } } },
"end": { "expr": { "Literal": { "Value": "10000D" } } },
"labelDisplayUnits": { "expr": { "Literal": { "Value": "1000D" } } },
"labelPrecision": { "expr": { "Literal": { "Value": "1L" } } },
"titleColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": 0.4 } } } } },
"gridlineColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 6, "Percent": 0.4 } } } } },
"gridlineStyle": { "expr": { "Literal": { "Value": "'solid'" } } },
"gridlineThickness": { "expr": { "Literal": { "Value": "2D" } } }
}
}
],
"legend": [
{
"properties": {
"position": { "expr": { "Literal": { "Value": "'TopCenter'" } } },
"labelColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 4, "Percent": -0.25 } } } } },
"showTitle": { "expr": { "Literal": { "Value": "true" } } }
}
}
],
"dataPoint": [
{
"properties": {
"fillTransparency": { "expr": { "Literal": { "Value": "0D" } } }
}
},
{
"properties": {
"fill": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 4, "Percent": -0.25 } } } } },
"borderShow": { "expr": { "Literal": { "Value": "true" } } },
"borderColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 7, "Percent": 0.2 } } } } },
"borderSize": { "expr": { "Literal": { "Value": "3D" } } }
},
"selector": { "metadata": "Sum(OrderBreakdown.Discount)" }
},
{
"properties": {
"fill": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 8, "Percent": -0.25 } } } } },
"borderShow": { "expr": { "Literal": { "Value": "true" } } },
"borderColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": -0.5 } } } } },
"borderSize": { "expr": { "Literal": { "Value": "5D" } } }
},
"selector": { "metadata": "CountNonNull(OrderBreakdown.Product Name)" }
}
],
"ribbonBands": [
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } }
}
},
{
"properties": {
"fillTransparency": { "expr": { "Literal": { "Value": "35D" } } }
},
"selector": { "metadata": "Sum(OrderBreakdown.Discount)" }
},
{
"properties": {
"fillTransparency": { "expr": { "Literal": { "Value": "44D" } } },
"borderShow": { "expr": { "Literal": { "Value": "true" } } },
"borderColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 4, "Percent": -0.5 } } } } },
"borderSize": { "expr": { "Literal": { "Value": "2D" } } },
"borderTransparency": { "expr": { "Literal": { "Value": "59D" } } }
},
"selector": { "metadata": "CountNonNull(OrderBreakdown.Product Name)" }
}
],
"labels": [
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"labelOrientation": { "expr": { "Literal": { "Value": "1D" } } },
"labelOverflow": { "expr": { "Literal": { "Value": "false" } } },
"optimizeLabelDisplay": { "expr": { "Literal": { "Value": "true" } } },
"enableTitleDataLabel": { "expr": { "Literal": { "Value": "true" } } },
"enableBackground": { "expr": { "Literal": { "Value": "true" } } },
"backgroundColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 4, "Percent": 0.6 } } } } },
"horizontalAlignment": { "expr": { "Literal": { "Value": "'center'" } } }
}
}
],
"totals": [
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"backgroundColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 4, "Percent": -0.5 } } } } },
"backgroundTransparency": { "expr": { "Literal": { "Value": "68D" } } }
}
},
{
"properties": {
"color": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 4, "Percent": -0.25 } } } } }
},
"selector": {
"data": [{ "dataViewWildcard": { "matchingOption": 1 } }]
}
}
],
"zoom": [
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"showLabels": { "expr": { "Literal": { "Value": "false" } } }
}
}
]
}
}
}
```
### Line Chart with Y2 Secondary Axis and Per-Series Styling
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 705, "y": 0, "z": 1, "height": 591, "width": 575, "tabOrder": 1 },
"visual": {
"visualType": "lineChart",
"query": {
"queryState": {
"Category": {
"projections": [
{
"field": {
"HierarchyLevel": {
"Expression": {
"Hierarchy": {
"Expression": {
"PropertyVariationSource": {
"Expression": { "SourceRef": { "Entity": "ListOfOrders" } },
"Name": "Variation",
"Property": "Ship Date"
}
},
"Hierarchy": "Date Hierarchy"
}
},
"Level": "Year"
}
},
"queryRef": "ListOfOrders.Ship Date.Variation.Date Hierarchy.Year",
"nativeQueryRef": "Ship Date Year",
"active": true
},
{
"field": {
"HierarchyLevel": {
"Expression": {
"Hierarchy": {
"Expression": {
"PropertyVariationSource": {
"Expression": { "SourceRef": { "Entity": "ListOfOrders" } },
"Name": "Variation",
"Property": "Ship Date"
}
},
"Hierarchy": "Date Hierarchy"
}
},
"Level": "Quarter"
}
},
"queryRef": "ListOfOrders.Ship Date.Variation.Date Hierarchy.Quarter",
"nativeQueryRef": "Ship Date Quarter",
"active": false
},
{
"field": {
"HierarchyLevel": {
"Expression": {
"Hierarchy": {
"Expression": {
"PropertyVariationSource": {
"Expression": { "SourceRef": { "Entity": "ListOfOrders" } },
"Name": "Variation",
"Property": "Ship Date"
}
},
"Hierarchy": "Date Hierarchy"
}
},
"Level": "Month"
}
},
"queryRef": "ListOfOrders.Ship Date.Variation.Date Hierarchy.Month",
"nativeQueryRef": "Ship Date Month",
"active": false
},
{
"field": {
"HierarchyLevel": {
"Expression": {
"Hierarchy": {
"Expression": {
"PropertyVariationSource": {
"Expression": { "SourceRef": { "Entity": "ListOfOrders" } },
"Name": "Variation",
"Property": "Ship Date"
}
},
"Hierarchy": "Date Hierarchy"
}
},
"Level": "Day"
}
},
"queryRef": "ListOfOrders.Ship Date.Variation.Date Hierarchy.Day",
"nativeQueryRef": "Ship Date Day",
"active": false
}
]
},
"Y": {
"projections": [{
"field": { "Aggregation": { "Expression": { "Column": { "Expression": { "SourceRef": { "Entity": "OrderBreakdown" } }, "Property": "Profit" } }, "Function": 0 } },
"queryRef": "Sum(OrderBreakdown.Profit)",
"nativeQueryRef": "Sum of Profit"
}]
},
"Y2": {
"projections": [{
"field": { "Aggregation": { "Expression": { "Column": { "Expression": { "SourceRef": { "Entity": "OrderBreakdown" } }, "Property": "Quantity" } }, "Function": 0 } },
"queryRef": "Sum(OrderBreakdown.Quantity)",
"nativeQueryRef": "Sum of Quantity"
}]
}
}
},
"objects": {
"categoryAxis": [{
"properties": {
"axisType": { "expr": { "Literal": { "Value": "'Categorical'" } } },
"titleColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 4, "Percent": -0.25 } } } } },
"labelColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 3, "Percent": 0.2 } } } } }
}
}],
"valueAxis": [{
"properties": {
"logAxisScale": { "expr": { "Literal": { "Value": "true" } } },
"invertAxis": { "expr": { "Literal": { "Value": "true" } } },
"labelDisplayUnits": { "expr": { "Literal": { "Value": "1000D" } } },
"gridlineStyle": { "expr": { "Literal": { "Value": "'custom'" } } },
"gridlineDashArray": { "expr": { "Literal": { "Value": "'5 5 0 10 20'" } } },
"gridlineThickness": { "expr": { "Literal": { "Value": "2D" } } }
}
}],
"y2Axis": [{
"properties": {
"secLabelColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 3, "Percent": 0.2 } } } } },
"secTitleColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": 0.2 } } } } }
}
}],
"lineStyles": [
{
"properties": {
"strokeWidth": { "expr": { "Literal": { "Value": "5D" } } },
"lineChartType": { "expr": { "Literal": { "Value": "'step'" } } },
"interpolationStep": { "expr": { "Literal": { "Value": "'after'" } } },
"showMarker": { "expr": { "Literal": { "Value": "true" } } },
"markerShape": { "expr": { "Literal": { "Value": "'diamond'" } } },
"markerSize": { "expr": { "Literal": { "Value": "9D" } } }
},
"selector": { "metadata": "Sum(OrderBreakdown.Profit)" }
},
{
"properties": {
"lineStyle": { "expr": { "Literal": { "Value": "'dashed'" } } },
"strokeWidth": { "expr": { "Literal": { "Value": "4D" } } },
"lineChartType": { "expr": { "Literal": { "Value": "'smooth'" } } },
"interpolationSmooth": { "expr": { "Literal": { "Value": "'cardinal'" } } }
},
"selector": { "metadata": "Sum(OrderBreakdown.Quantity)" }
},
{
"properties": {
"areaShow": { "expr": { "Literal": { "Value": "true" } } },
"showMarker": { "expr": { "Literal": { "Value": "true" } } }
}
}
],
"dataPoint": [
{
"properties": {
"fill": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": 0.2 } } } } },
"transparency": { "expr": { "Literal": { "Value": "76D" } } }
},
"selector": { "metadata": "Sum(OrderBreakdown.Profit)" }
},
{
"properties": {
"transparency": { "expr": { "Literal": { "Value": "59D" } } }
},
"selector": { "metadata": "Sum(OrderBreakdown.Quantity)" }
}
],
"legend": [{
"properties": {
"legendMarkerRendering": { "expr": { "Literal": { "Value": "'lineAndMarker'" } } },
"labelColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 6, "Percent": -0.25 } } } } },
"titleText": { "expr": { "Literal": { "Value": "'Line Chart'" } } }
}
}],
"zoom": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"showOnValueSecAxis": { "expr": { "Literal": { "Value": "true" } } }
}
}]
},
"drillFilterOtherVisuals": true
}
}
```
references/color-strategy.md
# Color Strategy & Patterns
How to apply color overrides on chart data points, when each pattern is safe,
and how to keep the same measure the same hue across multiple visuals.
> **Prerequisite reading:** [`formatting.md` § Selectors](formatting.md#selectors-targeting-specific-data)
> — the dual-entry pattern, metadata selectors, and selector precedence are
> assumed throughout. For palette authoring, see
> [`theming.md` § Data Colors](theming.md#1-data-colors-datacolors).
> Examples use `financials.Revenue` / `financials.Profit` etc. as concrete `queryRef` values — substitute your own `<table>.<measure>` identities.
## Contents
- [Color Strategy Quick Reference](#color-strategy-quick-reference)
- [Pattern: Per-Series Colors](#pattern-per-series-colors)
- [Pattern: Single-Series Default Color](#pattern-single-series-default-color)
- [Pattern: Cross-Visual Measure-Color Consistency](#pattern-cross-visual-measure-color-consistency)
- [Pattern: Different Formatting for Totals vs Data](#pattern-different-formatting-for-totals-vs-data)
## Color Strategy Quick Reference
When the user asks to change chart/bar/series colors, choose the right approach
based on **scope** and **series count**:
| User intent | Approach | Where to edit |
|-------------|----------|---------------|
| **Change the color palette across all visuals** | Update the theme's `dataColors` palette | `theme.json` — see [theming.md § Data Colors](theming.md#1-data-colors-datacolors) |
| **Same measure = same color across all visuals** | Maintain a measure→color mapping; apply explicit `dataPoint.fill`/`defaultColor` per visual | `visual.json → visual.objects.dataPoint` per visual — see [Cross-Visual Measure-Color Consistency](#pattern-cross-visual-measure-color-consistency) below |
| **Change color for a single-series chart** (one measure, no Series role) | `dataPoint.defaultColor` | `visual.json → visual.objects.dataPoint` |
| **Override specific series colors** on one visual | `dataPoint.fill` with `metadata` selectors per series | `visual.json → visual.objects.dataPoint` |
| **Change all colors AND keep series differentiation** | Update theme `dataColors` — do **NOT** use `defaultColor` | `theme.json` |
> **⚠️ Critical:** `defaultColor` applies a **single uniform color** to every
> bar/column/point in the visual — all series and categories become the same
> color, destroying differentiation. **Never use `defaultColor` on charts with
> a Series role or multiple Y measures.**
>
> For clustered bar/column/combo charts, each legend entry should map to its
> own `dataPoint.fill` selector. If all bars and the legend collapse to one
> color, you likely used `defaultColor` or missed a per-series selector.
>
> **⚠️ Palette vs identity:** Theme `dataColors` sets the **palette** but
> assigns colors by **index position** within each visual's projection order,
> not by measure identity. If Visual A binds `[MeasureA, MeasureB]` and
> Visual B binds `[MeasureB, MeasureA]`, `MeasureA` gets `dataColors[0]` in A
> but `dataColors[1]` in B — different colors for the same measure. For
> identity-level consistency, use explicit per-measure color overrides.
## Pattern: Per-Series Colors
```json
"dataPoint": [
{
"properties": {
"fill": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 0, "Percent": 0 } } } } }
}
},
{
"properties": {
"fill": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FF6B35'" } } } } }
},
"selector": { "metadata": "financials.Revenue" }
},
{
"properties": {
"fill": { "solid": { "color": { "expr": { "Literal": { "Value": "'#00A878'" } } } } }
},
"selector": { "metadata": "financials.Profit" }
}
]
```
> **Compatibility:** Per-series `dataPoint.fill` with `metadata` selectors
> works on cartesian charts (bar, column, line, area, combo). For non-cartesian
> visuals (pie, donut, scatter, treemap, funnel), `metadata` selectors are
> silently ignored — use `scopeId` selectors instead to color individual data
> points by category value. Use `powerbi-report-author formatting list-objects
> <type>` to confirm `dataPoint` is listed before adding color overrides.
## Pattern: Single-Series Default Color
When a chart has only one measure in the Y axis **and no Series role** (e.g.,
Revenue by Month with a single color), use `defaultColor` instead of `fill`
with a metadata selector. This overrides the theme's auto-assigned color for
all data points in the series:
```json
"dataPoint": [
{
"properties": {
"defaultColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FF6B35'" } } } } }
}
}
]
```
For multi-series charts (multiple measures in Y, or a Series grouping role),
use per-measure `fill` with `metadata` selectors as shown in the Per-Series
Colors pattern above, or update the theme `dataColors` palette for consistent
cross-visual coloring.
> Use `powerbi-report-author formatting describe-object <visualType> dataPoint`
> to confirm `defaultColor` is available for a given visual type.
> **⚠️ `fill` vs `defaultColor` trap:** `dataPoint.fill` without a selector
> causes bars/columns to be invisible (data is there — tooltips work — but
> nothing renders). For single-series charts, always use `defaultColor`.
> Only use `fill` with a `metadata` selector for per-series coloring.
## Pattern: Cross-Visual Measure-Color Consistency
When a report has multiple visuals that share the same measures, **always
assign consistent colors so the same measure gets the same hue everywhere.**
Theme `dataColors` alone cannot guarantee this because it assigns colors by
index position (see [Color Strategy Quick Reference](#color-strategy-quick-reference)
above).
**Rule:** Before creating visuals, define a **measure→color mapping** keyed by
`queryRef` (the metadata identity used in selectors), and apply it to every
visual that references those measures.
**Step 1 — Define the mapping** (plan this before writing visual JSON).
For each measure that appears across multiple visuals, assign one color.
**Key by `queryRef` / metadata identity** (e.g. `financials.Revenue`), not by
display name — `queryRef` is what `metadata` selectors match on.
**Example mapping** (substitute your own measures):
| Measure (`queryRef`) | Color | Notes |
|----------------------|-------|-------|
| `financials.Revenue` | `#118DFF` | Theme `dataColors[0]` |
| `financials.Profit` | `#E66C37` | Theme `dataColors[2]` |
| `financials.Cost` | `#D64554` | Theme `dataColors[7]` |
**Step 2 — Apply to multi-measure charts** using `dataPoint.fill` with
`metadata` selectors. **Always use `Literal` hex values** — `ThemeDataColor`
with metadata selectors can silently resolve to wrong colors (white, black):
> **⚠️ Background contrast:** Ensure chosen colors are saturated and contrast
> with the page/VCO background. On white backgrounds, avoid light or
> desaturated hues — pick mid-to-dark saturated colors.
```json
"dataPoint": [
{
"properties": {
"fill": { "solid": { "color": { "expr": { "Literal": { "Value": "'#118DFF'" } } } } }
},
"selector": { "metadata": "financials.Revenue" }
},
{
"properties": {
"fill": { "solid": { "color": { "expr": { "Literal": { "Value": "'#E66C37'" } } } } }
},
"selector": { "metadata": "financials.Profit" }
}
]
```
**Step 3 — Apply to single-measure charts** using `dataPoint.defaultColor`:
```json
"dataPoint": [
{
"properties": {
"defaultColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#118DFF'" } } } } }
}
}
]
```
> **Non-cartesian charts (pie, donut, scatter, treemap, funnel):**
> `dataPoint.fill` with `metadata` selectors is silently ignored on these
> visual types — they fall back to theme colors. Use `scopeId` selectors
> (per-category value) instead to color individual slices, bubbles, or segments.
## Pattern: Different Formatting for Totals vs Data
```json
"labels": [
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } }
}
},
{
"properties": {
"color": { "solid": { "color": { "expr": { "Literal": { "Value": "'#333333'" } } } } }
},
"selector": { "data": [{ "dataViewWildcard": { "matchingOption": 1 } }] }
},
{
"properties": {
"color": { "solid": { "color": { "expr": { "Literal": { "Value": "'#000000'" } } } } },
"bold": { "expr": { "Literal": { "Value": "true" } } }
},
"selector": { "data": [{ "dataViewWildcard": { "matchingOption": 2 } }] }
}
]
```
references/conditional-formatting.md
# Conditional Formatting Patterns
Read this when applying data-driven visual formatting in PBIR. Conditional
formatting is supported on chart `dataPoint` properties (charts) and on table /
matrix `values` cells, but **not** on container objects like axes, legends, or
visual containers.
Related references:
- [`formatting.md`](formatting.md) — value encoding, selectors, VCOs.
- [`formatting-overview.md`](formatting-overview.md) — cascade and encoding.
- [`table.md`](table.md) — table/matrix authoring and style presets.
> Examples use illustrative `<table>.<measure>` identifiers — substitute your own.
## Contents
- [Selector summary by visual type](#selector-summary-by-visual-type)
- [Type 1: Color Gradient (FillRule)](#type-1-color-gradient-fillrule)
- [Type 2: Rules-Based Formatting](#type-2-rules-based-formatting)
- [Type 3: Icon Sets](#type-3-icon-sets)
- [Type 4: Data Bars](#type-4-data-bars)
- [Type 5: Web URL](#type-5-web-url)
- [Type 6: Field-Driven Color](#type-6-field-driven-color)
## Selector summary by visual type
| Visual type | CF type | Object/property | Selector |
|-------------|---------|-----------------|----------|
| Tables/matrices | Data bars | `columnFormatting.dataBars` | `metadata` only |
| Tables/matrices | Background / font color | `values.backColor` / `values.fontColor` | `dataViewWildcard + metadata` |
| Tables/matrices | Icons | `values.icon` | `dataViewWildcard + metadata` |
| Charts | Gradient / rules / field color | `dataPoint.fill` | No selector, or `dataViewWildcard` only (do NOT include `metadata`) |
> This table covers **value-driven conditional formatting**. **Static** per-series
> color (coloring a specific series a fixed hue) uses a `metadata` selector
> instead — see [color-strategy.md § Per-Series Colors](color-strategy.md#pattern-per-series-colors).
## Type 1: Color Gradient (FillRule)
Applies data-driven color gradients. Uses `linearGradient2` (2-stop) or `linearGradient3` (3-stop).
> ⚠️ **Do not omit `mid` from `linearGradient3`.** A `linearGradient3` rule must
> include all three stops: `min`, `mid`, and `max`. If you only need two stops,
> use `linearGradient2`; deleting `mid` from `linearGradient3` can cause Desktop
> render errors or a blank table/matrix body.
### Choose gradient colors by measure meaning
Do **not** default to red/white/green for every numeric measure. Pick the color
scale based on what the measure means:
| Measure meaning | Use | Example measures | Color pattern |
|-----------------|-----|------------------|---------------|
| **Magnitude**: "how much?", "more vs less" | Single-hue `linearGradient2` | Sales, Revenue, Units, Gross Margin %, Count, COGS | Light tint of one theme `dataColors[N]` → base/saturated theme color |
| **Sentiment / variance**: "good vs bad?", negative vs positive, performance vs target | Divergent `linearGradient3` | Profit variance, MoM %, YoY %, vs target, budget variance | Bad color → neutral midpoint → good color |
**Rule of thumb:** if the measure can be read as "low to high", use a
light-to-dark gradient of one color. If the measure can be read as "bad to good"
with a meaningful neutral point (usually zero or target), use a divergent
red/neutral/green gradient.
Examples:
- `Sales`, `Units`, `Gross Margin %` as absolute magnitude: use
`#DEEFFF` → `#118DFF` (or another light-to-dark pair derived from one theme
`dataColors` entry).
- `Units MoM %`, `Profit variance`, `Actual vs Target %`: use divergent colors
only when negative values are bad and positive values are good.
> ⚠️ **Do not use sentiment colors for pure magnitude.** Red/green implies
> judgment. A low Sales value is not automatically "bad" unless the user asked
> for performance/target/variance semantics.
**Supported on** `dataPoint.fill` (or `dataPoint.fillRule`) for: barChart,
clusteredBarChart, clusteredColumnChart, columnChart, funnel,
hundredPercentStackedBarChart, hundredPercentStackedColumnChart, ribbonChart,
lineStackedColumnComboChart, lineClusteredColumnComboChart, map, filledMap,
shapeMap, treemap, scatterChart, heatMap.
**For tables/matrices**: add an entry to the `values` object array (NOT `columnFormatting`).
The entry must use:
- A `selector` with `data: [{ dataViewWildcard: { matchingOption: 1 } }]` and
`metadata` pointing to the measure's queryRef.
- A `FillRule` with `Input` using `SelectRef` / `ExpressionName` (referencing
the measure's queryRef) instead of a direct `Measure` / `SourceRef`.
> ⚠️ **Do NOT use `columnFormatting`** for conditional formatting on tables/matrices,
> except data bars (Type 4). `columnFormatting` is for static styling (alignment,
> display units, etc.). PBI Desktop writes other conditional formatting via "cell
> elements" to the `values` array, not `columnFormatting`.
**Pivot table / matrix magnitude gradient example** (placed as entry in `values` array inside `objects`):
```json
{
"properties": {
"backColor": {
"solid": {
"color": {
"expr": {
"FillRule": {
"Input": {
"SelectRef": { "ExpressionName": "metrics.NetIncome" }
},
"FillRule": {
"linearGradient2": {
"min": {
"color": { "Literal": { "Value": "'#DEEFFF'" } }
},
"max": {
"color": { "Literal": { "Value": "'#118DFF'" } }
},
"nullColoringStrategy": {
"strategy": { "Literal": { "Value": "'noColor'" } }
}
}
}
}
}
}
}
}
},
"selector": {
"data": [{ "dataViewWildcard": { "matchingOption": 1 } }],
"metadata": "metrics.NetIncome"
}
}
```
The `metadata` and `ExpressionName` values must match the measure's `queryRef`
from the visual's `queryState`.
**Pivot table / matrix sentiment or variance gradient example** (placed as entry in `values` array inside `objects`):
```json
{
"properties": {
"backColor": {
"solid": {
"color": {
"expr": {
"FillRule": {
"Input": {
"SelectRef": { "ExpressionName": "metrics.NetIncome" }
},
"FillRule": {
"linearGradient3": {
"min": {
"color": { "Literal": { "Value": "'#FF0000'" } },
"value": { "Literal": { "Value": "-5000000D" } }
},
"mid": {
"color": { "Literal": { "Value": "'#FFFFFF'" } },
"value": { "Literal": { "Value": "0D" } }
},
"max": {
"color": { "Literal": { "Value": "'#00FF00'" } },
"value": { "Literal": { "Value": "5000000D" } }
},
"nullColoringStrategy": {
"strategy": { "Literal": { "Value": "'asZero'" } }
}
}
}
}
}
}
}
}
},
"selector": {
"data": [{ "dataViewWildcard": { "matchingOption": 1 } }],
"metadata": "metrics.NetIncome"
}
}
```
**Key differences between chart and table/matrix conditional formatting:**
| Aspect | Charts (`dataPoint`) | Pivot Tables (`values`) |
|--------|---------------------|------------------------|
| Object | `dataPoint` | `values` |
| Property | `fill` | `backColor` or `fontColor` |
| Input ref | `Measure` + `SourceRef` (DAX measures) or `Aggregation` (columns) | `SelectRef` + `ExpressionName` |
| Selector | `dataViewWildcard` only (do NOT include `metadata`) | `data: [{ dataViewWildcard }]` + `metadata` |
| `matchingOption` | `0` | `1` |
**3-color sentiment / variance gradient** (linearGradient3) — use only when
negative/positive values have bad/good meaning:
```json
{
"solid": {
"color": {
"expr": {
"FillRule": {
"Input": {
"Measure": {
"Expression": { "SourceRef": { "Entity": "metrics" } },
"Property": "GrossMargin"
}
},
"FillRule": {
"linearGradient3": {
"min": {
"color": { "Literal": { "Value": "'#FF0000'" } },
"value": { "Literal": { "Value": "-0.01D" } }
},
"mid": {
"color": { "Literal": { "Value": "'#FFFF00'" } },
"value": { "Literal": { "Value": "0D" } }
},
"max": {
"color": { "Literal": { "Value": "'#00FF00'" } },
"value": { "Literal": { "Value": "0.01D" } }
},
"nullColoringStrategy": {
"strategy": { "Literal": { "Value": "'asZero'" } }
}
}
}
}
}
}
}
}
```
**2-color gradient** (linearGradient2) — omit `mid`:
```json
{
"fillRule": {
"linearGradient2": {
"min": { "color": { "Literal": { "Value": "'#DEEFFF'" } } },
"max": { "color": { "Literal": { "Value": "'#118DFF'" } } },
"nullColoringStrategy": {
"strategy": { "Literal": { "Value": "'noColor'" } }
}
}
}
}
```
When `value` is omitted from color stops, PBI auto-calculates from data range.
> ⚠️ **FillRule color stops must use `Literal` hex values** — `ThemeDataColor`
> silently renders black inside `linearGradient2` / `linearGradient3` color stops.
> To use theme-aware colors, read `dataColors[N]` from the theme file and compute
> a lighter tint (blend 40-60% toward `#FFFFFF`) for the min stop.
**For single-series bar/column charts** — the most common use case. Apply a
value-gradient so the highest bar is darkest and lowest is lightest:
```json
"dataPoint": [{
"properties": {
"fill": {
"solid": {
"color": {
"expr": {
"FillRule": {
"Input": {
"Measure": {
"Expression": { "SourceRef": { "Entity": "<table>" } },
"Property": "<measure>"
}
},
"FillRule": {
"linearGradient2": {
"min": { "color": { "Literal": { "Value": "'#D0E8F5'" } } },
"max": { "color": { "Literal": { "Value": "'#56B4E9'" } } },
"nullColoringStrategy": {
"strategy": { "Literal": { "Value": "'noColor'" } }
}
}
}
}
}
}
}
}
},
"selector": {
"data": [{ "dataViewWildcard": { "matchingOption": 0 } }]
}
}]
```
Key requirements:
- **`Input`** must reference the Y-axis measure (Measure or Aggregation field)
- **`selector`** must be `data: [{ dataViewWildcard: { matchingOption: 0 } }]` —
without this selector, the gradient does not render
- **Min color**: light tint of the base color (blend ~50% toward white)
- **Max color**: the base color at full saturation (never darker — avoid black)
- ⚠️ **Gradient color stops use `Literal` directly** — do NOT add an `expr`
wrapper inside the gradient `min.color` / `max.color`. Write
`{ "Literal": { "Value": "'#hex'" } }` not
`{ "expr": { "Literal": { "Value": "'#hex'" } } }`.
The `expr` wrapper exists on the outer `fill.solid.color.expr.FillRule` but
NOT inside the gradient stops. Adding `expr` inside stops causes a
Desktop crash (`Cannot read properties of undefined (reading 'accept')`
in `visitFillRuleStop`).
**Null coloring strategies:**
| Strategy | Behavior |
|----------|----------|
| `"asZero"` | Treat nulls as zero — apply corresponding gradient color |
| `"noColor"` | No color (transparent/default) |
| `"specificColor"` | Use the `color` property from the strategy object |
## Type 2: Rules-Based Formatting
Applies colors based on value conditions using `Conditional.Cases[]` inside a
color property. The structure is the same for charts (`dataPoint.fill`) and
tables/matrices (`values.backColor` or `values.fontColor`).
> ⚠️ There is no `backColorRule` or `fontColorRule` property — these do not
> exist. Rules are expressed as `Conditional.Cases[]` inside the standard color
> property path (`backColor.solid.color.expr.Conditional`).
**Table/matrix example** (entry in `values` array):
```json
{
"properties": {
"backColor": {
"solid": {
"color": {
"expr": {
"Conditional": {
"Cases": [
{
"Condition": {
"Comparison": {
"ComparisonKind": 2,
"Left": { "Measure": { "Expression": { "SourceRef": { "Entity": "Sales" } }, "Property": "TotalProfit" } },
"Right": { "Literal": { "Value": "500D" } }
}
},
"Value": { "Literal": { "Value": "'#1AAB40'" } }
},
{
"Condition": {
"Comparison": {
"ComparisonKind": 3,
"Left": { "Measure": { "Expression": { "SourceRef": { "Entity": "Sales" } }, "Property": "TotalProfit" } },
"Right": { "Literal": { "Value": "0D" } }
}
},
"Value": { "Literal": { "Value": "'#D64554'" } }
}
],
"DefaultValue": { "Literal": { "Value": "'#FFFFFF'" } }
}
}
}
}
}
},
"selector": {
"data": [{ "dataViewWildcard": { "matchingOption": 1 } }],
"metadata": "Sum(Sales.TotalProfit)"
}
}
```
**Chart example** (entry in `dataPoint` array — no selector needed):
```json
{
"properties": {
"fill": {
"solid": {
"color": {
"expr": {
"Conditional": {
"Cases": [
{
"Condition": {
"Comparison": {
"ComparisonKind": 2,
"Left": { "Measure": { "Expression": { "SourceRef": { "Entity": "Sales" } }, "Property": "TotalProfit" } },
"Right": { "Literal": { "Value": "500D" } }
}
},
"Value": { "Literal": { "Value": "'#1AAB40'" } }
}
],
"DefaultValue": { "Literal": { "Value": "'#118DFF'" } }
}
}
}
}
}
}
}
```
**Key rules:**
- All keys are **PascalCase**: `Conditional`, `Cases`, `Condition`, `Comparison`,
`ComparisonKind`, `Value`, `DefaultValue`.
- The operator is **`Comparison`** (not `Compare`).
- `Left` must be a self-aggregating expression: use `Measure` (already aggregated)
or wrap a `Column` in `Aggregation { Expression: Column, Function: N }`.
A raw `Column` in `Left` breaks the visual.
- `DefaultValue` provides the fallback color when no case matches.
- For "is not equal" conditions, use `Not { Expression: { Comparison: { ComparisonKind: 0, ... } } }`
— there is no NotEqual ComparisonKind.
**Selector requirements (critical — wrong selector silently drops all formatting):**
| Visual type | Required selector | Notes |
|-------------|-------------------|-------|
| Tables/matrices | `{ "data": [{ "dataViewWildcard": { "matchingOption": 1 } }], "metadata": "<queryRef>" }` | Both `data` AND `metadata` required — either alone fails |
| Charts | No selector, or `{ "data": [{ "dataViewWildcard": { "matchingOption": 1 } }] }` | Do NOT include `metadata` — it causes silent failure |
**ComparisonKind values:**
| Value | Operator | Meaning |
|-------|----------|---------|
| 0 | `==` | Equal |
| 1 | `>` | Greater Than |
| 2 | `>=` | Greater Than or Equal |
| 3 | `<` | Less Than |
| 4 | `<=` | Less Than or Equal |
## Type 3: Icon Sets
Adds icons alongside values in tables/matrices based on thresholds. Uses the
`icon` property in a `values` array entry with `Conditional.Cases[]`.
> ⚠️ There is no `iconRule` or `iconDefinition` property — these do not exist
> and are silently discarded. Icons use the same `Conditional.Cases[]` pattern
> as rules-based formatting, with icon name literals as `Value`.
**Table/matrix example** (entry in `values` array):
```json
{
"properties": {
"icon": {
"kind": "Icon",
"layout": {
"expr": { "Literal": { "Value": "'Before'" } }
},
"verticalAlignment": {
"expr": { "Literal": { "Value": "'Middle'" } }
},
"value": {
"expr": {
"Conditional": {
"Cases": [
{
"Condition": {
"Comparison": {
"ComparisonKind": 2,
"Left": { "Measure": { "Expression": { "SourceRef": { "Entity": "Sales" } }, "Property": "TotalProfit" } },
"Right": { "Literal": { "Value": "1000D" } }
}
},
"Value": { "Literal": { "Value": "'CircleHigh'" } }
},
{
"Condition": {
"Comparison": {
"ComparisonKind": 3,
"Left": { "Measure": { "Expression": { "SourceRef": { "Entity": "Sales" } }, "Property": "TotalProfit" } },
"Right": { "Literal": { "Value": "500D" } }
}
},
"Value": { "Literal": { "Value": "'CircleLow'" } }
}
],
"DefaultValue": { "Literal": { "Value": "'CircleMedium'" } }
}
}
}
}
},
"selector": {
"data": [{ "dataViewWildcard": { "matchingOption": 1 } }],
"metadata": "Sum(Sales.TotalProfit)"
}
}
```
**Icon property structure:**
| Property | Required | Type | Values |
|----------|----------|------|--------|
| `kind` | ✅ | string | `"Icon"` (always) |
| `value` | ✅ | expr Conditional | `Conditional.Cases[]` with icon name literals |
| `layout` | optional | expr literal | `"Before"` (default), `"After"`, `"IconOnly"` (hide value) |
| `verticalAlignment` | optional | expr literal | `"Top"`, `"Middle"` (default), `"Bottom"` |
**Icon name catalog** (use as `Value: { Literal: { Value: "'<name>'" } }`):
| Family | Icons (high → low / full → empty) |
|--------|-----------------------------------|
| Circles (3-state) | `CircleHigh` · `CircleMedium` · `CircleLow` |
| Circles (4-state) | `CircleHigh` · `CircleMedium` · `4CircleMedium2` · `4CircleLow` |
| Circle fill | `CircleFilled` · `Circle75` · `CircleHalf` · `Circle25` · `CircleEmpty` |
| Circle pattern | `CircleGreenPatternFill` · `CircleYellowPatternFill` · `CircleRedPatternFill` · `CircleBlackFill` · `CircleGrayPatternFill` · `CirclePurplePatternFill` |
| Circle pattern (black bg) | `CircleGreenBlackBackgroundPatternFill` · `CircleYellowBlackBackgroundPatternFill` · `CircleRedBlackBackgroundPatternFill` |
| Circle pattern (outline) | `CircleGreenBlackOutlinePatternFill` · `CircleYellowBlackOutlinePatternFill` · `CircleRedBlackOutlinePatternFill` |
| Signs | `SignMedium` · `SignLow` |
| Symbols (✓/!/✗) | `SymbolHigh` · `SymbolMedium` · `SymbolLow` |
| Circled symbols | `CircleSymbolHigh` · `CircleSymbolMedium` · `CircleSymbolLow` |
| Triangles | `TriangleHigh` · `TriangleMedium` · `TriangleLow` |
| Colored arrows | `ColoredArrowUp` · `ColoredArrowUpRight` · `ColoredArrowRight` · `ColoredArrowDownRight` · `ColoredArrowDown` |
| Colored arrows (alt) | `ColoredArrowUpRed` · `ColoredArrowDownGreen` |
| Grey arrows | `GreyArrowUp` · `GreyArrowUpRight` · `GreyArrowRight` · `GreyArrowDownRight` · `GreyArrowDown` |
| Traffic lights | `TrafficHigh` · `TrafficMedium` · `TrafficLow` · `TrafficBlackRimmed` |
| Traffic lights (light) | `TrafficHighLight` · `TrafficMediumLight` · `TrafficLowLight` · `TrafficBlackRimmedLight` |
| Flags | `FlagHigh` · `FlagMedium` · `FlagLow` · `FlagBlack` |
| Flag pattern | `FlagGreenPatternFill` · `FlagYellowPatternFill` · `FlagRedPatternFill` |
| Stars | `StarHigh` · `StarMedium` · `StarLow` |
| Stars (light) | `StarHighLight` · `StarMediumLight` |
| Signal bars | `SignalBarFull` · `SignalBarMedium2` · `SignalBarMedium` · `SignalBarLow` · `SignalBarEmpty` |
| Signal bars (colored) | `SignalBarFullColored` · `SignalBarMedium2Colored` · `SignalBarMediumColored` · `SignalBarLowColored` |
| Quadrants | `QuadrantFull` · `Quadrant75` · `Quadrant50` · `Quadrant25` · `QuadrantEmpty` |
| Quadrants (colored) | `QuadrantFullColored` · `Quadrant75Colored` · `Quadrant50Colored` · `Quadrant25Colored` |
> ⚠️ Invalid icon names cause a Desktop crash ("Unable to find resource").
> Only use names from the catalog above.
**Selector:** Same as rules-based — tables/matrices require both `data` and
`metadata`; the `metadata` must reference the measure's queryRef.
## Type 4: Data Bars
In-cell bar visualization for tables/matrices. Applied per-column via metadata selector.
**Placed as an entry in the `columnFormatting` array (NOT `values`), with a
metadata-only selector:**
> ⚠️ **Do not use `dataViewWildcard` for data bars.** Unlike `values.backColor`,
> `values.fontColor`, and `values.icon`, data bars are column-level formatting.
> They render with `{ "selector": { "metadata": "<queryRef>" } }`; adding
> `data: [{ "dataViewWildcard": ... }]` causes the bars to disappear.
```json
{
"properties": {
"dataBars": {
"positiveColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#118DFF'" } } } } },
"negativeColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#D64554'" } } } } },
"axisColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#999999'" } } } } },
"reverseDirection": { "expr": { "Literal": { "Value": "false" } } },
"hideText": { "expr": { "Literal": { "Value": "false" } } }
}
},
"selector": { "metadata": "Sales.Revenue" }
}
```
Properties: `positiveColor`, `negativeColor`, `axisColor` (fills), `reverseDirection` (bool),
`hideText` (bool), `minValue`/`maxValue` (optional numeric scale bounds).
## Type 5: Web URL
Turns text into clickable hyperlinks using a URL field:
```json
{
"properties": {
"webUrl": {
"expr": {
"Column": {
"Expression": { "SourceRef": { "Entity": "Companies" } },
"Property": "WebsiteUrl"
}
}
}
}
}
```
Supported in tables and matrices.
## Type 6: Field-Driven Color
Colors a property using hex values stored in a data column. There is no special
`fieldValue` property — this is a pattern of placing an `Aggregation` expression
(referencing a color column) inside any standard color property slot
(`backColor`, `fontColor`, `foreColor`, etc.).
### Contract
```json
{
"properties": {
"backColor": {
"solid": {
"color": {
"expr": {
"Aggregation": {
"Expression": {
"Column": {
"Expression": { "SourceRef": { "Entity": "Colors" } },
"Property": "Color"
}
},
"Function": 3
}
}
}
}
}
},
"selector": {
"data": [{ "dataViewWildcard": { "matchingOption": 1 } }],
"metadata": "Sum(OrderBreakdown.Sales)"
}
}
```
### Aggregation Function values
| Function | Meaning |
|----------|---------|
| 3 | Min |
| 4 | Max |
### Selector
The `metadata` queryRef targets the **column being colored** (the measure or
column whose cells receive the color), not the color source column.
| `matchingOption` | Meaning |
|------------------|---------|
| 0 | All data points including totals |
| 1 | Values only (excludes totals) |
### Applies to
Works with `backColor` and `fontColor`. The color source column must contain
valid hex strings (e.g., `#FF6B35`).
references/expressions.md
# Expressions — Semantic Query Trees
> Referenced from SKILL.md. Read this for expression tree templates used in
> visual queries (`queryState`) and sort definitions. For **filter** templates
> (Categorical, Range, etc.), see **references/filters.md § Add a Filter**.
## Expression Tree Templates
All data field references in `queryState`, `filterConfig`, and `sortDefinition`
use the Semantic Query expression tree format. These are the canonical patterns.
### Entity Source Expression
Expression references a table(entity) in the semantic model:
```json
{
"Name": "<alias>",
"Entity": "<Table>",
"Type": 0
}
```
### Column Expression
Expression references a column from a table (entity) in the semantic model:
```json
{
"Column": {
"Expression": {
"SourceRef": { "Entity": "<TableName>" }
},
"Property": "<ColumnName>"
}
}
```
### Measure Expression
Expression references a measure defined in the semantic model:
```json
{
"Measure": {
"Expression": {
"SourceRef": { "Entity": "<TableName>" }
},
"Property": "<MeasureName>"
}
}
```
### Hierarchy Level Expression
```json
{
"HierarchyLevel": {
"Expression": {
"Hierarchy": {
"Expression": {
"SourceRef": { "Entity": "<TableName>" }
},
"Hierarchy": "<HierarchyName>"
}
},
"Level": "<LevelName>"
}
}
```
### Aggregation Expression
Wraps a column with an aggregation function:
```json
{
"Aggregation": {
"Expression": {
"Column": {
"Expression": {
"SourceRef": { "Entity": "<TableName>" }
},
"Property": "<ColumnName>"
}
},
"Function": 0
}
}
```
Aggregation Function values: `0`=Sum, `1`=Avg, `2`=Count, `3`=Min, `4`=Max,
`5`=CountNonNull, `6`=Median, `7`=StdDev, `8`=Var
### Comparison Expression
Expression compares between two values
```json
{
"Comparison": {
"ComparisonKind": 0,
"Left": {
/* column/hierarchy/measure expression */
},
"Right": {
"Literal": {
"Value": "<Value>" /* "null" for blank value, "''" for empty text only value */
}
}
}
}
```
ComparisonKind values: `0`=Equal, `1`=GreaterThan, `2`=GreaterThanOrEqual, `3`=LessThan, `4`=LessThanOrEqual
### Not Expression
```json
{
"Not": {
"Expression": {
/* Comparison/Contains/StartsWith/In expression */
}
}
}
```
"Not" with "GreaterThan" equals "LessThanOrEqual"
### Contains/StartsWith Expression
```json
{
"Contains": { /* StartsWith for StartsWith Expression */
"Left": {
/* column/hierarchy/measure expression */
},
"Right": {
"Literal": {
"Value": "<Value>"
}
}
}
}
```
Only available for text value
### In Expression
```json
{
"In": {
"Expressions": [
{
"Column": {
"Expression": { "SourceRef": { "Source": "<alias>" } },
"Property": "<Column>"
}
}
],
"Values": [
[{ "Literal": { "Value": "'<value>'" } }],
[{ "Literal": { "Value": "'<value2>'" } }]
]
}
}
```
### Visual Query Projection
Each field well role contains an array of projections. Each projection binds a
data field to the visual:
```json
{
"field": { /* Column/Measure/Aggregation/Hierarchy expression from above */ },
"queryRef": "<Entity>.<Property>",
"nativeQueryRef": "<Property>",
"active": true
}
```
- `queryRef`: Unique per visual. Format: `Entity.Property` for simple refs.
For aggregated columns: `CountNonNull(Entity.Property)` etc.
- `nativeQueryRef`: Usually the Property name. If duplicated across roles,
append a number (e.g. `Sales1`).
- `active`: Only used in drill hierarchies. The currently active level is `true`.
### Sort Definition
`sortDefinition` is a property of the **`query`** object
(`visual.query.sortDefinition`). Supported in all schema versions (2.2.0+).
Use it to sort bar charts by value descending so "Top N" charts display
correctly.
```json
// Add inside: visual.query (sibling of queryState)
"sortDefinition": {
"sort": [
{
"field": { /* Column or Measure expression */ },
"direction": "Descending"
}
],
"isDefaultSort": false
}
```
- Direction values: `"Ascending"` or `"Descending"`.
- `isDefaultSort`: When `false`, Power BI treats this as user-explicit and
won't auto-override it. When `true` or omitted, Power BI may change the
sort to match the visual type's default.
**Known Limitations:**
- `sortDefinition` controls sort order of the **measure values**, not the
category axis order. Text-based categories (e.g., Month Name, Quarter
Name) always sort **alphabetically** regardless of `sortDefinition`.
"April" comes before "January" because "A" < "J".
- To get chronological order on a time axis, use a **Date column** or
**numeric column** (Month Number, Quarter Number) as the Category —
never text month/quarter names.
- If the semantic model has `sortByColumn` configured on a text column
(e.g., Month Name sorted by Month Number), PBI respects that. But
`sortDefinition` in PBIR cannot create or override `sortByColumn` —
it must exist in the TMDL model definition.
---
### Expansion state
Expansion state describes which nodes in a hierarchy are expanded or collapsed. It is only used when a visual has hierarchy structure (e.g., a matrix with Row or Column hierarchies, or a hierarchy slicer). If the visual has no hierarchy structure, omit `expansionStates` entirely.
- **`roles`** — which query roles contain the hierarchy (e.g., `["Rows"]` for a matrix, `["Values"]` for a slicer)
- **`levels`** — one entry per hierarchy level; `queryRefs` must match a `queryRef` in the visual's query; `identityKeys` reference the field expressions; `isCollapsed` / `isPinned` control the default UI state
- **`root.children`** — records which specific data values the user has toggled open; each child has `identityValues` (literal values identifying the node) and `isToggled: true`; nested `children` represent deeper expansions
```json
{
"expansionStates": [ // Defines the specific data points that are expanded
{
"roles": [
"Values"
],
"levels": [
{
"queryRefs": [ // Which fields in the query does this relate to - must match a queryRef in the query above
"Brand.BrandName"
],
"isCollapsed": true,
"identityKeys": [
{
/* Hierarchy level/Column/Measure/Aggregation expressions from the expression in the query above*/
}
],
"isPinned": true
},
],
"root": { // Defines the specific values that are expanded for each field in the hierarchy
"children": [
{
"identityValues": [
{
"Literal": {
"Value": "'A. Datum'"
}
}
],
"isToggled": true,
"children": [
{
"identityValues": [
{
"Literal": {
"Value": "'N'"
}
}
],
"isToggled": true
}
]
},
{
"identityValues": [
{
"Literal": {
"Value": "null"
}
}
],
"isToggled": true,
"children": [
{
"identityValues": [
{
"Literal": {
"Value": "null"
}
}
],
"isToggled": true
}
]
}
]
}
}
]
}
```
## Filters
Filter templates (Categorical, Range, Inverted, TopN, etc.) are in
**references/filters.md § Add a Filter**. Expression trees above (Column, Measure,
Aggregation) are used inside filter `field` and `Where` conditions.
references/filter-pane.md
# Filter Pane & Filter Card Formatting
The filter pane is the collapsible side panel where users select and apply
filters at runtime. Its chrome lives in `page.json → objects.outspacePane`,
and each individual filter control is styled via `page.json → objects.filterCard`
with `Applied` / `Available` state selectors.
> **Scope:** This file covers the **appearance** of the filter pane and its
> filter cards. For filter **definitions** (which fields are filtered, `Where`
> conditions, filter scopes at report/page/visual level), see
> [`filters.md`](filters.md). For slicer visuals (a different mechanism,
> stored in `visual.json`), see [`slicers.md`](slicers.md).
>
> **Read first:** [`formatting-overview.md`](formatting-overview.md) for the
> cascade model and PBIR encoding rules.
## Contents
- [Filter Pane (`outspacePane`)](#filter-pane-outspacepane)
- [Filter Cards (`filterCard`)](#filter-cards-filtercard)
- [Theme-Level Styling](#theme-level-styling)
## Filter Pane (`outspacePane`)
The collapsible filter panel on the right side of a page:
```json
"outspacePane": [{
"properties": {
"backgroundColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FFFFFF'" } } } } },
"foregroundColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#252423'" } } } } },
"titleSize": { "expr": { "Literal": { "Value": "12D" } } },
"headerSize": { "expr": { "Literal": { "Value": "11D" } } },
"searchTextSize": { "expr": { "Literal": { "Value": "9D" } } },
"fontFamily": { "expr": { "Literal": { "Value": "'Segoe UI'" } } },
"border": { "expr": { "Literal": { "Value": "true" } } },
"borderColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#C8C8C8'" } } } } },
"checkboxAndApplyColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#118DFF'" } } } } },
"inputBoxColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FFFFFF'" } } } } },
"width": { "expr": { "Literal": { "Value": "250D" } } }
}
}]
```
**All 12 properties:**
| Property | Type | Description |
|----------|------|-------------|
| `backgroundColor` | fill | Pane background |
| `transparency` | numeric | Background transparency (0–100) |
| `foregroundColor` | fill | Text and icon color |
| `titleSize` | numeric | Title text size (pt) |
| `headerSize` | numeric | Section header text size (pt) |
| `searchTextSize` | numeric | Search box text size (pt) |
| `fontFamily` | string | Font family for pane text |
| `border` | bool | Show pane border |
| `borderColor` | fill | Border color |
| `checkboxAndApplyColor` | fill | Checkbox accent and Apply button color |
| `inputBoxColor` | fill | Search/input box background |
| `width` | numeric | Pane width in pixels |
## Filter Cards (`filterCard`)
Individual filter controls within the pane. Use `id` selectors to distinguish
**Applied** (active) vs **Available** (inactive) states:
```json
"filterCard": [
{
"properties": {
"backgroundColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FFFFFF'" } } } } },
"foregroundColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#252423'" } } } } },
"textSize": { "expr": { "Literal": { "Value": "9D" } } },
"border": { "expr": { "Literal": { "Value": "true" } } },
"borderColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#C8C8C8'" } } } } }
},
"selector": { "id": "Available" }
},
{
"properties": {
"backgroundColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#E8F0FE'" } } } } },
"borderColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#118DFF'" } } } } }
},
"selector": { "id": "Applied" }
}
]
```
**⚠️ PascalCase required**: Selectors must be `"Applied"` / `"Available"`, not lowercase.
**8 properties per state:**
| Property | Type | Description |
|----------|------|-------------|
| `backgroundColor` | fill | Card background |
| `transparency` | numeric | Background transparency (0–100) |
| `foregroundColor` | fill | Text and icon color |
| `textSize` | numeric | Text size (pt) |
| `fontFamily` | string | Font family |
| `inputBoxColor` | fill | Input/dropdown background |
| `border` | bool | Show card border |
| `borderColor` | fill | Border color |
## Theme-Level Styling
The patterns above set filter pane appearance for a **single page** via
`page.json`. To apply filter pane styling to an entire report (e.g., as
part of a dark theme), set `outspacePane` and `filterCard` inside the
theme's `visualStyles["*"]["*"]`. The filter pane does **NOT** inherit
from theme structural colors, so theme switches will not update it
automatically — explicit theme entries are required.
See [`re-theming.md` § Dark Mode Authoring Checklist](re-theming.md#dark-mode-authoring-checklist)
for the complete dark-mode workflow, and [`re-theming.md` § Step 3: Filter pane + filter cards](re-theming.md#step-3-filter-pane--filter-cards)
for the theme-level filter pane recipe.
references/filters.md
# Filters authoring Workflows & Examples
> Referenced from SKILL.md. Read this when creating or modifying filters
> For reference of expressions, see **references/expressions.md**
>
> **For filter pane and filter card appearance** (collapse panel colors,
> fonts, Applied/Available state styling, pane width) — these are stored in
> `page.json → objects.outspacePane` / `filterCard`, separate from the
> `filterConfig` definitions covered below. See
> [`filter-pane.md`](filter-pane.md).
## Add a Filter
Filters exist at three levels: report (`report.json`), page (`page.json`),
and visual (`visual.json`). All use the same `filterConfig.filters` array.
### Filter Types
| Type | Description |
|------|-------------|
| `Categorical` | Discrete value selection (in-list) |
| `Range` | Numeric or date range |
| `Advanced` | Custom condition expressions |
| `TopN` | Top/Bottom N by measure |
| `RelativeDate` | Relative date (e.g. "Last 30 days") |
| `RelativeTime` | Relative time range |
| `Exclude` | Exclude specific data point |
| `Include` | Include specific data point |
### Categorical Filter (In-list)
```json
{
"name": "Filter<24hexchars>",
"field": {
"Column": {
"Expression": { "SourceRef": { "Entity": "<Table>" } },
"Property": "<Column>"
}
},
"type": "Categorical",
"filter": {
"Version": 2,
"From": [
{ /* Entity Source Expression */ }
],
"Where": [
{
"Condition": {
/* In Expression */
}
}
]
},
"howCreated": "User" // must have
}
```
**⚠️ Critical**: Inside a `filter.Where` condition, `SourceRef` uses `"Source"`
(the alias from `From`), NOT `"Entity"`. The `field` property at the top level
uses `"Entity"`. `powerbi-report-author validate` flags entity refs that
slip into `Where` with `PBIR_FILTER_ENTITY_IN_WHERE`. To inventory every filter
defined in a report (report / page / visual scopes), run
`powerbi-report-author preview-filters <path-to-.Report-dir>`. Each
entry returns `{ path, scope, name, type, displayName?, isHiddenInViewMode?,
isLockedInViewMode? }` only — open the file at `path` to inspect the actual
predicates (`filter.Where`), `From` sources, and `howCreated`.
### Inverted Selection (Exclude)
To exclude values (inverted selection), **two** changes are required:
1. **Filter condition** — wrap the `In` expression with `Not` in the `Where` clause:
```json
"Where": [
{
"Condition": {
"Not": {
"Expression": {
"In": {
"Expressions": [{ /* Column Expression */ }],
"Values": [
[{ "Literal": { "Value": "<excluded-value>" } }]
]
}
}
}
}
}
]
```
2. **Slicer interaction state** — set `isInvertedSelectionMode` in the slicer's
`objects.data` so Desktop knows future checkbox clicks should subtract from
an "all selected" base state:
```json
"objects": {
"data": [{
"properties": {
"isInvertedSelectionMode": {
"expr": { "Literal": { "Value": "true" } }
}
}
}]
}
```
### Exclude/Include Filter
```json
{
"name": "Filter<24hexchars>",
"type": "Include", // Will be "Exclude" for exclude filter
"filter": {
"Version": 2,
"From": [
{
/* Entity Source Expression */
}
],
"Where": [
{
"Condition": {
"In": {
"Expressions": [
{
/* Column Expression */
}
],
"Values": [
[
{
"Literal": {
"Value": "<Value>"
}
}
]
]
}
}
}
]
},
"howCreated": "Include" // Will be "Exclude" for exclude filter
}
```
### Range Filter
```json
{
"name": "Filter<24hexchars>",
"field": { /* Column expression */ },
"type": "Advanced",
"filter": {
"Version": 2,
"From": [{ /* Entity Source Expression */ }],
"Where": [{
"Condition": {
"Between": {
"Expression": {
"Column": {
"Expression": { "SourceRef": { "Source": "<alias>" } },
"Property": "<Column>"
}
},
"LowerBound": { "Literal": { "Value": "100D" } },
"UpperBound": { "Literal": { "Value": "500D" } }
}
}
}]
},
"howCreated": "User" // must have
}
```
### Advanced Filter
Uses logical operators (AND/OR) and comparison operators (e.g., contains, greater than, before) to dynamically include or exclude data
```json
{
"name": "Filter<24hexchars>",
"field": { /* Column expression */ },
"type": "Advanced",
"filter": {
"Version": 2,
"From": [{ /* Entity Source Expression */ }],
"Where": [{
"Condition": {
"And": {
"Left": { /* Comparison/Not/Contains/StartsWith Expression */},
"Right": {/* Comparison/Not/Contains/StartsWith Expression */}
}
}
}]
},
"howCreated": "User" // must have
}
```
Condition can be And/Or.
### TopN Filter
```json
{
"name": "Filter<24hexchars>",
"field": {
"Column": {
"Expression": {
"SourceRef": {
"Entity": "<Table>"
}
},
"Property": "<Column>"
}
},
"type": "TopN",
"filter": {
"Version": 2,
"From": [
{
"Name": "subquery",
"Expression": {
"Subquery": {
"Query": {
"Version": 2,
"From": [
{
/* Entity Source Expression */
}
],
"Select": [
{
"Column": {
/* Column Expression */
},
"Name": "field"
}
],
"OrderBy": [
{
"Direction": 2,
"Expression": {
/* Aggregation Expression */
}
}
],
"Top": 5
}
}
},
"Type": 2
},
{
"Name": "<alias>",
"Entity": "<Table>",
"Type": 0
}
],
"Where": [
{
"Condition": {
"In": {
"Expressions": [
{
/* Column Expression */
}
],
"Table": {
"SourceRef": {
"Source": "subquery"
}
}
}
}
}
]
},
"howCreated": "User" // must have
}
```
**⚠️ Critical:**
- TopN filter can only be added as a **visual-level** filter — not page-level or report-level.
- It can only be applied on a normal column or measure field.
- The `OrderBy.Expression` **must use an `Aggregation` expression** (wrapping a Column
with a function like Sum/Count), **not a `Measure` expression**. Using a Measure
reference in the subquery's OrderBy causes a Desktop error:
`Cannot read properties of undefined (reading 'accept')` in `SemanticQueryRewriter.rewriteOrderBy`.
- Direction value: `1` = Bottom, `2` = Top.
### Relative Date/Time filter
``` json
{
"name": "Filter<24hexchars>",
"field": {
"Column": {
"Expression": {
"SourceRef": {
"Entity": "<Table>"
}
},
"Property": "<Column>"
}
},
"type": "RelativeDate", // "RelativeTime" for relative time filter only targeting hours/minutes/seconds
"filter": {
"Version": 2,
"From": [
{
/* Entity Source Expression */
}
],
"Where": [
{
"Condition": {
"Between": {
"Expression": {
"Column": {
"Expression": {
"SourceRef": {
"Source": "<alias>"
}
},
"Property": "<Column>"
}
},
"LowerBound": {
"DateSpan": {
"Expression": {
"DateAdd": {
"Expression": {
"DateAdd": {
"Expression": {
"Now": {}
},
"Amount": 1,
"TimeUnit": 0
}
},
"Amount": -10,
"TimeUnit": 3
}
},
"TimeUnit": 0
}
},
"UpperBound": {
"DateSpan": {
"Expression": {
"Now": {}
},
"TimeUnit": 0
}
}
}
}
}
]
},
"howCreated": "User" // must have
}
```
TimeUnit values: `0`=Day, `1`=Week, `2`=Month, `3`=Year, `4`=Decade, `5`=Second, `6`=Minute, `7`=Hour
## References
Type value inside of the From array in the filter template: `0`=Table
references/formatting-overview.md
# Formatting & Theming Overview
> **Read this first** before editing any visual appearance in a PBIR report.
> It explains the cascade model, value encoding rules, and which file to read next.
## Formatting Cascade
Power BI resolves formatting in a layered cascade (highest priority wins):
| Priority | Layer | File | Encoding |
|----------|-------|------|----------|
| 1 (highest) | Conditional formatting | `visual.json` objects (FillRule, rules) | PBIR expressions |
| 2 | Per-visual objects | `visual.json → visual.objects` | PBIR expressions |
| 3 | Visual container objects | `visual.json → visual.visualContainerObjects` | PBIR expressions |
| 4 | Page objects | `page.json → objects` (background, filter pane) | PBIR expressions |
| 5 | Custom theme (type-specific) | `theme.json → visualStyles[type]["*"][obj]` | Theme encoding |
| 6 | Custom theme (wildcard) | `theme.json → visualStyles["*"]["*"][obj]` | Theme encoding |
| 7 | Base theme | `SharedResources/BaseThemes/` | Theme encoding |
| 8 (lowest) | System defaults | Built into PBI Desktop | — |
A property set at layer 2 overrides the same property at layers 3–8.
When a cascade result matters visually, verify in Desktop with the
`powerbi-desktop` screenshot workflow.
> **⚠️ Theme changes require sweeping all cascade layers.** The theme file
> only controls layers 5–6. Hardcoded colors in `page.json` (layer 4) and
> `visual.json` (layers 2–3) override the theme and must be updated in the
> same operation. See
> [re-theming.md § Re-theming an Existing Report](re-theming.md#re-theming-an-existing-report).
### VCO Per-Visual Requirements
These `visualContainerObjects` properties must be set **per-visual** —
they do not cascade reliably from theme `visualStyles`:
- `border` (including `radius`) — for rounded corners
- `background` (show, color, transparency)
- `padding` — must accompany any other VCO override
- Card-specific: `accentBar`, `outline`, `layout` (require `selector: { id: "default" }`)
**Rule**: when setting any `visualContainerObjects` per-visual, always
set `background`, `border` (with `radius`), `padding`, and
`visualHeader` together. Partial VCO overrides cause PBI to reset
omitted properties to system defaults.
## Value Encoding — Three Formats
**Critical**: Theme files and PBIR files encode the same properties differently.
Using the wrong encoding is the #1 formatting error.
| Value | Theme JSON (plain) | Theme visualStyles (hybrid) | PBIR (expression-wrapped) |
|-------|--------------------|-----------------------------|---------------------------|
| Boolean | `true` | `true` | `{"expr":{"Literal":{"Value":"true"}}}` |
| Number | `12` | `12` | `{"expr":{"Literal":{"Value":"12D"}}}` |
| Integer | `3` | `3` | `{"expr":{"Literal":{"Value":"3L"}}}` |
| String | `"dotted"` | `"dotted"` | `{"expr":{"Literal":{"Value":"'dotted'"}}}` |
| Color | `"#118DFF"` | `"#118DFF"` or `{"solid":{"color":"#118DFF"}}` | `{"solid":{"color":{"expr":{"Literal":{"Value":"'#118DFF'"}}}}}` |
| Theme color | — | — | `{"solid":{"color":{"expr":{"ThemeDataColor":{"ColorId":0,"Percent":0}}}}}` |
**Rules:**
- **theme.json** top-level keys (dataColors, good/bad, structural): plain JSON
- **theme.json** `visualStyles` properties: plain JSON, but some color props require `{"solid":{"color":"#hex"}}`
- **visual.json** and **page.json** objects: always PBIR expression wrappers
- Use `powerbi-report-author expr encode --kind <t> <v>` to generate PBIR
expression encodings; use `powerbi-report-author theme encode --kind <t> <v>`
for theme-style values
- Use `powerbi-report-author expr decode '<json>'` to inspect existing values
- Use `powerbi-report-author formatting describe-property <type> <object> <property>`
to look up the expected `type`/`kind` (or
`powerbi-report-author formatting search <type> <regex>` to find a property
across all objects on a visual)
## Selector Types (Quick Reference)
Selectors control which data a formatting entry targets. Five types exist,
in descending priority:
| Type | Syntax | Use Case |
|------|--------|----------|
| **data** (scope identity) | `"data": [{"scopeId": {...}}]` | Color a specific category value (e.g., "Electronics") |
| **data** (wildcard) | `"data": [{"dataViewWildcard": {"matchingOption": N}}]` | All instances (0), instances only (1), totals only (2) |
| **metadata** | `"metadata": "Table.Field"` | Target a specific measure/column |
| **id** | `"id": "default"` | User-defined instance (cards, filter cards) |
| **none** (static) | *(no selector)* | Base formatting — lowest priority |
Within each priority row, **first match in array order wins**.
See `references/formatting.md` for full selector patterns and examples.
## Which File to Read Next
| You are editing… | Read this |
|-----------------|-----------|
| `visual.json` — chart colors, labels, axes, data points, VCOs, conditional formatting, row banding | **references/formatting.md** |
| `page.json` — canvas background, wallpaper, page background images | **references/page-formatting.md** |
| `page.json` — filter pane (`outspacePane`), filter cards (`filterCard`) | **references/filter-pane.md** |
| `theme.json` — dataColors, textClasses, visualStyles, style presets | **references/theming.md** |
references/formatting.md
# Formatting Patterns
> **Read first:** [`formatting-overview.md`](formatting-overview.md) — cascade
> model and encoding rules. Related: [`authoring.md`](authoring.md) for full
> visual JSON examples, [`theming.md`](theming.md) for theme.json, and
> [`conditional-formatting.md`](conditional-formatting.md) for data-driven
> formatting.
> **⚠️ The CLI is the source of truth for property names and enum values.**
> Patterns here show structure, but property names vary between visual types.
> Confirm before applying formatting:
>
> | Use case | Command |
> |---|---|
> | Inspect properties + enums of one object | `powerbi-report-author formatting describe-object <type> <object>` |
> | Look up one property | `powerbi-report-author formatting describe-property <type> <object> <property>` |
> | Search by name across all objects of a visual | `powerbi-report-author formatting search <type> <regex>` |
> | Flattened (object, property) list across `objects` + VCOs | `powerbi-report-author formatting effective-properties <type>` |
> Examples use illustrative `<table>.<measure>` identifiers — substitute your own.
## Contents
- [Formatting JSON Structure](#formatting-json-structure)
- [Literal Values](#literal-values)
- [Solid Color Fill](#solid-color-fill)
- [Theme Data Color Reference](#theme-data-color-reference)
- [Selectors](#selectors-targeting-specific-data)
- [Visual Container Objects (VCO)](#visual-container-objects-vco)
- [Color Strategy & Patterns](#color-strategy--patterns) → [`color-strategy.md`](color-strategy.md)
- [Conditional Formatting](#conditional-formatting) → [`conditional-formatting.md`](conditional-formatting.md)
- [Shape Visual Formatting](#shape-visual-formatting) → [`shape.md`](shape.md)
- [Line & Marker Formatting](#line--marker-formatting-linestyles--markers) → [`cartesian.md`](cartesian.md)
- [Row Banding (Table & Matrix)](#row-banding-table--matrix) → [`table.md`](table.md)
- [Page-Level Formatting](#page-level-formatting-pagejson-objects) → [`page-formatting.md`](page-formatting.md)
- [Background Images — Routing](#background-images--routing) → [`image.md`](image.md), [`page-formatting.md`](page-formatting.md)
- [References](#references)
## Formatting JSON Structure
All formatting properties in `visual.json` live inside the `objects` or
`visualContainerObjects` keys within the `visual` object. Every object is an
**array of property sets** — even when there is only one entry.
```text
visual.json
└── visual
├── objects ← chart-specific formatting
│ └── <objectName> ← array of { properties, selector? }
│ └── [{ "properties": { "prop1": <expr>, ... }, "selector": ... }]
└── visualContainerObjects ← container formatting (title, background, …)
└── <objectName>
└── [{ "properties": { "prop1": <expr>, ... } }]
```
**Rules:**
1. Each object name (e.g. `dataPoint`, `categoryAxis`, `title`) holds an
**array** — `[{ "properties": { ... } }]`, not a bare properties object.
2. Each array entry is `{ "properties": { <propertyName>: <value-expression> } }`.
3. An optional `"selector"` may appear alongside `"properties"` — see the
[Selectors](#selectors-targeting-specific-data) section below for which
objects require or accept selectors and what shape to use.
4. `objects` contains chart-specific formatting (axes, data colors, legend, labels).
5. `visualContainerObjects` contains container formatting (title, background,
border, shadow). See the VCO section below.
6. Discover valid object and property names with the CLI:
`powerbi-report-author formatting list-objects <visualType>`
`powerbi-report-author formatting describe-object <visualType> <objectName>`
## Literal Values
Most formatting properties use a `Literal` expression wrapper:
```json
{
"expr": {
"Literal": { "Value": "<typedValue>" }
}
}
```
**Value type suffixes:**
| Suffix | Type | Example |
|--------|------|---------|
| `D` | Double/decimal | `"11D"`, `"0.8D"`, `"80D"` |
| `L` | Long/integer | `"1L"`, `"1000000L"` |
| (none) | Boolean | `"true"`, `"false"` |
| `'...'` | String (single-quoted) | `"'Left'"`, `"'Center'"`, `"'Top'"` |
| `'#...'` | Color hex (single-quoted) | `"'#118DFF'"`, `"'#f6c7b9'"` |
## Solid Color Fill
```json
{
"solid": {
"color": {
"expr": {
"Literal": { "Value": "'#118DFF'" }
}
}
}
}
```
> **Color name → hex mapping:** When a user specifies a color by name (e.g.
> "green", "red", "blue") without an explicit hex code, use the **standard
> CSS/HTML named-color hex value**. Common mappings:
>
> | Name | Hex | | Name | Hex |
> |------|-----|-|------|-----|
> | red | `#FF0000` | | green | `#008000` |
> | blue | `#0000FF` | | yellow | `#FFFF00` |
> | orange | `#FFA500` | | purple | `#800080` |
> | black | `#000000` | | white | `#FFFFFF` |
> | gray / grey | `#808080` | | lime | `#00FF00` |
>
> Note: CSS "green" is `#008000`, **not** `#00FF00` (which is "lime").
## Theme Data Color Reference
References a color from the active theme palette:
```json
{
"solid": {
"color": {
"expr": {
"ThemeDataColor": {
"ColorId": 4,
"Percent": 0.6
}
}
}
}
}
```
- `ColorId`: 0-based index into the theme's `dataColors` array.
- `Percent`: Lightness adjustment. 0 = base, positive = lighter, negative = darker.
## Selectors (Targeting Specific Data)
Selectors control which data a formatting entry applies to. Each object array
entry can have an optional `selector` that targets specific data points.
### Selector Types and Precedence
Resolution order (highest to lowest priority):
| Priority | Type | Syntax | Use Case |
|----------|------|--------|----------|
| 1 | **data** (scope identity) | `"data": [{"scopeId": {Comparison...}}]` | Color a specific category value (charts only) |
| 2 | **data** (wildcard) | `"data": [{"dataViewWildcard": {"matchingOption": N}}]` | All instances, totals, or both |
| 3 | **metadata** | `"metadata": "Table.Field"` | Target a specific measure/column |
| 4 | **id** | `"id": "default"` | User-defined instance (cards, filter cards) |
| 5 | **none** (static) | *(no selector)* | Base/fallback for objects that use selectors; the only mode for visual-wide objects (axes, legend, VCOs) |
Within each priority row, **first match in array order wins**.
### No Selector (Static / Base)
```json
{ "properties": { "show": true } }
```
Base formatting for all data points. Any other selector overrides this.
### Metadata Selector
```json
{
"properties": { "fill": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FF0000'" } } } } } },
"selector": { "metadata": "financials.Revenue" }
}
```
Targets a specific field by its queryName (`Table.Field` or `Sum(Table.Field)`).
Common for per-series colors in `dataPoint.fill`.
### DataViewWildcard Selector
```json
{
"properties": { "color": { "solid": { "color": { "expr": { "Literal": { "Value": "'#333'" } } } } } },
"selector": {
"data": [{ "dataViewWildcard": { "matchingOption": 1 } }]
}
}
```
**`matchingOption` values:**
| Value | Constant | Meaning |
|-------|----------|---------|
| `0` | InstancesAndTotals | Both data rows and subtotal/total rows |
| `1` | InstancesOnly | Regular data instances only (not subtotals) |
| `2` | TotalsOnly | Only subtotal and grand total rows |
**`highlightMatching`** (optional, on the selector itself):
| Value | Meaning |
|-------|---------|
| `0` | ValuesOnly — apply to non-highlighted only (default) |
| `1` | ValuesAndHighlight — apply to both |
| `2` | HighlightsOrValues — highlighted if exists, else non-highlighted |
### Scope Identity Selector
Targets a specific data point value (e.g., "Electronics" in Category).
Works on chart visuals only (bar, column, pie, donut, line, area, scatter,
treemap, funnel).
```json
{
"properties": { "fill": { "solid": { "color": { "expr": { "Literal": { "Value": "'#E66C37'" } } } } } },
"selector": {
"data": [{
"scopeId": {
"Comparison": {
"ComparisonKind": 0,
"Left": { "Column": { "Expression": { "SourceRef": { "Entity": "Product" } }, "Property": "Category" } },
"Right": { "Literal": { "Value": "'Electronics'" } }
}
}
}]
}
}
```
> ⚠️ **Only `ComparisonKind: 0` (Equal) is honored.** Other comparison kinds
> (1–4) pass schema validation but are silently ignored at render. Values ≥ 5
> cause schema validation errors. For compound conditions, use rules-based
> `Conditional.Cases[]` (see `conditional-formatting.md` Type 2).
Highest priority among data selectors. Used for per-category color assignment.
### ID Selector (Instance Selector)
```json
{
"properties": { "fontSize": { "expr": { "Literal": { "Value": "28D" } } } },
"selector": { "id": "default" }
}
```
User-defined instance identifier. Several visual types require id selectors for
their formatting objects to take effect. Run
`powerbi-report-author formatting list-objects <type>` to see which objects
need selectors — they're annotated inline.
**Common id values by visual type:**
| Visual Type | ID Values | Objects Affected |
|---|---|---|
| `cardVisual` | `"default"` | outline, accentBar, fillCustom, shape, label, value, layout, spacing, padding, divider, image, shadowCustom, glowCustom, referenceLabelTitle/Value/Detail |
| `pageNavigator` / `bookmarkNavigator` / `actionButton` | `"default"`, `"hover"`, `"selected"`, `"disabled"` | fill, outline, text, icon, shadow, glow, accentBar, image, value, label, background, padding |
| `advancedSlicerVisual` | `"default"`, `"hover"`, `"press"`, `"selected"`, `"mixed"` | fill, outline, text, accentBar, background, label, padding, spacing, selectionIcon, expansionIcon |
| `pivotTable` | `"Row"`, `"Column"` | subTotals |
| `filterCard` (page-level) | `"Available"`, `"Applied"` (PascalCase required) | filterCard |
> The CLI provides this data automatically:
> `powerbi-report-author formatting describe-object <type> <object>` shows
> `_selectorHint` when an object requires id selectors.
> `powerbi-report-author formatting list-objects <type>` annotates objects that
> need selectors.
#### Dual-Entry Pattern
Objects with `id` selectors always require at least the entry with the `id` selector.
Some visual types (`actionButton`, `pageNavigator`, `bookmarkNavigator`) require
**two** array entries — one static (no selector) and one with the `id` selector.
For `cardVisual` and `shape` objects, the entry with the `id` selector alone is
sufficient (the static entry is redundant but harmless).
**Example: actionButton fill (dual entry required)**
```json
"fill": [
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"fillColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#00FF00'" } } } } },
"transparency": { "expr": { "Literal": { "Value": "0D" } } }
}
},
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"fillColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#00FF00'" } } } } },
"transparency": { "expr": { "Literal": { "Value": "0D" } } }
},
"selector": { "id": "default" }
}
]
```
**Example: cardVisual accentBar (single entry sufficient)**
```json
"accentBar": [
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"color": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FF0000'" } } } } },
"transparency": { "expr": { "Literal": { "Value": "0L" } } }
},
"selector": { "id": "default" }
}
]
```
> ⚠️ **Without the `id` selector, property overrides are silently dropped.**
> The object still renders but with its default theme appearance instead of
> the JSON-specified values. No error is produced. If formatting has no effect,
> check `powerbi-report-author formatting describe-object <type> <object>` for
> a `_selectorHint` on that object.
### Which Objects Support Selectors?
| Object | Supports | Selector Types |
|--------|----------|---------------|
| `dataPoint` | ✅ | `metadata`, `data` (wildcard + scope identity) |
| `labels` | ✅ | `data`, `metadata` |
| `columnFormatting` | ✅ | `metadata` |
| `values` | ✅ | `metadata`, `data` |
| `filterCard` | ✅ | `id` (`"Applied"`, `"Available"`) |
| cardVisual objects (16) | ✅ | `id` (`"default"`) — see table above |
| navigator/button objects (12) | ✅ | `id` (4 states) — see table above |
| slicer objects | ✅ | `id` (5 states) — see table above |
| shape objects | ✅ | `id` (`"default"`) — single entry sufficient |
| `legend` | ❌ | **none** — omit `selector` |
| `categoryAxis` / `valueAxis` | ❌ | **none** — omit `selector` |
| `title`, `background`, `border` (VCO) | ❌ | **none** — omit `selector` |
**Rule**: Data-bound objects support `metadata`/`data` selectors. Tile-based
visuals (cardVisual, shape, navigators, slicers) use `id` selectors for
instance targeting. Visual-wide settings (axes, legends, VCOs) do not support
selectors.
## Visual Container Objects (VCO)
Format the visual container itself (not chart data). Located **inside `visual`**
as a sibling of `objects` — NOT as a top-level property of the visual.json root.
```text
visual.json root
├── name, position
└── visual
├── visualType, query
├── objects ← chart-specific formatting
└── visualContainerObjects ← container formatting (title, background, etc.)
```
### Auto-Generated Subtitles
When you set a VCO `title` on a chart, PBI also auto-generates a
**subtitle** from the bound field names (e.g., "Sales and Profit by Date").
Set the subtitle state in the same pass as the title. If the subtitle repeats
the title or exposes raw field names, it creates visual noise. Keep or author a
subtitle when it adds context the title cannot carry cleanly (time window,
active filter, units, comparison baseline, caveat); hide it when it is
redundant.
```json
"visualContainerObjects": {
"title": [{ "properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"text": { "expr": { "Literal": { "Value": "'Revenue by Region'" } } }
}}],
"subTitle": [{ "properties": {
"show": { "expr": { "Literal": { "Value": "false" } } }
}}]
}
```
> ⚠️ If you see duplicate titles in screenshots (your custom title + an
> auto-generated field-name subtitle below it), set `subTitle.show` to false or
> replace it with a hand-authored contextual subtitle.
### Page Objects vs Visual Container Objects
Page objects (`page.json → objects`) and VCOs (`visual.json → visualContainerObjects`)
share some names but have **different valid properties**:
| Object | Page (`page.json`) | VCO (`visual.json`) |
|--------|--------------------|---------------------|
| `background` | `color`, `image`, `transparency` — **NO `show`** | `show`, `color`, `transparency` |
| `outspace` | `color`, `image`, `transparency` | *(not a VCO)* |
| `outspacePane` | 12 filter pane properties | *(not a VCO)* |
| `filterCard` | 8 properties per Applied/Available state | *(not a VCO)* |
| `title` | *(not a page object)* | `show`, `text`, `fontColor`, `fontSize`, etc. |
| `border` | *(not a page object)* | `show`, `color`, `radius`, `width` |
Do not mix page and VCO property lists — use `powerbi-report-author validate`
to catch mismatches (`PBIR_FORMATTING_OBJECT_UNKNOWN`,
`PBIR_FORMATTING_PROP_UNKNOWN`).
### VCO Property Reference
There are **15 VCO keys**, shared across all visual types:
title, subTitle, divider, spacing, background, padding, lockAspect, general,
border, dropShadow, visualLink, visualTooltip, stylePreset, visualHeader,
visualHeaderTooltip.
Discover them with `powerbi-report-author formatting list-vcos`. For property
details, see the CLI table in the preamble.
## Color Strategy & Patterns
For color overrides on chart data points — when to use theme `dataColors`,
`dataPoint.defaultColor`, and per-series `dataPoint.fill` with `metadata`
selectors, plus the cross-visual measure-color consistency pattern — see
[`color-strategy.md`](color-strategy.md).
## Conditional Formatting
For data-driven formatting (FillRule color gradients, rules-based formatting,
icon sets, data bars, web URLs, field values) — including the `expr` wrapper
rule inside FillRule color stops and the `dataViewWildcard` selector pattern
for table/matrix conditional formatting — see
[`conditional-formatting.md`](conditional-formatting.md).
## Shape Visual Formatting
For shape-object discovery, available shapes, and formatting, see
[`shape.md` § Available Shapes and Formatting](shape.md#available-shapes-and-formatting).
## Line & Marker Formatting (lineStyles / markers)
For line stroke properties (width, style, dash cap, line join, interpolation),
marker properties (shape, size, border, rotation), and the per-series metadata
selector pattern for line/area/scatter charts, see
[`cartesian.md` § lineStyles](cartesian.md#linestyles--line-specific) and
[`cartesian.md` § markers](cartesian.md#markers--marker-styling).
## Row Banding (Table & Matrix)
For row banding (`backColorPrimary` / `backColorSecondary`), the full
table/matrix region map (`values`, `columnHeaders`, `rowHeaders`, `total`,
`subTotals`), the **critical style preset rule** (`stylePreset` must be set to
`'None'` for custom row colors to render), and the `backColor` vs
`backColorPrimary` distinction, see
[`table.md` § Row Banding](table.md#row-banding-table--matrix).
## Page-Level Formatting (`page.json` objects)
For canvas background, wallpaper (`outspace`), and page-level background
images, see [`page-formatting.md`](page-formatting.md). For filter pane
(`outspacePane`) and filter card states (Applied / Available), see
[`filter-pane.md`](filter-pane.md).
## Background Images — Routing
When the user requests a "background image," route based on the target:
| User says | Target | Reference |
|-----------|--------|-----------|
| "background image" while creating/modifying a chart visual | `visual.objects.plotArea.image` | [`image.md` § Plot Area Background Image](image.md#plot-area-background-image-plotareaimage) |
| "page background image" / "canvas background" | `page.json → objects.background.image` | [`page-formatting.md` § Background Images](page-formatting.md#background-images) |
| "background image" with no visual context | Ask the user to clarify — page canvas or visual plot area | — |
**⚠️ Both visual plot areas and page backgrounds use the nested `image.image` structure** (`image.image.name`, `image.image.url`, `image.image.scaling`). A flat `image.name/url/scaling` will silently fail to render.
For the `image` object on image visuals themselves (border, background,
corner-radius routing between `objects.image` and VCOs), see
[`image.md` § Image Formatting](image.md#image-formatting-objectsimage).
## References
- [`formatting-overview.md`](formatting-overview.md) — cascade resolution order (visual → VCO → page → custom theme → base theme → defaults), encoding rules, and the Theme JSON vs PBIR encoding comparison table.
- [`theming.md`](theming.md) — `theme.json` authoring: dataColors, textClasses, visualStyles, dark-mode checklist.
- [`conditional-formatting.md`](conditional-formatting.md) — gradients, rules, field values, and the six conditional formatting types.
- [`table.md`](table.md), [`shape.md`](shape.md), [`cartesian.md`](cartesian.md), [`image.md`](image.md), [`card.md`](card.md) — visual-type-specific formatting details.
references/image.md
# Image Visual Authoring Guide
Create and configure image visuals in PBIR format. Covers all three source types
and links to formatting reference for styling.
## Table of Contents
- [Source Types Overview](#source-types-overview)
- [1. Local File Image](#1-local-file-path-to-the-image)
- [2. URL Image](#2-a-url-from-the-web)
- [3. Data-Bound Image](#3-select-from-data)
- [Image Formatting (`objects.image`)](#image-formatting-objectsimage)
- [Plot Area Background Image (`plotArea.image`)](#plot-area-background-image-plotareaimage)
---
## Source Types Overview
The report could be a new report or an existing report that might have pages and visuals already added, there could be images already in the resources folder and registered. In any case, do not assume the source type and source of the image. Please follow the below steps:
The input to the image to render the image visual could be from any of the following sources
1. Local file path to the image
2. A url (from the web)
3. Select from data
If the user has not specified the source or just asks to add a new image, prompt the user with the source options. Do NOT even render an empty image visual layout, prompt for the source first unless the user mentions to render only the layout specifically.
Each source has a different structure and different set of properties.
### 1. Local file path to the image
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 16, "y": 16, "z": 0, "height": 104, "width": 304, "tabOrder": 0 },
"visual": {
"visualType": "image",
"objects": {
"general": [{
"properties": {
"imageUrl": {
"expr": {
"ResourcePackageItem": {
"PackageName": "RegisteredResources",
"PackageType": 1,
"ItemName": "<filename_in_RegisteredResources>.<ext>"
}
}
}
}
}]
},
"drillFilterOtherVisuals": true
}
}
```
**⚠️ Important**: Image visuals use `ResourcePackageItem` expression (NOT a URL literal).
The image file must exist in `StaticResources/RegisteredResources/` and be registered
in `report.json` → `resourcePackages[]`. If the image already exists,
then do not add a duplicate copy, use the existing one.
**Filename convention**: When copying an image to `RegisteredResources/`, append a unique numeric
suffix (e.g., a timestamp) to the base filename before the extension — this replicates Power BI
Desktop's behavior and avoids name collisions. The original filename is preserved as the display
name in `image.image.name` (for background images) or `ResourcePackageItem.ItemName` references
use the suffixed name.
| Concept | Example |
|---------|---------|
| Original file | `screenshot2.png` |
| File on disk in RegisteredResources | `screenshot217123456789012345.png` |
| `report.json` resource `name` & `path` | `screenshot217123456789012345.png` |
| `ResourcePackageItem.ItemName` | `screenshot217123456789012345.png` |
| `image.image.name` (display name) | `'screenshot2.png'` |
**Registration in `report.json`** — add a `RegisteredResources` entry (or append to an existing one):
```json
"resourcePackages": [
{
"name": "RegisteredResources",
"type": "RegisteredResources",
"items": [
{
"name": "my_image17123456789012345.png",
"path": "my_image17123456789012345.png",
"type": "Image"
}
]
}
]
```
- **`type` must be `"Image"`** — not `"ResourceItem"` or any other value (fails schema validation).
- **All filenames must include the file extension** (e.g., `.png`, `.jpg`, `.svg`). Omitting the extension changes the file type and breaks image rendering.
- **`name`** is the identifier used by `ResourcePackageItem.ItemName` in the visual — it must match exactly.
- **`path`** is the filename on disk in `StaticResources/RegisteredResources/`.
- **`ResourcePackageItem.ItemName`** in the visual must match the `name` field in the resource package entry.
### 2. A url (from the web)
**⚠️ URL image requirements**:
- The URL must be **publicly accessible** (anonymous access, no sign-in required). Test by opening in a browser incognito window.
- The URL must point **directly to an image file** (e.g., ending in `.jpg`, `.png`, `.gif`, `.bmp`, `.svg`), not a webpage containing the image.
- Use **HTTPS** URLs — HTTP may work in Desktop but can be blocked by the Power BI Service.
- Reference: https://community.fabric.microsoft.com/t5/Desktop/Image-URL-creation/td-p/1910627
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 16, "y": 16, "z": 0, "height": 104, "width": 304, "tabOrder": 0 },
"visual": {
"visualType": "image",
"objects": {
"image": [
{
"properties": {
"sourceType": {
"expr": {
"Literal": {
"Value": "'imageUrl'"
}
}
},
"sourceUrl": {
"expr": {
"Literal": {
"Value": "'<url_to_the_image_location>'"
}
}
}
}
}
]
},
"drillFilterOtherVisuals": true
}
}
```
### 3. Select from data
For data-bound image URLs, the column or measure must have **Data Category set to "Image URL"** in the semantic model.
> ⚠️ **Validate the field before creating a data-bound image visual.**
> Before creating ANY data-bound image visual:
> 1. Search the TMDL file for the **specific field the user requested** and check whether it has `dataCategory: ImageUrl`.
> 2. If the field does **not** have `dataCategory: ImageUrl`, warn the user before proceeding — the visual will render blank or error, and the user may not know this requirement. Present alternatives (other ImageUrl fields, local file, URL, or adding `dataCategory: ImageUrl` in the semantic model) and confirm the user's choice before creating.
> 3. Proceed when the field has `dataCategory: ImageUrl`, or after the user has chosen an alternative.
**Before creating a data-bound image visual**, always inspect the TMDL files and search for `dataCategory: ImageUrl`. This tells you which fields can render as images.
- **Requested field lacks ImageUrl** → **Warn the user before creating.** The requested field (e.g., `Revenue`) is not image-capable because it has no `dataCategory: ImageUrl`. Suggest alternatives: (1) use a field that does have `dataCategory: ImageUrl` (list any you found), (2) use a local file or URL image instead, or (3) add `dataCategory: ImageUrl` to a column/measure in the semantic model that contains image URLs. Confirm the user's choice before creating.
- **No ImageUrl fields found at all** → Inform the user that the semantic model has no fields with the ImageUrl data category. Suggest the local file or URL alternatives and confirm before creating any data-bound image visual.
- **One ImageUrl field found** → You can safely use that field/measure to render the image.
- **Multiple ImageUrl fields found** → Prompt the user to select which field/measure to use. Do NOT randomly select one unless the user asks to do so.
**From a measure:**
```json
"sourceField": {
"expr": {
"Measure": {
"Expression": {
"SourceRef": {
"Entity": "<TableName>"
}
},
"Property": "<MeasureName>"
}
}
}
```
**From a column** (wrapped in Aggregation with `Function: 3` = Min):
```json
"sourceField": {
"expr": {
"Aggregation": {
"Expression": {
"Column": {
"Expression": {
"SourceRef": {
"Entity": "<TableName>"
}
},
"Property": "<ColumnName>"
}
},
"Function": 3
}
}
}
```
Full visual template (using measure example):
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 16, "y": 16, "z": 0, "height": 280, "width": 280, "tabOrder": 0 },
"visual": {
"visualType": "image",
"objects": {
"image": [
{
"properties": {
"sourceType": {
"expr": {
"Literal": {
"Value": "'imageData'"
}
}
},
"sourceField": {
"expr": {
"Measure": {
"Expression": {
"SourceRef": {
"Entity": "<TableName>"
}
},
"Property": "<MeasureName>"
}
}
}
}
}
]
},
"drillFilterOtherVisuals": true
}
}
```
## Image Formatting (`objects.image`)
Image visuals have formatting properties under the `image` object
(`objects.image`) that control the **image content area** (border, background,
corner radius, effects). Discover these via the CLI:
```bash
powerbi-report-author formatting describe-object image image # image-level properties
powerbi-report-author formatting search image <property> # find which object owns a property
```
### Image object vs Visual Container Objects (VCO) — overlapping properties
Image visuals have **two layers** of formatting that can both be active at the
same time:
| Concern | Image object (`objects.image`) | VCO (`visualContainerObjects`) |
|---------|-------------------------------|-------------------------------|
| **Scope** | The image content area | The outer visual container card (title, subtitle area included) |
| **Border** | `strokeShow/strokeColor/strokePattern/strokeWidth/strokeTransparency` — supports `solid`, `dashed`, `dotted` | `border.show/color/width/radius` — solid only |
| **Background** | `backgroundEnabled/backgroundColor` — behind the image content | `background.show/color/transparency` — behind the entire card |
| **Corner radius** | `cornerRadius` + per-corner variants — rounds the image content | `border.radius` — rounds the outer card |
**⚠️ Routing rule for image visuals** — because both layers have overlapping
concerns (border, background, corner radius), always run
`powerbi-report-author formatting search image <property>` to find the correct
object before setting a value. The `image` object properties control the image
content area; VCO properties control the outer card container.
## Plot Area Background Image (`plotArea.image`)
This is **not** an image visual — it's a background image rendered *behind* the
data area of a chart visual (bar, column, line, etc.) that supports a
`plotArea` formatting object.
> **Routing — "background image" disambiguation:**
> - "background image" in the context of a specific chart visual →
> `visual.objects.plotArea.image` (this section)
> - "page background image" / "canvas background" →
> [`page-formatting.md` § Background Images](page-formatting.md#background-images)
> - User adds an image visual itself → see [Source Types Overview](#source-types-overview) above
Confirm the visual type supports `plotArea` with
`powerbi-report-author formatting search <visualType> "plotArea"`, then inspect
the `image` sub-structure with
`powerbi-report-author formatting describe-object <visualType> plotArea`.
### Example — Local registered resource as plot area background
```json
"plotArea": [{
"properties": {
"transparency": { "expr": { "Literal": { "Value": "0D" } } },
"image": {
"image": {
"name": { "expr": { "Literal": { "Value": "'my-bg-image.png'" } } },
"url": {
"expr": {
"ResourcePackageItem": {
"PackageName": "RegisteredResources",
"PackageType": 1,
"ItemName": "my-bg-image17123456789012345.png"
}
}
},
"scaling": { "expr": { "Literal": { "Value": "'Fit'" } } }
}
}
}
}]
```
**⚠️ Both visual plot areas and page backgrounds use the nested `image.image` structure** (`image.image.name`, `image.image.url`, `image.image.scaling`). A flat `image.name/url/scaling` will silently fail to render.
**⚠️ Registration required:** The referenced image must be copied to
`StaticResources/RegisteredResources/` and registered in `report.json` — same
workflow as the local-file image source above.references/map.md
# Azure Map Visual Authoring Guide
## Overview
Always use `azureMap` as the visual type for map visuals.
**Do not use `map` or `filledMap`** — they are legacy Bing Maps visuals and
must never be created. `powerbi-report-author validate` raises
`PBIR_VISUAL_TYPE_DEPRECATED` (warning) on these types.
### When a Map Fails to Render
Azure Maps can fail to geocode or render for a variety of reasons (unsupported
data format, ambiguous location names, missing coordinates). When this happens:
1. **Debug the problem** — check field names, data values, geocoding compatibility
2. **Try alternative geographic fields or coordinates** — try lat/lon columns, a
more specific location column, or a different aggregation level (e.g., country
instead of city)
3. **Ask the user for clarification** — if you cannot resolve the geocoding issue,
use `ask_user` to describe the problem and ask which field to use or whether
lat/lon columns are available
4. **Do not silently substitute a non-map visual** for data/geocoding issues — if
the user explicitly requested a map and the underlying geography is workable,
use `ask_user` before changing visual types. Substituting a non-map visual for a
resolvable data problem violates the design brief.
5. **When Azure Maps is unavailable in the environment** (for example, disabled by
tenant policy or unsupported region), fall back to a non-map encoding such as a
`tableEx` of locations with conditional formatting or a `clusteredBarChart` by
region, and tell the user why the map was replaced. Avoid the legacy `map`
and `filledMap` visuals as fallbacks; `shapeMap` is a specialized supported
visual for built-in or custom shape-based geographies, not a general-purpose
substitute for Azure Maps.
---
## Template
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 20, "y": 20, "z": 0, "height": 400, "width": 610, "tabOrder": 0 },
"visual": {
"visualType": "azureMap",
"query": {
"queryState": {
"Category": {
"projections": [
{
"field": {
"Column": {
"Expression": { "SourceRef": { "Entity": "<TableName>" } },
"Property": "<LocationColumnName>"
}
},
"queryRef": "<TableName>.<LocationColumnName>",
"active": true
}
]
},
"Size": {
"projections": [
{
"field": {
"Measure": {
"Expression": { "SourceRef": { "Entity": "<TableName>" } },
"Property": "<MeasureName>"
}
},
"queryRef": "<TableName>.<MeasureName>",
"active": true
}
]
}
}
},
"objects": {}
}
}
```
## Roles
| Role | Display Name | Kind | Required | Max |
|------|-------------|------|----------|-----|
| Category | Location | Grouping | ✅ | — |
| Size | Size | Measure | — | 1 |
| Series | Legend | Grouping | — | 1 |
| Y | Latitude | GroupingOrMeasure | — | 1 |
| X | Longitude | GroupingOrMeasure | — | 1 |
| Tooltips | Tooltips | Measure | — | — |
> **Tip**: Bind a geographic column (country, state, city) to `Category`.
> Azure Maps handles geocoding automatically — no explicit lat/lon needed
> unless you have coordinate data.
references/page-formatting.md
# Page-Level Formatting (`page.json` objects)
Page formatting controls the page canvas, wallpaper, and page-level background
images. All stored in `page.json → objects` using PBIR expression encoding.
> **Read first:** [`formatting-overview.md`](formatting-overview.md) for the
> cascade model and encoding rules. For visual-level formatting (inside
> `visual.json`), see [`formatting.md`](formatting.md). For filter pane and
> filter card chrome (also in `page.json` but a separate concern), see
> [`filter-pane.md`](filter-pane.md).
## Contents
- [Canvas Background (`background`)](#canvas-background-background)
- [Wallpaper (`outspace`)](#wallpaper-outspace)
- [Background Images](#background-images)
## Canvas Background (`background`)
The canvas rectangle behind all visuals. This is a **page-level** object —
NOT the same as VCO `background` on visuals.
> **⚠️ Page background has NO `show` property.** It is always visible.
> Only VCO `background` (on visuals) supports `show`. Do not add
> `"show": true/false` to page background — the schema will reject it.
```json
"background": [{
"properties": {
"color": { "solid": { "color": { "expr": { "Literal": { "Value": "'#F5F5F5'" } } } } },
"transparency": { "expr": { "Literal": { "Value": "0D" } } }
}
}]
```
Properties: `color` (fill), `image` (see below), `transparency` (0–100).
## Wallpaper (`outspace`)
The area behind/outside the canvas:
```json
"outspace": [{
"properties": {
"color": { "solid": { "color": { "expr": { "Literal": { "Value": "'#0D1117'" } } } } },
"transparency": { "expr": { "Literal": { "Value": "0D" } } }
}
}]
```
Same properties as `background`: `color`, `image`, `transparency`.
**⚠️ Transparency pitfall**: Canvas background at high transparency (e.g., white at 80%)
creates a translucent overlay — the wallpaper bleeds through creating unexpected
composite colors. For dark themes, set both layers to opaque dark colors.
## Background Images
Both `background` and `outspace` support images. The `image` property uses a **nested `image` sub-object** — the same structure as visual plot area background images (see [`image.md` § Plot Area Background Image](image.md#plot-area-background-image-plotareaimage)).
**⚠️ Both page backgrounds and visual plot areas use the nested `image.image` structure** (`image.image.name`, `image.image.url`, `image.image.scaling`). A flat `image.name/url/scaling` will silently fail to render.
**⚠️ The image must be copied to `StaticResources/RegisteredResources/` and registered in `report.json` — see [`image.md`](image.md) for the registration workflow.**
```json
"background": [{
"properties": {
"image": {
"image": {
"name": { "expr": { "Literal": { "Value": "'my-bg-image.png'" } } },
"url": {
"expr": {
"ResourcePackageItem": {
"PackageName": "RegisteredResources",
"PackageType": 1,
"ItemName": "my-bg-image17123456789012345.png"
}
}
},
"scaling": { "expr": { "Literal": { "Value": "'Fit'" } } }
}
},
"transparency": { "expr": { "Literal": { "Value": "0D" } } },
"color": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FFFFFF'" } } } } }
}
}]
```
**Image fit modes:**
| Value | Behavior |
|-------|----------|
| `'Fit'` | Fit within bounds, preserve aspect ratio (may letterbox) |
| `'Stretch'` | Stretch to fill bounds exactly (may distort) |
| `'Fill'` | Fill bounds, preserve aspect ratio (may crop) |
## See Also
- [`filter-pane.md`](filter-pane.md) — filter pane (`outspacePane`) and
filter card (`filterCard`) appearance, also stored in `page.json → objects`.
references/powerbi-desktop.md
# Power BI Desktop Verification
> Read this when you need to open Power BI Desktop, reload a PBIP/PBIR report,
> capture screenshots, choose a Desktop PID, or interpret `powerbi-desktop` CLI
> output. This file is the detailed runbook; `SKILL.md` keeps the short command
> loop because visual verification is a critical report-authoring step.
## Core Rule
For PBIR edits that affect rendered output, do not rely on JSON validation alone.
Power BI Desktop can reject or visually misrender definitions that are
structurally valid. Use this loop:
1. Edit PBIR files.
2. Run `powerbi-report-author validate "<path-to-.Report-dir>"`.
3. Run `powerbi-desktop status`.
4. Select the intended Desktop instance by PID.
5. Run `powerbi-desktop reload --pid <pid>` for PBIP/PBIR current files. This
workflow is for report-definition changes. For semantic model/TMDL changes,
use the semantic-model skill or Modeling MCP workflow and reopen the PBIP if
model changes are not reflected.
6. Capture screenshots from the same PID.
7. Review screenshots; if anything is wrong, fix PBIR and restart at validation.
## Command Reference
### Setup
Ensure the latest Desktop Bridge CLI is installed globally:
```bash
npm install -g @microsoft/powerbi-desktop-bridge-cli@latest
powerbi-desktop --version
```
### Commands
```bash
powerbi-desktop open "<path.pbip>"
powerbi-desktop status
powerbi-desktop manifest --pid <pid>
powerbi-desktop reload --pid <pid>
powerbi-desktop screenshot <page-id> --pid <pid> --output screenshots/page.png
powerbi-desktop screenshot-all --pid <pid> --output-dir screenshots
```
`open <path>` is the only command that accepts a PBIP/PBIX path. Other commands
target a running Desktop Bridge instance. If exactly one instance is available,
`--pid` may be omitted, but passing the PID is safer and should be preferred in
agent workflows.
No command accepts `--report`. The same PBIP can be open in multiple Desktop
processes, so report path is not a safe selector. Use `status` and choose by PID.
`screenshot <page-id>` takes the PBIR page ID such as `ReportSection1a2b3c`, not the
display name shown in Desktop.
Screenshots default to scale `2` for readable visual review. Pass `--scale 1`
only when you need smaller files or faster captures; pass `--scale 3` only when
you need extra detail and can tolerate larger PNGs.
## Status and PID Selection
Run:
```bash
powerbi-desktop status
```
Use the returned `instances[]` list to choose the PID. Prefer an instance where:
- `bridgeStatus` is `connected`.
- `currentFilePath` matches the target PBIP/PBIX.
- `reportDir` resolves to the expected `.Report` folder for PBIP/PBIR reports.
- `diagnostics` is empty or only contains non-blocking warnings.
If multiple instances are present, never guess. Choose the PID from `status` and
reuse that same PID for `reload`, `screenshot`, and `screenshot-all`.
## Open and Reload
To start Desktop:
```bash
powerbi-desktop open "C:\Reports\Sales\Sales.pbip"
powerbi-desktop status
```
After editing PBIR files:
```bash
powerbi-report-author validate "C:\Reports\Sales\Sales.Report"
powerbi-desktop reload --pid <bridge-pid-from-status>
```
Reload is supported for the selected Desktop instance's current PBIP/PBIR file.
It is intended for report definition changes, not semantic model authoring. If
you changed TMDL/model files, use the semantic-model skill or Modeling MCP
workflow and reopen the PBIP if model changes are not reflected. `reload` is
supported only for PBIP-backed reports; if the selected PID has only a `.pbix`
open, `reload` returns `REPORT_DIR_REQUIRED`. If `reload` returns
`REPORT_DIR_REQUIRED`, choose a PID whose `status` output has a PBIP/PBIR
`currentFilePath` and resolved `reportDir`, or open the target PBIP first.
**Exception — theme JSON cache:** When editing an existing theme JSON file,
Desktop may not pick up the change on reload because theme files are cache-keyed
by file name. Either rename the theme file with a small random suffix and update
its registration in `report.json`, or close and reopen Desktop.
Fix all validation errors before reloading. Reloading invalid PBIR will usually
surface Desktop errors and can leave the report in a broken visual state until
the files are fixed and reloaded again.
## Screenshots
> **Capture scope:** Each screenshot captures the report page **AND** the right-hand filter pane (`outspacePane`) when the filter pane is enabled and expanded. Filter pane and filter card (`filterCard`) chrome are formattable PBIR surfaces — verify them alongside the on-page visuals.
Capture one page when the change is isolated:
```bash
powerbi-desktop screenshot ReportSection1a2b3c --pid <bridge-pid-from-status> --output screenshots/sales.png
```
Capture all pages when the change affects theme, shared formatting, page order,
navigation, or report-wide behavior:
```bash
powerbi-desktop screenshot-all --pid <bridge-pid-from-status> --output-dir screenshots
```
Run reload and screenshot operations serially for a given PID — never in
parallel against the same PID, even as a workaround for a slow or retryable
error. Discovery commands such as `status` and `manifest` are safe to run
concurrently. Parallel reload/screenshot calls produce `Cancelled` and slow
recovery.
Review screenshots for:
- visual error banners, blank visuals, or "Requires X fields" messages;
- clipped card/KPI values, truncated labels, hidden legends, or overlap;
- theme/background/font colors that do not match the requested design;
- insufficient contrast or indistinguishable chart series;
- slicers with no selectable values;
- unexpected blank/null values.
For non-trivial visual changes, use an independent review pass or sub-agent with
the screenshot paths, the expected outcome, and the checklist above.
## Common Outcomes
| Output/error | Meaning | Action |
|--------------|---------|--------|
| `"status": "not_connected"` | No Desktop Bridge instance is discoverable | Open the report with `powerbi-desktop open "<path.pbip>"` or ask the user to start Desktop |
| `NO_BRIDGE` or repeated `connect ENOENT \\.\pipe\pbi-desktop-bridge-<pid>` | Desktop is running, but the bridge pipe is not available | In Desktop, open **File > Options and settings > Options > Preview features**, enable **Enable external tool access to Power BI Desktop through secure local APIs**, restart Desktop, then retry `powerbi-desktop status --wait-seconds 30`. If the bridge is still unavailable, share [Power BI report authoring docs](https://aka.ms/Report_Authoring_skill_LearnDocs) for the current Desktop support story |
| `AMBIGUOUS_DESKTOP_INSTANCE` | More than one bridge instance is available | Run `powerbi-desktop status`, choose the intended PID, and retry with `--pid` |
| `METHOD_NOT_AVAILABLE` | Desktop build lacks a required production bridge method | Tell the user Desktop is stale/unsupported for this workflow and link [Power BI report authoring docs](https://aka.ms/Report_Authoring_skill_LearnDocs) for current support constraints |
| `HostNotReady` or retryable bridge error | Desktop is up but the report host isn't ready for this request yet (often a brief moment right after a reload) | The CLI auto-retries this; you usually won't see it. If you do, rerun the same command once — the host has typically settled. Do not add custom sleeps in agent code; rely on the CLI's retry path. |
| `Timeout` (bridge error) | A reload or screenshot attempt took longer than the CLI's retry budget allowed | Run `powerbi-desktop status` and confirm `bridgeStatus: "connected"`. If connected, rerun the same command once — a transient slow operation usually clears on the next attempt. If `Timeout` persists across two reruns, the report or model is genuinely slow on this machine: rerun with a larger budget, e.g. `powerbi-desktop reload --pid <pid> --wait-seconds 120`. If `status` shows `bridgeStatus: "error"` or stops responding, ask the user whether a Desktop modal dialog is blocking input. |
| `Cancelled` during screenshot/reload | A reload or screenshot was cancelled, usually because another reload/screenshot ran against the same PID concurrently. Distinct from `Timeout` (which means the operation ran too long) | Make sure you are running reload and screenshot serially per PID. Run `status`, wait for `bridgeStatus: "connected"`, then retry one operation at a time. |
| `ReportDefinitionValidationFailed` | Desktop rejected the PBIR definition | Fix PBIR, run `powerbi-report-author validate <path>`, then reload again |
| `REPORT_DIR_REQUIRED` | Selected PID does not expose a PBIP/PBIR current file; reload and screenshot-all need PBIP/PBIR state | Select the correct PID from `status` or open the target PBIP |
| Page not found / empty page screenshot | `<page-id>` used a display name or stale page ID | Read `definition/pages/pages.json` and retry with the PBIR page ID |
## Fix-Retry Pattern
When Desktop reports a load or render error:
1. Read the CLI error and Desktop diagnostic details.
2. Open the referenced PBIR JSON file and fix the offending property, visual, or
page definition.
3. Run `powerbi-report-author validate <path-to-.Report-dir>`.
4. Run `powerbi-desktop reload --pid <same-pid>`.
5. Capture screenshots again from the same PID.
Do not switch PIDs mid-loop unless `status` shows the original Desktop instance
closed or the user explicitly asks you to verify a different Desktop process.
references/powerbi-report-author-cli.md
# `powerbi-report-author` CLI Reference
Use this file when the root command table is not enough. The CLI is the source
of truth for visual roles, formatting objects, property names, enum values,
selectors, expression/value encodings, and PBIR validation.
## Command catalog
| Command | Purpose | When to use |
|---|---|---|
| `--help` / `<command> --help` | Show syntax and available flags | Before using an unfamiliar command or flag |
| `catalog list` | List all built-in visual types and deprecated entries | Choosing a visual type |
| `catalog describe <type>` | Roles, formatting keys, cardinality | Before creating/editing a visual |
| `formatting list-objects <type>` | Valid `objects.*` keys + VCO keys; flags objects needing id selectors | Before applying formatting |
| `formatting describe-object <type> <object>` | Property names, types, enum values, descriptions; `_selectorHint` when id selector required | Finding exact property names and allowed values |
| `formatting describe-property <type> <object> <prop>` | Focused single-property lookup | When you already know the object and want one property |
| `formatting search <type> <regex>` | Regex search across formatting objects + VCOs | When you do not know which object a property belongs to |
| `formatting list-vcos` | Enumerate shared visualContainerObjects | Auditing chrome/container formatting surface |
| `formatting effective-properties <type>` | Flattened visual objects + shared VCOs | One-shot snapshot of every formatting surface for a visual |
| `expr encode --kind <t> <v> [percent]` | Generate correct PBIR value encoding | Writing formatting values |
| `expr decode '<json>'` | Decode a PBIR expression to plain value | Inspecting existing formatted values |
| `theme encode --kind <t> <v>` | Generate the plain-JSON value used inside a theme file | Editing theme JSON |
| `theme shade-color <hex> <percent>` | Apply ThemeDataColor shadeColor adjustment | Previewing tinted/shaded theme colors |
| `validate <path>` | Full validation of a `.pbip` or `.Report` directory | After every batch of PBIR edits |
| `preview-visuals <path> [--with-derived]` | Enumerate every visual with stable summary fields + path to JSON | Auditing visuals across a report |
| `preview-pages <path> [--with-derived]` | Page metadata summary | Quick page overview |
| `preview-filters <path>` | Enumerate report/page/visual filters | Filter audit |
| `preview-themes <path> [--with-derived]` | Registered custom theme summary | Theme audit |
| `doctor` | Environment self-check | First-run setup or troubleshooting |
## Validation result handling
Run `powerbi-report-author validate <path-to-.Report-dir>` after every logical
batch of PBIR edits.
- Fix every error before Desktop reload.
- Review warnings before proceeding. Unknown visual types or theme visual keys
usually mean a typo unless the report intentionally uses a custom `.pbiviz`.
- Diagnostics include file paths and JSON paths. Use them to jump directly to
the broken node.
- For large diagnostics, use `--pretty` for readable output or `--out <file>` to
write the full result to a file.
references/re-theming.md
# Re-theming & Dark Mode — Workflow Reference
> Referenced from SKILL.md and theming.md. Read this when switching an existing
> report to a new theme or applying dark mode to a report with existing visuals.
> For theme JSON structure and creation, read [theming.md](theming.md) first.
When a report already has per-visual formatting overrides (shapes, buttons, page
backgrounds, cards), changing the theme JSON alone does not propagate colors to
those overrides — they sit above the theme in the cascade. A re-theming
operation updates both the theme file AND sweeps inline overrides in a single
atomic step.
> **When is the full workflow needed?** Reports with explicit per-visual `objects`
> or `visualContainerObjects` color properties need the sweep. A freshly-created
> report with no inline color overrides (all colors inherited from theme) can be
> re-themed by editing the theme file alone — the cascade will propagate changes.
> When unsure, grep `definition/` for hex color values from the old theme — if
> any appear outside the theme file, the sweep is needed.
## Table of Contents
- [Re-theming an Existing Report](#re-theming-an-existing-report)
- [Why Theme Changes Don't Fully Propagate](#why-theme-changes-dont-fully-propagate)
- [Re-theming Workflow](#re-theming-workflow) — Steps 0–4
- [Preventive Authoring: Theme-Adaptive Visuals](#preventive-authoring-theme-adaptive-visuals)
- [Dark Mode Authoring Checklist](#dark-mode-authoring-checklist)
## Re-theming an Existing Report
When switching a report to a new theme (especially light → dark or dark → light),
changing `theme.json` alone is **not sufficient** if visuals have per-visual
`objects` or `visualContainerObjects` color properties. Those per-visual colors
override the theme cascade and remain unchanged.
### Why Theme Changes Don't Fully Propagate
The formatting cascade (formatting-overview.md) resolves as:
```
Priority 1: Conditional formatting ← highest, always wins
Priority 2: Per-visual objects ← overrides theme
Priority 3: Visual container objects (VCO) ← overrides theme
Priority 4: Page objects
Priority 5: Theme visualStyles[type] ← only applies if no per-visual override
Priority 6: Theme visualStyles["*"]
Priority 7: Base theme
Priority 8: System defaults
```
If a visual has **any** explicit `objects` or `visualContainerObjects` color
properties, those win over the new theme. The visual appears unchanged
despite the theme switch.
**Key insight**: If an `objects` group has even ONE explicit property (e.g.,
`columnAdjustment: 'growToFit'`), ALL color properties in that same group
stop inheriting from the theme. You must add/update explicit colors for
every property in that group.
### Re-theming Workflow
> **⚠️ POLARITY CHANGE GATE — Read this FIRST:**
> Determine whether the theme switch changes polarity (dark → light or
> light → dark). A polarity change means foreground/text colors that were
> designed for contrast against the old background MUST flip too:
>
> - **Dark → Light**: Light text (`#F9FAFB`, `#E6EDF3`, `#FFFFFF`) → dark
> text (`#1F2937`, `#252423`, `#333333`)
> - **Light → Dark**: Dark text (`#1F2937`, `#252423`, `#333333`) → light
> text (`#F9FAFB`, `#E6EDF3`, `#FFFFFF`)
>
> These foreground colors are hardcoded as `Literal` values on shapes,
> textboxes, slicer items/headers, card values, and nav buttons. The theme
> cascade does NOT override them. **If you omit foreground colors from your
> color mapping and sweep, text becomes invisible** — light-on-light or
> dark-on-dark.
>
> Same-polarity switches (dark → dark, light → light) may share foreground
> values, but include them in the mapping when they differ — different text
> shades produce a more cohesive result. The sweep of accent/background colors
> is always needed — shapes, nav buttons, accent bars, and chart borders
> commonly hardcode `dataColors[0]` and other accent hex values as Literal
> fills/outlines that do not auto-resolve via the theme cascade.
#### Step 0: Build a color mapping table
Before touching any files, extract all hex colors from the **old** theme JSON
and map each to its replacement in the **new** theme. Include ALL categories:
- **Foreground/text colors** (`foreground`, `firstLevelElements`,
`secondLevelElements`, `textClasses` colors) — critical for polarity changes
(visibility), but also include for same-polarity changes when the new theme
uses different text shades for a more cohesive result
- Structural colors (`background`, `secondaryBackground`, `thirdLevelElements`)
- `dataColors` array (index-by-index) — **CRITICAL even for same-polarity
changes.** Shapes, nav buttons, accent bars, and chart borders commonly
hardcode `dataColors[0..N]` hex values as Literal fills/outlines. These do
NOT auto-resolve via `ThemeDataColor` or the theme cascade. Grep `definition/`
for every old `dataColors` value.
- `tableAccent`, `good`/`neutral`/`bad`, `maximum`/`center`/`minimum`
- `visualStyles` colors (borders, gridlines, accent colors, backgrounds)
- **⚠️ Page-level background/outspace colors** — these are NOT in the theme
file but are hardcoded in `page.json` files. For polarity switches in either
direction, the page background retains the old theme's color (e.g., dark→light:
a dark hex like `#1A1A2E` remains; light→dark: a light hex like `#FFFFFF`
remains). **Grep all `page.json` files for their current `background` and
`outspace` color values and include them in the mapping.** This is the second
most commonly missed category after foreground text.
- **⚠️ Slicer `items.background` and `header.background`** — on polarity
changes, slicer dropdown backgrounds are hardcoded Literal values at
Priority 2. Light→dark: set dark. Dark→light: set light/white.
**⚠️ Slicer header.background requires SEMANTIC mapping, not accent mapping:**
In dark themes, slicer headers typically use a colored/accent background bar
(e.g., `#3730A3`) with white text. In light themes, slicer headers
conventionally have a **white or transparent background** with dark text —
NOT the new accent color. If you naively map the old dark header background
to the new theme's accent color, you get a heavy colored bar that looks
wrong in a light context. Map dark-theme slicer `header.background` to
`#FFFFFF` (or remove the property to inherit) for dark→light changes.
- **⚠️ Azure Map basemap style** — `azureMap.objects.mapControls.defaultStyle`
is a hardcoded enum, not a hex color, so bulk color replacement will never
update it. For polarity switches: light→dark use `night`, `grayscale_dark`,
or `high_contrast_dark`; dark→light use `road` or `grayscale_light`.
```
Old hex → New hex Category
#1F2937 → #F9FAFB FOREGROUND
#0F172A → #FFFFFF BACKGROUND
#1E293B → #F8FAFC SECONDARY BG
#3B82F6 → #2563EB ACCENT (dataColors[0])
#312E81 → #FFFFFF VISUAL CARD BG (semantic: cards are white in light themes)
#3730A3 → #FFFFFF SLICER HEADER BG (semantic: no colored bar in light themes)
#6366F1 → #BBF7D0 BORDER (semantic: soft/muted border for light themes)
```
Extract actual hex values from the old and new theme JSON files. Every color
that differs needs a row — including foreground colors when they differ between
old and new themes.
> **⚠️ SEMANTIC ROLE AWARENESS — critical for polarity changes:**
> Not every color maps 1:1 from old theme to new theme. Some colors serve a
> structural role that changes meaning across polarities:
>
> - **Colored header/accent backgrounds** (slicer headers, table column headers
> with solid fills): In dark themes these are typically mid-tone accent bars
> for visual separation. In light themes, the same role is usually served by
> white/transparent backgrounds — the accent color moves to borders or text
> instead. Map these to `#FFFFFF` for dark→light.
> - **Visual card backgrounds**: Dark themes use dark card fills. Light themes
> use white (`#FFFFFF`), not the new theme's background color (which is for
> the page canvas, not cards).
> - **Border/accent colors**: Dark themes use bright borders for contrast.
> Light themes typically use soft/muted borders (e.g., light green `#BBF7D0`
> instead of saturated `#16A34A`).
>
> Build a two-column mapping where the **target** color reflects the
> **semantic role** in the new polarity, not just index-matching from old→new
> theme properties.
#### Step 1: Update theme JSON AND bulk-replace all old colors (single atomic step)
> **Do NOT reload Desktop until this entire step is complete.**
**1a. Update the custom theme JSON by writing a new GUID-suffixed file** —
any change to the active
`StaticResources/RegisteredResources/<CustomThemeName>-<guid>.json` file
requires a new GUID, regardless of what changed (colors, fonts,
`visualStyles`, metadata, schema, whitespace, or any other content). Do not edit
the existing GUID-suffixed theme file in place and keep the same filename.
**Keep the same `<CustomThemeName>`** — only rotate the GUID suffix per the
[GUID convention in theming.md](theming.md#theme-name-guid-convention-cache-busting).
Do NOT rename `<CustomThemeName>` (e.g., `DarkCoral` stays `DarkCoral` even if
the new palette is light green) unless the user explicitly asks to rename it.
Update all `report.json` references to match the new GUID. For dark themes,
update ALL structural colors together (see § Structural Colors above).
**1b. Bulk-replace old-theme hex values across the entire `definition/`
directory** using the color mapping from Step 0.
**Sweep category checklist — include ALL applicable rows from Step 0:**
- [ ] Background colors (page canvas, card/VCO backgrounds)
- [ ] Secondary/tertiary background colors
- [ ] Border/divider/gridline colors
- [ ] Accent/data colors
- [ ] **Foreground/text colors** — MANDATORY for polarity changes (dark↔light).
These are the inline `fontColor`, `text.fontColor`, `labelColor`,
`textRuns[].textStyle.color` values on shapes, slicers, cards, textboxes,
and nav buttons. Omitting this category guarantees invisible text.
- [ ] **Slicer `items.background` and `header.background`** — MANDATORY for
polarity changes. Dropdown backgrounds are hardcoded Literal values that
don't inherit from theme. Light→dark: set dark. Dark→light: set light.
**For `header.background` specifically**: dark→light should map to
`#FFFFFF` (not the new accent), because light-theme slicers use
transparent/white headers. See § Semantic Role Awareness above.
- [ ] **`#FFFFFF` VCO backgrounds on all visuals** — MANDATORY for light→dark.
`#FFFFFF` has dual meaning in light themes (card backgrounds AND text on
colored shapes). Exclude it from the bulk sweep; instead scan all
`visual.json` for `#FFFFFF` in `visualContainerObjects.background.color`
and replace with the dark card color. Leave `#FFFFFF` in text contexts
(shape `fontColor`, textbox `textRuns` color). See § 1b-extra-2 below.
- [ ] **Azure Map `mapControls.defaultStyle`** — MANDATORY for polarity
changes. The basemap style is an enum (`night`, `road`,
`grayscale_dark`, etc.), not a color, so it is missed by hex sweeps.
**1b-extra. Pages without explicit `objects.background` (polarity changes):**
During polarity changes, pages that have **no** `objects.background` property
at all will default to white (system default) regardless of the theme's
`background` value. The bulk hex-sweep cannot fix these because there is no
color value to replace. After the bulk sweep, scan all `page.json` files for
pages missing `objects.background` entirely and add one with the new theme's
`background` value. See [formatting-overview.md § Page Objects](formatting-overview.md)
for the JSON structure.
**1b-extra-2. Remaining `#FFFFFF` VCO backgrounds (light→dark):**
In light themes, `#FFFFFF` is used for **both** visual card/VCO backgrounds
(which must become dark) **and** text colors on colored shapes/title bars
(which must stay white for contrast). The bulk hex-sweep cannot distinguish
these two roles, so `#FFFFFF` must be excluded from the main sweep and handled
separately.
After the bulk sweep, scan all `visual.json` files for `#FFFFFF` inside
`visualContainerObjects.background.color` properties and replace with the new
theme's card background color (e.g., `#312E81`). This applies to **all** visual
types — charts (donut, bar, column, line, pie, area), cards, tables, maps, and
any other visual with an explicit white VCO background.
Do **NOT** replace `#FFFFFF` in these text contexts — they need white for
contrast against colored fills:
- `objects.text.fontColor` (shape visuals)
- `objects.general.paragraphs[].textRuns[].textStyle.color` (textboxes)
- `visualContainerObjects.title.fontColor` (when title sits on a colored bar)
> **Why this is commonly missed:** The theme wildcard `visualStyles["*"]["*"].background`
> sets card backgrounds for visuals that inherit from theme. But visuals with
> **any** explicit `visualContainerObjects.background` property (even just
> `show: true` or `transparency`) stop inheriting — they retain whatever `color`
> value they have. In light themes that value is almost always `#FFFFFF`.
```bash
# Grep the report definition tree for ALL old-theme hex values (including foreground!)
grep -rn "<old.background>\|<old.secondaryBg>\|<old.border>\|<old.accent>\|<old.foreground>" <report>.Report/definition/
# Replace systematically (prefer JSON-aware tooling or edit tool over regex):
# For each old_hex → new_hex pair in the mapping, replace across all JSON files
# in definition/ — this covers pages, visuals, and VCOs in one sweep.
```
**1c. Confirm zero old-theme colors remain:**
```bash
grep -rn "#EDE9FE\|#F5F3FF\|..." <report>.Report/definition/
# Expected: no matches
```
Only after confirming zero matches should you proceed to Step 2.
#### Step 2: Audit per-visual formatting for gaps
This step catches cases the bulk sweep cannot fix — visuals that **lack** a
color property entirely (relying on theme inheritance that may now produce wrong
results) or that need new properties added.
> **Bulk hex-replacement is insufficient** when a visual *lacks* a color
> property entirely. Shape `text` objects commonly omit `fontColor` (relying on
> theme inheritance), so a find-and-replace pass won't touch them. After bulk
> replacement, scan for shapes with `text.show: true` that have no explicit
> `fontColor` and add one.
**Quick-reference — properties to audit per visual type:**
| Visual Type | Key color properties to check | Details |
|---|---|---|
| `tableEx` / `pivotTable` | `columnHeaders.fontColor/backColor`, `values.fontColorPrimary/Secondary`, `values.backColorPrimary/Secondary`, `rowTotal.fontColor/backColor`, `columnTotal.fontColor/backColor` | [table.md](table.md) |
| `cardVisual` | `value.fontColor`, `label.fontColor`, `fillCustom` (all need `{ id: "default" }`) | [card.md](card.md) |
| Bar/column/line charts | `categoryAxis.labelColor`, `valueAxis.labelColor`, `labels.color`, `gridlineColor` | [cartesian.md](cartesian.md) |
| `azureMap` | `mapControls.defaultStyle` (enum), marker/bubble colors | [map.md](map.md) |
| `slicer` / `filterSlicer` / `advancedSlicerVisual` | `header.fontColor/background`, `items.fontColor/background` | [slicers.md](slicers.md) |
| `textbox` | `textRuns[].textStyle.color` inside `paragraphs` array | [textbox.md](textbox.md) |
| `shape` | `fill.color`, `line.color`, `text.fontColor` (**must be explicit** if `text.show: true`) | [shape.md](shape.md) |
**VCO properties to check on every visual** (regardless of type):
`background.color`, `border.color`, `title.fontColor`, `title.background`,
`subTitle.fontColor`, `divider.color`, `dropShadow.color`,
`visualHeader.background/foreground`.
**CLI commands for discovery:**
```bash
powerbi-report-author formatting list-objects <visualType>
powerbi-report-author formatting describe-object <visualType> <objectName>
```
Properties with `type: "fill"` are the ones that need color adjustment.
> **Critical for tables/matrices**: If you set custom row colors, you MUST also
> set `stylePreset` to `'None'` — otherwise the default preset overrides your
> colors. See [table.md § Style Presets](table.md#style-presets-for-tables).
#### Step 3: Ensure contrast and visibility
For every visual with explicit formatting, verify foreground/background contrast:
- Dark background → light text; light background → dark text
- Chart axis labels/legend must contrast with VCO background (or page canvas)
- Slicer items must contrast with slicer body background
- Chart series colors (`dataPoint.fill` or `dataColors`) must be saturated/vivid —
dark/muted colors blend into dark backgrounds making bars/lines invisible
#### Step 4: Validate and verify
Follow the standard Edit → Validate → Reload → Screenshot loop.
### Preventive Authoring: Theme-Adaptive Visuals
To minimize re-theming effort when building new reports:
1. **Minimize explicit color properties** — let the theme cascade handle defaults.
2. **Use `ThemeDataColor` references** for adaptive colors:
`{ "ThemeDataColor": { "ColorId": 0, "Percent": 0.4 } }`
3. **Set table/chart colors via theme `visualStyles`** when possible.
4. **Document your color mapping** for systematic future switches.
> **Use `ThemeDataColor`** for: VCO `background`, `border`, `title.fontColor`,
> `values.backColorPrimary/Secondary`, `columnHeaders.backColor/fontColor`.
>
> **Use `Literal` hex** for: `dataPoint.fill` with `metadata` selectors,
> `FillRule` gradient stops (ThemeDataColor silently breaks these).
> **Key insight**: If an `objects` group exists on a visual (even for a non-color
> property like `columnAdjustment`), color properties in that same group will NOT
> inherit from the theme — you must add explicit color properties to any group
> that already has explicit entries. Use `ThemeDataColor` so they stay adaptive.
## Dark Mode Authoring Checklist
Dark mode triggers every formatting trap simultaneously. Follow this checklist
to avoid multiple iteration rounds debugging silent failures.
> **Existing report?** If applying dark mode to a report that already has
> visuals with per-visual formatting, you MUST also follow
> [§ Re-theming Workflow](#re-theming-workflow) (Steps 0–4) to sweep hardcoded
> colors. The theme alone will NOT update per-visual overrides. After the
> sweep, verify these common symptoms are resolved:
>
> | Symptom | Cause | Fix |
> |---------|-------|-----|
> | Chart card stays white/light | VCO `background.color` set to light hex | Update to dark color or remove to inherit |
> | Page background white on dark theme | `page.json` has no `objects.background` (defaults to white) | Add explicit `objects.background` with dark color |
> | Shape retains old accent | `objects.fill.fillColor` hardcoded | Update to dark-appropriate color |
> | Slicer dropdown stays white | `objects.items.background` not set (defaults to white) | Set `items.background` to dark color |
> | Slicer text invisible | `objects.items.fontColor` dark on dark bg | Set to light color |
> | Card/textbox text invisible | `fontColor` or `textStyle.color` dark | Set to light color (with `{ id: "default" }` for cards) |
> | Axis labels invisible | `labelColor` set dark | Update to light color |
> | VCO title invisible | `title.fontColor` dark | Update to light color |
### Step 1: Theme structural colors
Set ALL structural colors together in `theme.json` (see [theming.md § Structural Colors](theming.md#4-structural-colors)):
`background`, `foreground`, `firstLevelElements`, `secondLevelElements`,
`tableAccent`, `secondaryBackground`, `dataColors` array.
> Missing any one structural color creates invisible text or clashing chrome.
### Step 2: Page canvas backgrounds
Set `page.json → objects.background.color` on **every page** to the dark canvas
color. The page background does NOT inherit from the theme's `background`
structural color — it must be set explicitly per page.
See [formatting-overview.md § Page Objects](formatting-overview.md) for the JSON structure.
### Step 3: Filter pane + filter cards
The filter pane does **NOT** inherit from structural colors — set `outspacePane`
and `filterCard` (with `"$id": "Applied"` and `"$id": "Available"`) in
`visualStyles["*"]["*"]`. See [filter-pane.md](filter-pane.md).
### Step 4: Slicer entries in `visualStyles`
Modern slicers (`filterSlicer`, `advancedSlicerVisual`) do **NOT** inherit from
the legacy `"slicer"` key. Add entries for **all three** slicer types — set
`items.background`, `items.fontColor`, `header.background`, `header.fontColor`.
> Per-visual `objects` (Priority 2) override `visualStyles`. Include old slicer
> hex values in your Step 0 color mapping so the sweep updates them.
See [slicers.md](slicers.md) for full `visualStyles` slicer JSON templates.
### Step 5: Azure Map basemap style
`objects.mapControls.defaultStyle` is an enum, not a color — bulk sweeps miss it.
- Light theme: `road` or `grayscale_light`
- Dark theme: `night`, `grayscale_dark`, or `high_contrast_dark`
### Step 6: Table/matrix `stylePreset` = `'None'`
Style presets override explicit row/header colors with white/gray backgrounds.
Set `stylePreset` to `'None'` on every `tableEx`/`pivotTable` with custom colors.
See [table.md § Style Presets](table.md#style-presets-for-tables).
### Step 7: Visual-specific dark mode properties
| Visual | What to set | Reference |
|--------|-------------|-----------|
| `tableEx` / `pivotTable` | `values.backColorPrimary/Secondary`, `values.fontColorPrimary/Secondary`, `columnHeaders.backColor/fontColor`, `rowTotal.fontColor/backColor`, `columnTotal.fontColor/backColor` | [table.md](table.md) |
| `cardVisual` | `fillCustom` with `{ id: "default" }` selector, `value.fontColor`, `label.fontColor` | [card.md](card.md) |
| `shape` | `text.fontColor` (explicit — shapes with `text.show: true` but no `fontColor` inherit wrong color after polarity switch) | [shape.md](shape.md) |
| `textbox` | `textRuns[].textStyle.color` per run | [textbox.md](textbox.md) |
### Step 8: Contrast audit
Verify all text-bearing properties have adequate contrast against their
backgrounds. Key properties per type:
| Visual Type | Properties to verify |
|---|---|
| `tableEx` / `pivotTable` | `values.fontColorPrimary/Secondary`, `columnHeaders.fontColor`, `rowTotal.fontColor`, `columnTotal.fontColor` |
| `cardVisual` | `value.fontColor`, `label.fontColor` (with `{ id: "default" }`) |
| Charts | `categoryAxis.labelColor`, `valueAxis.labelColor`, `legend.labelColor` |
| Slicers | `items.fontColor`, `header.fontColor` |
Also verify chart series colors are saturated/vivid — dark/muted `dataColors`
blend into dark backgrounds making bars and lines invisible.
references/screenshot-review.md
# Screenshot Review
Use this file after `powerbi-desktop` screenshot capture and before reporting completion for any rendered-output change.
## Review workflow
Perform an independent screenshot review using:
1. The screenshot image file paths.
2. A short description of what changed and what should be visible.
3. The checklist and troubleshooting table below.
Do not rely only on structural validation. Fix any issue found in screenshots, then repeat the PBIR validate → Desktop reload → screenshot review loop.
> **Screenshot scope:** Each screenshot captures the report page **AND** the right-hand filter pane (`outspacePane`) when the filter pane is enabled and expanded. Filter pane and filter card (`filterCard`) chrome are formattable surfaces — review them alongside on-page visuals (background color, border, font color, search/checkbox treatment, applied vs available states). See [filter-pane.md](filter-pane.md) for the formatting model.
## Checklist
### Layout and visibility
- Are all visuals fully visible with no edge clipping or unintentional overlap?
- Does each page have a visible descriptive title/header?
- Do top-bar slicers sit to the right of the title instead of replacing the title anchor?
- Are titles, labels, values, subtitles, and legends readable and not truncated?
- Do cards and KPIs show complete values, not ellipses or missing digits?
- Do textboxes render without scrollbars?
- Are slicers fully visible, including lower portions and dropdown areas?
- Is spacing intentional, with no accidental large gaps?
### Data rendering
- Do charts show actual bars, lines, or points rather than empty frames?
- Do tables and matrices have data rows, not just headers?
- Are any visuals showing error icons, "Can't display", or "Requires X fields"?
- Are blank, null, or dash values expected?
- Do slicers show selectable values?
### Formatting and theming
- Are background colors and theme applied, not default white/gray?
- Do font colors contrast against their backgrounds?
- Do chart colors contrast with card/page backgrounds and distinguish series?
- Does each measure follow `Design Brief.color_map` consistently across visuals?
- Are card gutters/padding intentional, with no accidental white padding from wildcard theme styles?
- Is conditional formatting visually present where expected?
- Do border radius, shadows, and other effects render as intended?
> **Note:** Some formatting properties are valid JSON but have no visible effect for a visual type. If a property is not rendering, verify it with `powerbi-report-author formatting describe-object <type> <object>`.
## Common screenshot problems
| Symptom | Likely cause | Fix |
|---|---|---|
| Visual shows error icon | Wrong entity/property names in `queryState` | Check TMDL for correct table/column names. |
| Chart frame with no data | Missing or wrong role bindings | Verify roles with `powerbi-report-author catalog describe <type>`. |
| Card/KPI value shows `...` or cutoff | Font too large for visual size, or card resized without font adjustment | Read `position.height`/`width`, then increase size or reduce `value.fontSize`; recheck `label.fontSize` and padding. |
| Textbox shows scrollbar or clipped title | Textbox height did not account for theme/VCO padding | Increase height with `max(18, ceil(fontSize * 25/16)) + padding_top + padding_bottom`, or add textbox-specific zero padding. |
| Redundant subtitle appears | Auto-generated or hand-authored subtitle repeats the title | Hide it or replace with useful context such as date range, units, active filter, baseline, or caveat. |
| Text invisible on dark background | Font color matches background | Set explicit contrasting `fontColor`. |
| Visual overlaps another | Position coordinates conflict | Recalculate `x`, `y`, `width`, and `height`. |
| Slicer hidden behind chart/table | Header band overlaps next row, or slicer height is too small | Re-read `references/slicers.md`, recompute height, and reserve a full top/header band or rail. |
| Card has white gutters/padding | Wildcard theme padding/background applied to cards | Add/restore `cardVisual`-specific padding, spacing, and background overrides. |
| Same measure uses different colors | `Design Brief.color_map` was not applied consistently | Re-read the brief and set each measure-bound visual to the mapped color. |
| Blank page | Page has no visuals, or visuals have `z < 0` | Check visual directories and z-order. |
| Slicer shows no items | Wrong column binding or filter conflict | Verify the slicer's `queryState` column has data. |
| Bars/columns invisible despite data | `dataPoint.fill` without a selector | Use `defaultColor` for base color, or add a `metadata` selector to `fill`. |
references/shape.md
# Shape Visual
<!-- TOC -->
- [Basic Example (Rectangle Divider)](#basic-example-rectangle-divider)
- [Container Shapes](#container-shapes)
- [Available Shapes and Formatting](#available-shapes-and-formatting)
- [Shape Text Caveat](#shape-text-caveat)
- [Complete Example (Arrow with All Formatting)](#complete-example-arrow-with-all-formatting)
<!-- /TOC -->
Use `shape` for decorative, non-data elements such as dividers, accent lines,
or colored bars. Shapes respect small dimensions accurately — unlike textbox
visuals, which enforce a minimum rendered height of approximately 24px.
## Basic Example (Rectangle Divider)
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 0, "y": 50, "z": 10001, "height": 4, "width": 1280, "tabOrder": 1 },
"visual": {
"visualType": "shape",
"objects": {
"shape": [{ "properties": { "tileShape": { "expr": { "Literal": { "Value": "'rectangle'" } } } } }],
"fill": [{
"properties": {
"fillColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#118DFF'" } } } } },
"transparency": { "expr": { "Literal": { "Value": "0D" } } }
},
"selector": { "id": "default" }
}],
"outline": [{
"properties": { "show": { "expr": { "Literal": { "Value": "false" } } } },
"selector": { "id": "default" }
}]
},
"visualContainerObjects": {
"background": [{ "properties": { "show": { "expr": { "Literal": { "Value": "false" } } } } }],
"border": [{ "properties": { "show": { "expr": { "Literal": { "Value": "false" } } } } }],
"padding": [{ "properties": {
"top": { "expr": { "Literal": { "Value": "0D" } } },
"bottom": { "expr": { "Literal": { "Value": "0D" } } },
"left": { "expr": { "Literal": { "Value": "0D" } } },
"right": { "expr": { "Literal": { "Value": "0D" } } }
} }]
}
}
}
```
> **Tip:** Set padding to 0 and hide background/border to ensure the shape
> fills its position exactly — important for thin divider lines.
## Container Shapes
When using a shape as a **layout container** (e.g., a navigation bar, header
strip, or grouping rectangle behind other visuals), choose the fill color and
transparency to **match the reference image** while ensuring text/visuals
layered on top remain readable:
1. **If the reference shows a visible colored container** (e.g., a dark header
bar, a blue accent strip), set `fillColor` to the reference color and
`transparency` to `0D` (or whatever matches the reference opacity). Use
`z: 0` for the container shape and higher `z` for content on top.
2. **If the container is invisible** (same color as page background, or purely
for grouping), either omit the shape entirely or set `transparency` to
`100D`.
3. **If the page background already provides the desired color**, the container
shape is unnecessary — skip it to avoid layering issues.
> **⚠️ Always set explicit `fontColor` on shape text:** When a shape has
> `text.show: true`, always include an explicit `fontColor` in the
> `{ selector: { id: "default" } }` text entry. Without it, the text color
> inherits from the theme's foreground — which may not contrast with the
> shape's `fill` color after a theme switch (e.g., a white pill button on a
> light canvas becomes invisible). This is the most commonly missed property
> during re-theming because bulk hex-replacement only updates existing colors,
> not missing ones.
```json
// Visible container (e.g., dark header bar on a black page)
"fill": [{
"properties": {
"fillColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#1E1E1E'" } } } } },
"transparency": { "expr": { "Literal": { "Value": "0D" } } }
},
"selector": { "id": "default" }
}]
```
> **⚠️ Text contrast pitfall:** Setting `transparency` to `100D` makes the
> shape invisible. If text or visuals layered on top use a color that matches
> the **page canvas background** (not the shape), that content becomes
> unreadable. Example: white canvas + blue container shape + white text →
> making the shape 100% transparent turns the text invisible (white on white).
> Always verify that text color contrasts with whatever is visible behind it
> after transparency is applied.
>
> **⚠️ Don't add unnecessary shapes:** When recreating a UI layout from a
> reference image, check whether the page background already provides the
> needed color. If the reference shows a black page with content directly on
> it, set the page `background.color` and place visuals directly — do not
> add a redundant container shape that could block content or cause layering
> issues.
## Available Shapes and Formatting
Discover shape types (`tileShape` enum) and shape-specific geometry parameters:
```bash
powerbi-report-author formatting list-objects shape
powerbi-report-author formatting describe-object shape <object>
```
Shape objects that require `id` selectors need only the **single entry with the
`id` selector** — the static (no-selector) entry is redundant but harmless.
The CLI annotates these with `(selector: default)` in
`powerbi-report-author formatting list-objects` output and `_selectorHint` in
`powerbi-report-author formatting describe-object` output.
### Rotation
The `rotation` object is the **only** shape formatting object that does **not**
require a selector. It supports three independent angles:
```json
"rotation": [{
"properties": {
"shapeAngle": { "expr": { "Literal": { "Value": "78L" } } },
"angle": { "expr": { "Literal": { "Value": "136D" } } },
"textAngle": { "expr": { "Literal": { "Value": "92L" } } }
}
}]
```
- `shapeAngle` (integer L) — rotates the shape geometry
- `angle` (numeric D) — rotates the entire visual container
- `textAngle` (integer L) — rotates the text label independently
> **⚠️ Textbox minimum height:** Do not use textbox visuals as thin decorative
> lines. PBI Desktop enforces a minimum rendered height (~24px) regardless of
> the `height` value in `position`. Use a shape visual instead.
## Shape Text Caveat
Shape `text` properties can validate successfully but still fail to render
reliably in Desktop for prominent page-level titles or headers. For visible page
titles, use a `textbox` visual with the native `paragraphs` array structure from
`textbox.md`; use a separate `shape` behind it only when you need a title
panel, accent bar, or background container.
Reserve shape text for simple labels only after screenshot verification confirms
that the text is visible. When shape text is used, always set an explicit
`fontColor` and verify contrast against the rendered fill/background.
## Complete Example (Arrow with All Formatting)
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 100, "y": 290, "z": 1, "height": 300, "width": 280, "tabOrder": 1 },
"visual": {
"visualType": "shape",
"objects": {
"shape": [{
"properties": {
"tileShape": { "expr": { "Literal": { "Value": "'arrow'" } } },
"roundEdge": { "expr": { "Literal": { "Value": "6L" } } },
"arrowheadSize": { "expr": { "Literal": { "Value": "51L" } } },
"arrowStemWidth": { "expr": { "Literal": { "Value": "13L" } } }
}
}],
"rotation": [{
"properties": {
"shapeAngle": { "expr": { "Literal": { "Value": "78L" } } },
"angle": { "expr": { "Literal": { "Value": "136D" } } },
"textAngle": { "expr": { "Literal": { "Value": "92L" } } }
}
}],
"fill": [{
"properties": {
"fillColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 8, "Percent": 0.2 } } } } },
"transparency": { "expr": { "Literal": { "Value": "22D" } } }
},
"selector": { "id": "default" }
}],
"outline": [{
"properties": {
"lineColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 6, "Percent": 0 } } } } },
"weight": { "expr": { "Literal": { "Value": "4D" } } },
"transparency": { "expr": { "Literal": { "Value": "22D" } } }
},
"selector": { "id": "default" }
}],
"text": [
{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } }
}
},
{
"properties": {
"text": { "expr": { "Literal": { "Value": "'Arrow Shape Visual'" } } },
"fontFamily": { "expr": { "Literal": { "Value": "'Georgia'" } } },
"fontSize": { "expr": { "Literal": { "Value": "8D" } } },
"bold": { "expr": { "Literal": { "Value": "true" } } },
"fontColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 7, "Percent": 0.2 } } } } },
"horizontalAlignment": { "expr": { "Literal": { "Value": "'right'" } } },
"verticalAlignment": { "expr": { "Literal": { "Value": "'middle'" } } },
"topMargin": { "expr": { "Literal": { "Value": "9L" } } },
"leftMargin": { "expr": { "Literal": { "Value": "6L" } } },
"rightMargin": { "expr": { "Literal": { "Value": "5L" } } },
"bottomMargin": { "expr": { "Literal": { "Value": "4L" } } }
},
"selector": { "id": "default" }
}
],
"shadow": [
{ "properties": { "show": { "expr": { "Literal": { "Value": "true" } } } } },
{
"properties": {
"color": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 4, "Percent": 0 } } } } },
"transparency": { "expr": { "Literal": { "Value": "18D" } } },
"shadowBlur": { "expr": { "Literal": { "Value": "55D" } } },
"shadowPositionPreset": { "expr": { "Literal": { "Value": "'topRight'" } } }
},
"selector": { "id": "default" }
}
],
"glow": [
{ "properties": { "show": { "expr": { "Literal": { "Value": "true" } } } } },
{
"properties": {
"color": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 7, "Percent": 0.6 } } } } },
"transparency": { "expr": { "Literal": { "Value": "20D" } } }
},
"selector": { "id": "default" }
}
]
},
"visualContainerObjects": {
"title": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"text": { "expr": { "Literal": { "Value": "'Arrow Visual'" } } },
"heading": { "expr": { "Literal": { "Value": "'Heading3'" } } },
"italic": { "expr": { "Literal": { "Value": "true" } } },
"fontColor": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 5, "Percent": 0.2 } } } } },
"background": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 4, "Percent": 0.6 } } } } },
"alignment": { "expr": { "Literal": { "Value": "'center'" } } }
}
}],
"subTitle": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"text": { "expr": { "Literal": { "Value": "'Arrow Visual'" } } },
"alignment": { "expr": { "Literal": { "Value": "'center'" } } }
}
}],
"divider": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"color": { "solid": { "color": { "expr": { "ThemeDataColor": { "ColorId": 8, "Percent": -0.5 } } } } },
"style": { "expr": { "Literal": { "Value": "'dashed'" } } },
"width": { "expr": { "Literal": { "Value": "2D" } } }
}
}],
"spacing": [{
"properties": {
"customizeSpacing": { "expr": { "Literal": { "Value": "false" } } },
"verticalSpacing": { "expr": { "Literal": { "Value": "2D" } } }
}
}],
"padding": [{
"properties": {
"top": { "expr": { "Literal": { "Value": "5D" } } },
"left": { "expr": { "Literal": { "Value": "6D" } } },
"right": { "expr": { "Literal": { "Value": "6D" } } },
"bottom": { "expr": { "Literal": { "Value": "5D" } } }
}
}],
"lockAspect": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } }
}
}],
"visualLink": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"tooltip": { "expr": { "Literal": { "Value": "'Arrow Visual Type'" } } }
}
}]
},
"drillFilterOtherVisuals": true
}
}
```
references/slicers.md
# Slicer Authoring Guide
> **Always read first** when adding/modifying slicers or slicer selections.
> For expression reference, see **references/expressions.md**
Slicers provide interactive filtering. This guide covers creation,
formatting defaults, and selection configuration for all slicer types.
- [Slicer Authoring Guide](#slicer-authoring-guide)
- [Recommended Defaults](#recommended-defaults)
- [Slicer Template](#slicer-template)
- [Sizing](#sizing)
- [Fill variant](#fill-variant)
- [Date Between Slicer Template](#date-between-slicer-template)
- [Add/Modify a Slicer](#addmodify-a-slicer)
- [Slicer types](#slicer-types)
- [Setting slicer selections](#setting-slicer-selections)
- [Slicer Sync Groups](#slicer-sync-groups)
- [Theme Approach](#theme-approach)
- [Discovering Properties](#discovering-properties)
<a id="per-visual-vco-override-caveat"></a>
> ⚠️ **Per-visual VCO override caveat**: As soon as a slicer declares **any**
> `visualContainerObjects` entry (background, border, title, visualHeader,
> etc.), Power BI stops inheriting the theme's `*.*.padding` cascade for that
> visual and resets its inner padding to **0**. The chrome (header label +
> dropdown box) then sits flush against the visual border — no breathing
> room — and the bottom row of an inline slicer can visibly clip even though
> the height formula said it would fit.
>
> **Fix**: every slicer that sets *any* per-visual VCO must also **declare a
> `padding` VCO explicitly** — the bug is *omitting* `padding`, not the value
> itself. Use the theme's `8/8/8/8` for normal slicers, or `0/0/0/0` for the
> [fill variant](#fill-variant) (where the white background must reach the
> container edges). Recompute `h` via the [Sizing](#sizing) formula whenever
> the value changes. The base template below already includes `padding` — keep
> it even if you remove other VCOs.
---
## Recommended Defaults
Three slicer visual types exist in PBIR, each with different query roles
and capabilities:
- `slicer` — supports `data.mode` (Dropdown, Basic, Between, Single, etc.)
- `listSlicer` — scrollable list with tooltips and hierarchy support
- `advancedSlicerVisual` — tile/button layout, single field only
Before changing a slicer's `data.mode`, `position.height`, font size, padding,
background/border VCO, or theme chrome, re-run the sizing rules in this file.
Do not fix clipping by shrinking to `h=48` or 8pt text; resize the slicer and
its reserved band/rail instead.
### Slicer Template
All slicer types share this base structure. Adapt `visualType`, query
roles, and `data.mode` per type (see [Slicer types](#slicer-types) below).
> **`height` value below**: derived from `60 + top_padding + bottom_padding`
> snapped to 8px (see [Sizing](#sizing)). The `80` shown matches the skill's
> default theme padding (`*.*.padding = 8/8`). If your theme uses zero
> padding, drop to `64`; if it uses `10/10` (common in dark/card forks),
> keep `80` (still fits, since 60+20=80 lands on the grid).
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<unique-id>",
"position": { "x": 24, "y": 72, "z": 1000, "height": 80, "width": 160, "tabOrder": 1000 },
"visual": {
"visualType": "slicer",
"query": {
"queryState": {
"Values": {
"projections": [{
"field": { "Column": { "Expression": { "SourceRef": { "Entity": "<table>" } }, "Property": "<column>" } },
"queryRef": "<table>.<column>",
"nativeQueryRef": "<column>"
}]
}
}
},
"objects": {
"data": [{ "properties": { "mode": { "expr": { "Literal": { "Value": "'Dropdown'" } } } } }],
"header": [{ "properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"text": { "expr": { "Literal": { "Value": "'<Display Name>'" } } }
}}]
},
"visualContainerObjects": {
"padding": [{ "properties": {
"top": { "expr": { "Literal": { "Value": "8D" } } },
"bottom": { "expr": { "Literal": { "Value": "8D" } } },
"left": { "expr": { "Literal": { "Value": "8D" } } },
"right": { "expr": { "Literal": { "Value": "8D" } } }
}}]
}
}
}
```
> **Why `padding` is in the template even though no other VCOs are set yet**:
> the moment you add **any** VCO (background, border, title.show=false,
> visualHeader.show=false, etc.) Power BI drops the theme `*.*.padding`
> cascade for that visual and zeros it. Keeping `padding` always-on makes
> the template safe to extend without re-introducing the bug. If your theme
> uses a different default (e.g. `10/10/10/10` in dark forks), match that
> value here and resize `h` per the formula in [Sizing](#sizing).
Rules applied to ListSlicer (default for **list slicer** requests):
1. visualType is listSlicer.
2. ONLY "Values" and "Tooltips" allowed under queryState. "Values" only allow Column or Hierarchy Expressions. "Tooltips" only allow Measure or Aggregation Expressions.
Rules applied to ButtonSlicer (default for **tile slicer** requests):
1. visualType is advancedSlicerVisual.
2. "Values" only allow one field with Column Expression. "Label" only allow one field with Measure or Aggregation Expression. "Tooltips" can have multiple fields with Aggregation Expressions.
Rules applied to Slicer (classic — default for **all other slicer** requests):
1. visualType is slicer. To make it a list slicer, set the Value inside of the mode as 'Basic'. To make it a dropdown slicer, set it to 'Dropdown'.
2. ONLY "Values" allowed under queryState and "Values" only allow Column/Hierarchy Expressions.
3. "data" under "objects" only available for slicer.
General rules applied to all slicers:
1. Only slicer with Basic/Dropdown mode and listSlicer can be hierarchy slicers.
2. The filter property under general in objects describes value selections.
3. The expansionStates only available for hierarchy slicers and only needed when the slicer is expanded.
4. identityKeys only defined on the first level of the hierarchy (the level whose nodes can be expanded to reveal children). Not defined on leaf levels. identityValues defined inside root.children[] only when a specific node has been toggled open (expanded)
5. Adding a field to the `Tooltips` queryState role is **necessary but not sufficient** to show tooltips. You must **also** set `visualContainerObjects.visualTooltip.show = true` — the tooltip panel is off by default and the user will see nothing on hover without it:
```json
"visualContainerObjects": {
"visualTooltip": [
{ "properties": { "show": { "expr": { "Literal": { "Value": "true" } } } } }
]
}
```
With the theme applied, this template is complete for the **light variant**
(border from theme, no fill). The theme handles header font, items font,
border, and visual header.
> **Check `header.text`** — if the field name from the model is already
> human-readable (e.g., "Weight Class"), no override needed. If it's raw
> like "weight_class_name", set `header.text` to a clean display name.
### Sizing
Slicer height depends on the selected `data.mode` (or the visual type for
`listSlicer` / `advancedSlicerVisual`). The wrong height is the most common
cause of clipped items at the bottom of the visual — Power BI does **not**
auto-grow the container and refuses to render partial rows, so the last
item silently disappears when the math is off.
**Width** (mode-independent): 160px standard, 120px for short labels
(Year, Stance), 216px for `'Between'` date pickers (side-by-side dates).
> **No height/font shortcuts**: if a slicer collides with the next row or clips
> on a dark/fill theme, increase the reserved band/rail and recompute `h`.
> Do not lower header/items/date text below 9pt or force `h=48` to make the
> layout fit.
**Height by mode:**
| Mode / `visualType` | Height | Notes |
|---|---|---|
| `slicer` mode `'Dropdown'` | **`h = 60 + top_padding + bottom_padding`**, snap up to the next 8px. Worked values: zero padding → **h=64**; theme default `8/8` → **h=80**; dark-card `10/10` → **h=80**. The 60px chrome = `header (~28px at 10pt Semibold) + dropdown selector field (~32px)`. | Items render in a popup, not inline, so item count doesn't affect height. The padding stays *outside* the visible chrome — every padding pixel must be added to `h`. **Verify the layout below the slicer leaves room for the new height** (e.g. if a fork bumps padding from 8 to 10, recompute and shift the next-row visuals). |
| `slicer` mode `'Between'` / `'Before'` / `'After'` | **`h = 60 + top + bottom`** side-by-side (same chrome as Dropdown — two date pickers fit on one row). Stacked vertical = **`h = 84 + top + bottom`** (two date pickers on two rows). | See [Date Between Slicer Template](#date-between-slicer-template). |
| `slicer` mode `'Basic'` / `'Single'` | **Use the formula below** | Items render inline; height must cover header + search box + every visible row. |
| `listSlicer` | **Use the formula below** | Same inline-list behavior as Basic mode. Scrolls when items exceed available area, but the bottom row still clips if height < `chrome + 1 row`. |
| `advancedSlicerVisual` | **≥ 56px per tile row** (add padding the same way) | ≤10 tiles; size by number of tile rows × tile height. |
**Inline-list height formula** (Basic / Single / `listSlicer`):
```text
height = top_padding + bottom_padding
+ header_height (≈ 32px when header.show = true; 0 when hidden)
+ search_box_height (≈ 32px when items > ~10; 0 otherwise)
+ (visible_items × row_height)
+ 8 (safety margin — PBI rounds row heights and
hides any partial row at the bottom)
```
Defaults you can plug in:
| Term | Default value |
|---|---|
| `top_padding` + `bottom_padding` | **20px** total when VCO padding is 10/10 (the value used by most fill/dark themes); **16px** when VCO padding is left at the theme's `*` default of 8/8. |
| `header_height` | **32px** at `header.textSize = 10pt` (the skill default). Add 4px per +1pt above 10pt. |
| `row_height` | **24px** at `items.textSize = 9pt` with `items.padding = 2`. Add 4px per +1pt of items text size, and add `2 × items.padding` per row for any padding above 2. |
| `search_box_height` | **32px** when shown. Set `searchBox.show = false` to recover this space if the slicer has few items. |
> **Worked example:** any 5-item slicer with skill defaults (`header` 10pt,
> `items` 9pt, `padding` 2), VCO padding 10/10, header visible, search box
> visible →
> `20 + 32 + 32 + (5 × 24) + 8` = **212px**. Round up to the 8px grid → **216px**.
> The same slicer at h=56 (the chrome-only Dropdown minimum, before
> padding) **will clip every item but the first**.
> ⚠️ **VCO padding eats item area, not chrome.** Setting
> `visualContainerObjects.padding` to anything > 0 (common when adding a
> `background` or `border`) shrinks the inner content rect *before* PBI
> lays out the header/search/items. Always re-run the formula after
> changing padding. Any non-zero VCO `padding` (e.g. 10/10/10/10 used by
> many fill/dark themes) subtracts directly from the available item area.
> ⚠️ **Theme-level `*.*` padding cascades to slicers.** The most common
> source of slicer clipping is a theme that sets
> `visualStyles."*"."*".padding` (e.g. the skill's default base theme uses
> `8/8/8/8`; many fork themes bump this to `10/10/10/10` for a more dramatic
> dark-card aesthetic). Every visual — including slicers — inherits it via
> the formatting cascade. **Always size slicer `h` to match the theme
> padding**: with theme padding `8/8`, use `h=80`; with `10/10`, use `h=80`
> (snapped); with `0/0`, use `h=64`. Re-run the formula above whenever you
> fork the base theme and change the global padding. Don't strip the
> padding from slicers as an escape — the breathing room around the header
> is part of the visual frame.
> ⚠️ **Don't switch `data.mode` from `'Dropdown'` to `'Basic'` without
> resizing.** The default 56px dropdown height fits exactly one inline
> row's worth of chrome — every row of data will be clipped. Always
> recalculate height when changing mode.
>
> `powerbi-report-author validate` flags slicers whose
> `position.height` is below the per-mode floor with
> `PBIR_SLICER_HEIGHT_BELOW_FLOOR` (warning). To inventory every slicer's mode
> and current height before resizing, run
> `powerbi-report-author preview-visuals <path>` and filter on
> `visualType` ∈ `slicer` / `listSlicer` / `advancedSlicerVisual` (both
> `visualType` and `position` are in the default output; `--with-derived`
> only adds `hasFilters`, `hasFormatting`, `hasVCOs`).
- Snap all coordinates to the 8px grid (round the formula result *up*).
### Fill variant
For the card-like white background (top strip placement), add VCO to
the template. The explicit `padding = 0/0/0/0` below is **deliberate**: the
white `background` fill is only painted *inside* the VCO padding rect, so
any non-zero padding leaves a transparent ring around the fill and the page
background bleeds through, breaking the solid-card look. This `0` override
is **not** a violation of the
[VCO override caveat](#per-visual-vco-override-caveat) — the caveat
requires `padding` to be *declared explicitly*, not to match any specific
value. When you use this variant, drop the dropdown template's `h=80` to
**`h=64`** (`60 + 0 + 0`, snapped) so the chrome still fits.
```json
"visualContainerObjects": {
"background": [{ "properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"color": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FFFFFF'" } } } } },
"transparency": { "expr": { "Literal": { "Value": "0D" } } }
}}],
"padding": [{ "properties": {
"top": { "expr": { "Literal": { "Value": "0D" } } },
"bottom": { "expr": { "Literal": { "Value": "0D" } } },
"left": { "expr": { "Literal": { "Value": "0D" } } },
"right": { "expr": { "Literal": { "Value": "0D" } } }
}}]
}
```
---
### Date Between Slicer Template
For temporal filtering, use the `slicer` visual in `Between` mode only when
users need arbitrary date-range exploration and the bound field is a renderable
Date/DateTime column. For executive dashboards or annual/quarterly grain,
prefer a compact Year/Period dropdown or tile.
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<unique-id>",
"position": { "x": 1040, "y": 8, "z": 1000, "height": 80, "width": 216, "tabOrder": 1000 },
"visual": {
"visualType": "slicer",
"query": {
"queryState": {
"Values": {
"projections": [{
"field": { "Column": { "Expression": { "SourceRef": { "Entity": "<table>" } }, "Property": "<date_column>" } },
"queryRef": "<table>.<date_column>",
"nativeQueryRef": "<date_column>"
}]
}
}
},
"objects": {
"data": [{ "properties": { "mode": { "expr": { "Literal": { "Value": "'Between'" } } } } }],
"header": [{ "properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"text": { "expr": { "Literal": { "Value": "'Date Range'" } } }
}}]
},
"visualContainerObjects": {
"padding": [{ "properties": {
"top": { "expr": { "Literal": { "Value": "8D" } } },
"bottom": { "expr": { "Literal": { "Value": "8D" } } },
"left": { "expr": { "Literal": { "Value": "8D" } } },
"right": { "expr": { "Literal": { "Value": "8D" } } }
}}]
}
}
}
```
**Sizing**:
- Inline with title: **`w=216, h = 60 + top + bottom`** — dates render
side-by-side. With theme default `8/8` padding that's `h=80`; with zero
padding `h=64`. This is the minimum width for side-by-side dates.
- Vertical rail: **`w=200, h = 84 + top + bottom`** — dates stack vertically
at narrow widths.
Fill and light variants are the same as the dropdown slicer (see above).
---
## Add/Modify a Slicer
### Slicer types
| Type | `visualType` | Query roles | `data.mode` | Notes |
|------|-------------|-------------|-------------|-------|
| **Dropdown** | `slicer` | `Values` only (Column/Hierarchy) | `'Dropdown'` | Any cardinality, compact; default for executive Year/Period filters |
| **Date range** | `slicer` | `Values` only (Date/DateTime column) | `'Between'` | Date picker with range; use only for arbitrary date-range exploration |
| **Single** | `slicer` | `Values` only (Column) | `'Single'` | Single-select |
| **Before** | `slicer` | `Values` only (Date/Numeric) | `'Before'` | Upper bound only (≤) |
| **After** | `slicer` | `Values` only (Date/Numeric) | `'After'` | Lower bound only (≥) |
| **Relative date** | `slicer` | `Values` only (Date column) | `'Relative'` | "Last N days/months/years" — needs `data.relativeRange`, `relativePeriod`, `relativeDuration` + `dateRange.includeToday` + `general.filter` with DateSpan/DateAdd |
| **Relative time** | `slicer` | `Values` only (DateTime column) | `'RelativeTime'` | "Last N minutes/hours" — uses `data.relativeTimePeriod` instead of `relativePeriod` |
| **Scrollable list** | `listSlicer` | `Values` (Column/Hierarchy), `Tooltips` (Measure/Aggregation) | — | Default for list-style slicers. `data.mode` not available. |
| **Button/tile** | `advancedSlicerVisual` | `Values` (1 Column only), `Label` (1 Measure, optional), `Tooltips` (Aggregations) | — | ≤10 values, tile layout. `data.mode` not available. |
**Temporal decision matrix:**
| Signal | Use |
|---|---|
| Executive page, annual grain, ≤10 years | Year dropdown or year tile |
| Discrete period comparison (e.g., 2020 vs 2023) | Year/month dropdown with multi-select |
| Month/quarter reporting with 12-36 periods | Period dropdown or relative date |
| Arbitrary day/month range exploration on Date/DateTime field | Full-date `Between` |
| Integer/text date key or date picker does not render | Year/Period dropdown |
**Constraints:**
- `data.mode` is only available on the classic `slicer` visual — not on
`listSlicer` or `advancedSlicerVisual`.
- `advancedSlicerVisual` allows only **one field** in `Values`.
- Hierarchy slicers: only `slicer` (mode: Basic/Dropdown) and `listSlicer`
support hierarchies. Add `expansionStates` for expanded nodes (see
expressions.md).
- A full-date column does not automatically mean `Between`. Choose by grain:
Year/Period dropdown or tile for annual/quarterly executive pages; `Between`
only when the field renders as Date/DateTime and users need arbitrary ranges.
### Setting slicer selections
The `general.filter` property in `objects` controls which values are
selected. This is only needed when pre-selecting specific values — omit
it entirely for the default "All" state.
```json
"general": [{
"properties": {
"orientation": { "expr": { "Literal": { "Value": "0D" } } },
"filter": {
"filter": {
"Version": 2,
"From": [
{ "Name": "d", "Entity": "dim_company", "Type": 0 }
],
"Where": [{
"Condition": { /* filter expression — see expressions.md */ },
"Annotations": {
"filterExpressionMetadata": {
"expressions": [{ /* Column Expression for the filtered field */ }],
"decomposedIdentities": {
"values": [[
{ "0": [{ "Literal": { "Value": "'A. Datum'" } }] },
{ "1": [{ "Literal": { "Value": "'N'" } }] },
{ "2": [{ "Literal": { "Value": "5L" } }] },
{ "3": [{ "Literal": { "Value": "'A. Datum'" } }] }
]],
"columns": [{ "value": { /* Column Expression — grouping key */ } }]
},
"valueMap": [{ "0": "A. Datum", "1": "N", "2": "5", "3": "A. Datum" }]
}
}
}]
}
}
}
}]
```
- `decomposedIdentities.values` — the actual selected values as literals
- `decomposedIdentities.columns` — the grouping key columns
- `valueMap` — maps indices in `decomposedIdentities` to queryRef values
- `expansionStates` — only needed for hierarchy slicers when nodes are expanded;
`identityKeys` defined on the first level only, `identityValues` inside
`root.children[]` only when a specific node has been toggled open
- **Literal format**: strings use single quotes inside double quotes
(`"Value": "'A. Datum'"`); numbers use type suffixes (`"Value": "5L"`).
---
## Slicer Sync Groups
Slicers can be synced across pages so that changing a selection on one page
applies to all other pages with slicers in the same sync group. Add `syncGroup`
inside the `visual` object (sibling of `visualType`, `query`, `objects`):
```json
{
"visual": {
"visualType": "slicer",
"syncGroup": {
"groupName": "DateSync",
"fieldChanges": true,
"filterChanges": true
},
"query": { /* ... */ },
"objects": { /* ... */ }
}
}
```
| Property | Type | Description |
|----------|------|-------------|
| `groupName` | string | Unique name for the sync group. Slicers with the same `groupName` across pages are synced. |
| `fieldChanges` | boolean | When `true`, field/projection changes propagate to all group members. |
| `filterChanges` | boolean | When `true`, filter/selection changes propagate to all group members. |
**Rules:**
- All slicers in the same sync group must have the same `visualType` and bound column.
Slicers of the same type that produce the same filter expressions can be in the
same group — e.g. two `slicer` visuals both bound to `Date.Date` with mode `'Between'`.
- Set the same `groupName` on each slicer you want synced (e.g. `"DateSync"`; any unique string works).
- Typically set both `fieldChanges: true` and `filterChanges: true`.
> **Note:** The published PBIR JSON schemas (`visualContainer/2.5.0–2.9.0`) do
> not list `syncGroup`. However, the internal schema (`visualConfiguration/9999.0.0`)
> does include it with full type definition. Desktop reads and writes it correctly.
---
## Theme Approach
These slicer defaults are applied report-wide via theme `visualStyles`
(plain JSON, not PBIR `expr` wrappers):
```json
"slicer": {
"*": {
"header": [{
"fontFamily": "Segoe UI Semibold",
"textSize": 10,
"fontColor": { "solid": { "color": "#252423" } },
"outlineStyle": 0
}],
"items": [{
"fontFamily": "Segoe UI Variable, Segoe UI, sans-serif",
"textSize": 9,
"fontColor": { "solid": { "color": "#252423" } },
"outlineStyle": 0,
"padding": 2
}]
}
}
```
The global `*.*` wildcard also provides: border (#E8E8E8, radius=8),
hidden visual header, and VCO padding (8px). Per the
[VCO override caveat](#per-visual-vco-override-caveat), this `*.*.padding`
cascade is **dropped** the moment a slicer sets any per-visual VCO, so every
slicer template must redeclare `padding` explicitly — `8/8/8/8` to match the
theme for normal slicers, or `0/0/0/0` for the [fill variant](#fill-variant)
(so the white fill reaches the container edges).
> `header.text` does **NOT** usefully cascade from theme — it would set
> the same name on every slicer. Always set per-visual.
---
## Discovering Properties
```bash
# List all formatting objects for a slicer type
powerbi-report-author formatting list-objects slicer
powerbi-report-author formatting list-objects advancedSlicerVisual
powerbi-report-author formatting list-objects listSlicer
# Inspect specific objects
powerbi-report-author formatting describe-object slicer header
powerbi-report-author formatting describe-object slicer items
powerbi-report-author formatting describe-object slicer data
powerbi-report-author formatting describe-object slicer selection
# Search across all objects
powerbi-report-author formatting search slicer "font|text|padding"
```
references/table.md
# Table & Matrix Visual Authoring Guide
Tables (`tableEx`) and matrices (`pivotTable`) in PBIR format.
## Default Rule — Grow to Fit
**Always** set both properties in `columnHeaders` unless the user explicitly opts out:
| Property | Value | Effect |
|----------|-------|--------|
| `autoSizeColumnWidth` | `true` | Enables automatic column sizing |
| `columnAdjustment` | `growToFit` | Columns expand to fill visual width (vs `fitToContent` which shrink-wraps) |
Both are already included in the templates below. To apply report-wide via
theme instead of per-visual, see [Theme Approach](#theme-approach).
---
## Table (`tableEx`)
Flat tabular data — columns bound to the `Values` role.
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 20, "y": 20, "z": 1000, "height": 400, "width": 700, "tabOrder": 1000 },
"visual": {
"visualType": "tableEx",
"query": {
"queryState": {
"Values": {
"projections": [
{
"field": { "Column": { "Expression": { "SourceRef": { "Entity": "<Table>" } }, "Property": "<Column1>" } },
"queryRef": "<Table>.<Column1>",
"nativeQueryRef": "<Column1>"
},
{
"field": { "Column": { "Expression": { "SourceRef": { "Entity": "<Table>" } }, "Property": "<Column2>" } },
"queryRef": "<Table>.<Column2>",
"nativeQueryRef": "<Column2>"
}
]
}
}
},
"objects": {
"columnHeaders": [{
"properties": {
"columnAdjustment": {
"expr": { "Literal": { "Value": "'growToFit'" } }
},
"autoSizeColumnWidth": {
"expr": { "Literal": { "Value": "true" } }
}
}
}]
}
}
}
```
---
## Matrix (`pivotTable`)
Row grouping, column grouping, and value aggregation. **When users ask for a
"matrix", use this template.**
| Role | Purpose |
|------|---------|
| `Rows` | Row grouping (hierarchy levels) |
| `Columns` | Column grouping |
| `Values` | Aggregated measures |
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 20, "y": 20, "z": 1000, "height": 400, "width": 700, "tabOrder": 1000 },
"visual": {
"visualType": "pivotTable",
"query": {
"queryState": {
"Rows": {
"projections": [{
"field": { "Column": { "Expression": { "SourceRef": { "Entity": "<Table>" } }, "Property": "<RowField>" } },
"queryRef": "<Table>.<RowField>",
"nativeQueryRef": "<RowField>"
}]
},
"Values": {
"projections": [{
"field": { "Measure": { "Expression": { "SourceRef": { "Entity": "<Table>" } }, "Property": "<Measure>" } },
"queryRef": "<Table>.<Measure>",
"nativeQueryRef": "<Measure>"
}]
}
}
},
"objects": {
"columnHeaders": [{
"properties": {
"columnAdjustment": {
"expr": { "Literal": { "Value": "'growToFit'" } }
},
"autoSizeColumnWidth": {
"expr": { "Literal": { "Value": "true" } }
}
}
}]
},
"expansionStates": [/* see expressions.md — "roles": ["Rows"]; only for hierarchy visuals */]
}
}
```
---
## Theme Approach
To apply grow-to-fit to **every** table and matrix in the report, add to the
report theme's `visualStyles` (uses plain JSON values, not PBIR `expr` wrappers):
```json
"visualStyles": {
"tableEx": {
"*": {
"columnHeaders": [{
"autoSizeColumnWidth": true,
"columnAdjustment": "growToFit"
}]
}
},
"pivotTable": {
"*": {
"columnHeaders": [{
"autoSizeColumnWidth": true,
"columnAdjustment": "growToFit"
}]
}
}
}
```
> ⚠️ Do NOT use `"*"` as the visual type key — `columnHeaders` is specific
> to table/matrix and could cause issues on other visual types.
>
> Visual-level `objects` override theme `visualStyles`. See
> `references/formatting-overview.md` for the full cascade order.
---
## Row Banding (Table & Matrix)
Tables (`tableEx`) and matrices (`pivotTable`) use a **Primary/Secondary color
pair** model for alternating row colors.
### Values Object Properties
```json
"values": [{
"properties": {
"backColorPrimary": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FFFFFF'" } } } } },
"backColorSecondary": { "solid": { "color": { "expr": { "Literal": { "Value": "'#F5F5F5'" } } } } },
"fontColorPrimary": { "solid": { "color": { "expr": { "Literal": { "Value": "'#333333'" } } } } },
"fontColorSecondary": { "solid": { "color": { "expr": { "Literal": { "Value": "'#333333'" } } } } }
}
}]
```
| Property | Applied To |
|----------|-----------|
| `backColorPrimary` | Odd rows (1st, 3rd, 5th…) |
| `backColorSecondary` | Even rows (2nd, 4th, 6th…) |
| `fontColorPrimary` | Text on odd rows |
| `fontColorSecondary` | Text on even rows |
When Primary ≠ Secondary → banding visible. When equal → no banding.
### Table/Matrix Formatting Regions
| Region | Object Name | Key Properties |
|--------|-------------|---------------|
| Data cells + row headers | `values` | `backColorPrimary/Secondary`, `fontColorPrimary/Secondary` |
| Column headers | `columnHeaders` | `fontColor`, `backColor`, `outline`, `outlineColor`, `autoSizeColumnWidth`, `columnAdjustment` |
| Row headers (matrix) | `rowHeaders` | `fontColor`, `backColor` |
| Total label (matrix) | inherits from `rowHeaders` | `fontColor` |
| Row totals (bottom row values) | `rowTotal` | `fontColor`, `backColor`, `applyToHeaders` (bool) |
| Column totals ("Total" column) | `columnTotal` | `fontColor`, `backColor`, `applyToHeaders` (bool) |
| Subtotals | `subTotals` | `fontColor`, `backColor` |
> ⚠️ **`pivotTable` has `rowTotal` and `columnTotal` — not just `total`.**
> The `total` object exists but its `fontColor` is a conditional formatting slot
> (like `values.fontColor`). Use `rowTotal` and `columnTotal` for static total
> colors on matrices.
Matrix has an additional `bandedRowHeaders` (bool) property to band row headers.
### Style Presets for Tables
Use the `stylePreset` VCO to apply built-in formatting bundles:
```json
"visualContainerObjects": {
"stylePreset": [{
"properties": {
"name": { "expr": { "Literal": { "Value": "'AlternatingRows'" } } }
}
}]
}
```
**9 built-in presets**: `None`, `Minimal`, `BoldHeader`, `AlternatingRows`,
`ContrastAlternatingRows`, `FlashyRows`, `BoldHeaderFlashyRows`, `Sparse`, `Condensed`.
The new table visual (`tableEx`) also has `Default` and `AlternatingRowsNew` presets.
> ⚠️ **Critical: Style presets OVERRIDE `objects`-level formatting.** When no
> `stylePreset` VCO is set, the default preset applies automatically. The default
> preset includes white row/header backgrounds that override any `backColorPrimary`,
> `backColorSecondary`, or `columnHeaders.backColor` you set in `objects`.
>
> **You MUST set `stylePreset` to `'None'` when using custom row/header colors:**
>
> ```json
> "visualContainerObjects": {
> "stylePreset": [{
> "properties": {
> "name": { "expr": { "Literal": { "Value": "'None'" } } }
> }
> }]
> }
> ```
>
> Without this, custom table colors silently fail — no error, no warning, just
> white backgrounds. This is the single most common dark-mode table formatting bug.
### `backColor` vs `backColorPrimary` — Different Purposes
Both appear as valid `fill` properties on `values` in the CLI, but they serve
different roles:
| Property | Purpose | Use Case |
|----------|---------|----------|
| `backColorPrimary` | **Static** odd-row background | Base row banding |
| `backColorSecondary` | **Static** even-row background | Base row banding |
| `backColor` | **Conditional formatting** slot | FillRule gradients, rules-based, field-value |
**For base row colors, always use `backColorPrimary` / `backColorSecondary`.**
`backColor` is intended for conditional formatting — it's the property PBI
Desktop writes when you enable data-driven cell coloring (gradients, rules,
field values). Use `backColorPrimary`/`Secondary` for static row backgrounds.
### `fontColor` vs `fontColorPrimary` — Different Purposes (pivotTable)
Both appear as valid `fill` properties on `values` in the CLI, but they serve
different roles:
| Property | Purpose | Use Case |
|----------|---------|----------|
| `fontColorPrimary` | **Static** odd-row text color | Base text coloring |
| `fontColorSecondary` | **Static** even-row text color | Base text coloring |
| `fontColor` | **Conditional formatting** slot | Rules-based, field-value text coloring |
**For static data cell text colors, always use `fontColorPrimary` /
`fontColorSecondary`.** Setting `values.fontColor` to a static color has NO
effect — text will inherit from the theme `foreground` instead. This is the most
common cause of invisible text on dark-themed matrices.
> ⚠️ **This inconsistency applies only to `values` and `total` on
> `pivotTable`.** On other objects (`columnHeaders`, `rowHeaders`, `rowTotal`,
> `columnTotal`, `subTotals`), `fontColor` works as a normal static color.
> `tableEx` does not expose `fontColor` on `values` at all — only
> `fontColorPrimary`/`Secondary`.
**Scope of `values.fontColorPrimary`/`Secondary`:** These properties control
text color for both data cells AND row header labels. The "Total" row label
inherits from `rowHeaders.fontColor` instead.
### Full Table Example with Custom Styling
```json
{
"visual": {
"visualType": "pivotTable",
"objects": {
"columnHeaders": [{
"properties": {
"fontColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FFFFFF'" } } } } },
"backColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#2B579A'" } } } } },
"columnAdjustment": { "expr": { "Literal": { "Value": "'growToFit'" } } },
"autoSizeColumnWidth": { "expr": { "Literal": { "Value": "true" } } }
}
}],
"rowHeaders": [{
"properties": {
"fontColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FFFFFF'" } } } } },
"backColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#1A1F27'" } } } } }
}
}],
"values": [{
"properties": {
"fontColorPrimary": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FFFFFF'" } } } } },
"fontColorSecondary": { "solid": { "color": { "expr": { "Literal": { "Value": "'#E0E0E0'" } } } } },
"backColorPrimary": { "solid": { "color": { "expr": { "Literal": { "Value": "'#14181E'" } } } } },
"backColorSecondary": { "solid": { "color": { "expr": { "Literal": { "Value": "'#1A1F27'" } } } } }
}
}],
"rowTotal": [{
"properties": {
"fontColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FFFFFF'" } } } } },
"backColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#1B3A5C'" } } } } }
}
}],
"columnTotal": [{
"properties": {
"fontColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#FFFFFF'" } } } } },
"backColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#1B3A5C'" } } } } }
}
}]
},
"visualContainerObjects": {
"stylePreset": [{
"properties": {
"name": { "expr": { "Literal": { "Value": "'None'" } } }
}
}]
}
}
}
```
> **Why `stylePreset: 'None'` is included:** without it, the default style
> preset overrides the custom `backColor` / `backColorPrimary` /
> `backColorSecondary` values above and the table renders with white
> backgrounds (no error, no warning). This applies to every `tableEx` and
> `pivotTable` with custom row, header, or total colors.
---
## References
- [formatting.md](formatting.md) — selectors, encoding, conditional formatting, VCO cascade
- [theming.md § Visual Styles](theming.md#6-visual-styles-visualstyles) — theme-level defaults
- `powerbi-report-author formatting list-objects tableEx` — discover all formatting objects
- `powerbi-report-author formatting describe-object tableEx columnHeaders` — inspect column header properties
references/textbox.md
# Textbox Visual Authoring Guide
Use `textbox` for visible static page titles, headers, annotations, and dynamic
text values. Textboxes require a **native `paragraphs` array** directly under
`objects.general[].properties.paragraphs`; do not wrap it as
`{ "paragraphs": [...] }` and do not stringify the JSON. The wrapped object form
can validate but render invisible text in Desktop.
> Examples use illustrative `<table>.<measure>` identifiers — substitute your own.
## Static textbox title
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 20, "y": 20, "z": 1000, "height": 60, "width": 520, "tabOrder": 0 },
"visual": {
"visualType": "textbox",
"objects": {
"general": [
{
"properties": {
"paragraphs": [
{
"textRuns": [
{
"value": "Sales Overview",
"textStyle": {
"fontFamily": "Segoe UI Semibold",
"fontSize": "24px",
"color": "#FFFFFF"
}
}
],
"horizontalTextAlignment": "left"
}
]
}
}
]
},
"visualContainerObjects": {
"background": [{ "properties": { "show": { "expr": { "Literal": { "Value": "false" } } } } }],
"border": [{ "properties": { "show": { "expr": { "Literal": { "Value": "false" } } } } }],
"padding": [{
"properties": {
"top": { "expr": { "Literal": { "Value": "0D" } } },
"bottom": { "expr": { "Literal": { "Value": "0D" } } },
"left": { "expr": { "Literal": { "Value": "0D" } } },
"right": { "expr": { "Literal": { "Value": "0D" } } }
}
}]
}
}
}
```
## Dynamic textbox value
```json
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.9.0/schema.json",
"name": "<20hexchars>",
"position": { "x": 20, "y": 20, "z": 0, "height": 50, "width": 300, "tabOrder": 0 },
"visual": {
"visualType": "textbox",
"objects": {
"general": [{
"properties": {
"paragraphs": [
{
"textRuns": [
{
"value": {
"propertyIdentifier": {
"objectName": "values",
"propertyName": "expr"
},
"selector": {
"id": "Value 3"
}
},
"textStyle": {
"fontFamily": "Arial Black",
"fontSize": "20pt",
"fontWeight": "bold",
"color": "#118DFF"
}
}
],
"horizontalTextAlignment": "left",
"listType": "bullet",
"indent": 1
}
]
}
}],
"values": [{
"properties": {
"expr": {
"expr": {
"Min": {
"Expression": {
"Column": {
"Expression": {
"Subquery": {
"Query": {
"Version": 2,
"From": [
{
/* Entity Source Expression */
}
],
"Select": [
{
"Aggregation": {
/* Aggregation Expression */
},
"Name": "Sum(Sales.SalesTax)"
}
],
"Where": [
{
"Condition": {
/* Not/Comparison Expressions */
}
}
]
}
}
},
"Property": "Sum(Sales.SalesTax)"
}
},
"IncludeAllTypes": 1
},
"Annotations": {
"NaturalLanguage": {
"version": 1,
"kind": "NaturalLanguage",
"annotation": {
"name": "Value 3",
"utterance": "Sum SalesTax (column 20)"
}
}
}
}
}
},
"selector": {
"id": "Value 3"
}
}]
}
}
}
```
## Rules
- `paragraphs` is a **native JSON array** directly under the `paragraphs`
property — NOT a stringified JSON literal and NOT an object wrapper
containing another `paragraphs` key. The wrapped form can validate but
render invisible text in Desktop.
- Each paragraph has `textRuns` plus optional `horizontalTextAlignment`,
`listType`, and `indent`.
- Font sizes can use CSS units such as `"24px"` for static title text, or `pt`
units such as `"40pt"` / `"12pt"`. Preserve existing units when editing.
- For static text, `textRuns[].value` is a plain string.
- For dynamic values, `textRuns[].value` is an object and the matching
`values` expression is required.
references/theming.md
# Theme Authoring — theme.json Reference
> Referenced from SKILL.md. Read [formatting-overview.md](formatting-overview.md) first for the cascade model.
> Read this when creating or editing a Power BI custom theme JSON file.
> Theme files live in `StaticResources/RegisteredResources/` within the report.
## Table of Contents
- [Registering a Custom Theme](#registering-a-custom-theme)
- [Theme Name GUID Convention (Cache-Busting)](#theme-name-guid-convention-cache-busting)
- [Schema Version](#schema-version)
- [Theme JSON Anatomy](#theme-json-anatomy)
- [1. Data Colors (`dataColors`)](#1-data-colors-datacolors)
- [2. Sentiment Colors](#2-sentiment-colors)
- [3. Gradient Defaults](#3-gradient-defaults)
- [4. Structural Colors](#4-structural-colors)
- [5. Text Classes (`textClasses`)](#5-text-classes-textclasses)
- [6. Visual Styles (`visualStyles`)](#6-visual-styles-visualstyles)
- [7. Style Presets](#7-style-presets)
- [8. ThemeDataColor Resolution](#8-themedatacolor-resolution)
- [9. Theme Defaults for Page Objects](#9-theme-defaults-for-page-objects)
- [10. Custom Icons](#10-custom-icons)
- [Re-theming & Dark Mode](#re-theming--dark-mode)
- [Theme Authoring Best Practices](#theme-authoring-best-practices)
- [Common Pitfalls](#common-pitfalls)
## Registering a Custom Theme
A custom theme requires two entries in `report.json`:
1. **`themeCollection.customTheme`** — references the theme by name and type:
```json
"customTheme": {
"name": "MyTheme-a1b2c3d4.json",
"reportVersionAtImport": { "visual": "...", "report": "...", "page": "..." },
"type": "RegisteredResources"
}
```
2. **`resourcePackages[]`** — registers the file so Desktop can locate it:
```json
{
"name": "RegisteredResources",
"type": "RegisteredResources",
"items": [{
"name": "MyTheme-a1b2c3d4.json",
"path": "MyTheme-a1b2c3d4.json",
"type": "CustomTheme"
}]
}
```
### Theme Name GUID Convention (Cache-Busting)
Power BI Desktop caches themes by name. To guarantee that every theme edit
is picked up on reload, **append a short GUID suffix** to the theme filename:
```
<CustomThemeName>-<guid>.json
```
- **`<CustomThemeName>`** — the user-provided name (e.g., `DarkCoralTheme`) or
the name you choose if the user doesn't specify one. Once established, the
`<CustomThemeName>` is **stable** — it does NOT change when the theme content
is updated (even if the palette changes entirely). Only change
`<CustomThemeName>` if the user explicitly asks to rename the theme.
- **`<guid>`** — a freshly generated short GUID (8–12 hex chars is sufficient,
e.g., `a1b2c3d4`). Use a full UUID without dashes truncated to 8+ chars, or
any unique random hex string.
**Examples:**
- User says "call it DarkCoralTheme" → `DarkCoralTheme-f7e2a91c.json`
- You choose the name → `ExecutiveDark-3bc04d8e.json`
- User later says "change to light green palette" → keep the same
`<CustomThemeName>`, only rotate the GUID: `DarkCoralTheme-e4f5a6b7.json`
(content changes, `<CustomThemeName>` stays)
- User says "change the theme to dark orange" → this is a palette/style change,
NOT a rename. Keep the same `<CustomThemeName>`, only rotate the GUID and
update content. Phrases like "change the theme to X" or "apply a X theme"
describe the desired appearance — they are NOT rename requests.
- User says "rename the theme to LightGreen" → now change
`<CustomThemeName>`: `LightGreen-e4f5a6b7.json`
**On every custom theme file update** — any change to
`StaticResources/RegisteredResources/<CustomThemeName>-<guid>.json`, regardless
of what changed (colors, fonts, `visualStyles`, metadata, schema, whitespace, or
any other content):
1. Generate a **new GUID**. **Keep the same `<CustomThemeName>`** — do NOT
change it unless the user explicitly requests a rename.
2. **Edit the theme file content** in place — update colors, fonts,
`visualStyles`, and update the `"name"` field inside the JSON to match the
new filename (e.g., `"DarkCoralTheme-e4f5a6b7.json"`).
3. **Rename the file** from `<CustomThemeName>-<oldGUID>.json` to
`<CustomThemeName>-<newGUID>.json` using a filesystem rename (`Rename-Item`
/ `mv`) — do NOT create a new file and delete the old one; rename is
equivalent and avoids a redundant write + delete.
4. **Update all references** in `report.json`:
- `themeCollection.customTheme.name`
- `resourcePackages[].items[].name`
- `resourcePackages[].items[].path`
5. Reload Desktop — the new filename guarantees cache invalidation.
> **Why?** Desktop's theme engine caches by filename. Editing a file in place
> with the same name can leave stale theme state even after reload. Changing
> the filename on every update forces a fresh load every time.
> **Important:** Inside `report.json`, both `customTheme.name` and the
> matching `resourcePackages[].items[].name` MUST include the `.json`
> extension (e.g., `"MyTheme-a1b2c3d4.json"`) and MUST equal the item's `path`.
> Using the bare theme name (e.g., `"MyTheme-a1b2c3d4"`) causes the published
> report on the Power BI service to incorrectly apply the theme because the resource
> mapping never matches the file under `StaticResources/RegisteredResources/`.
>
> The `path` must be the **filename only** (e.g., `"MyTheme-a1b2c3d4.json"`)
> with the `.json` extension. Do NOT include directory prefixes like
> `"RegisteredResources/MyTheme-a1b2c3d4.json"` — this causes Desktop to silently
> ignore the theme.
>
> The `name` field **inside the theme JSON file itself** MUST match
> `customTheme.name` / `resourcePackages[].items[].name` exactly, including the
> `.json` extension (e.g., `"MyTheme-a1b2c3d4.json"`). Using a bare name such as
> `"MyTheme-a1b2c3d4"` causes validation failure and can break Desktop theme
> loading.
>
> Run `powerbi-report-author validate <path-to-.Report-dir>` after
> registering or editing a theme to catch registration and theme JSON issues
> before reloading Desktop.
## Schema Version
Set `"$schema"` to a published `reportThemeSchema-<major>.<minor>.json` from the
[powerbi-desktop-samples repo](https://github.com/microsoft/powerbi-desktop-samples/tree/main/Report%20Theme%20JSON%20Schema/).
How to choose the version:
1. **Check Microsoft's base themes first.** Scan
`<Report>/StaticResources/SharedResources/BaseThemes/*.json` for any file
that carries a `"$schema"`. If found, **reuse the same version** — those
files are shipped by Power BI Desktop and represent the version it
currently understands.
2. **No `"$schema"` in any base theme** (the common case today, since the
shipped base themes don't pin one) — use the **latest published**
`reportThemeSchema-<version>.json` from the samples repo above.
Reference the schema as a full published HTTPS URL or as a local
`reportThemeSchema-<version>.json` placed beside the theme JSON.
## Theme JSON Anatomy
A custom theme has 7 sections. All use **plain JSON encoding** (no PBIR expression wrappers):
```json
{
"name": "MyTheme-a1b2c3d4.json",
"dataColors": ["#118DFF", "#12239E", "#E66C37", ...],
"good": "#1AAB40",
"neutral": "#D9B300",
"bad": "#D64554",
"maximum": "#118DFF",
"center": "#D9B300",
"minimum": "#DEEFFF",
"null": "#FF7F48",
"foreground": "#252423",
"background": "#FFFFFF",
"tableAccent": "#118DFF",
"textClasses": { ... },
"visualStyles": { ... },
"icons": { ... }
}
```
## 1. Data Colors (`dataColors`)
An array of hex strings defining the categorical color palette:
```json
"dataColors": [
"#118DFF", "#12239E", "#E66C37", "#6B007B", "#E044A7",
"#744EC2", "#D9B300", "#D64550", "#197278", "#1AAB40"
]
```
- Indexed by `ThemeDataColor.ColorId` (0-based) in PBIR formatting
- The default base theme has **41 colors**
- Custom `dataColors` **fully replaces** the base theme array (no merge)
- When exhausted, PBI generates unique colors via saturation/hue shifts
## 2. Sentiment Colors
Used by waterfall charts and KPI visuals:
| Key | Purpose | Default |
|-----|---------|---------|
| `good` | Positive values | `#1AAB40` (green) |
| `neutral` | Neutral values | `#D9B300` (yellow) |
| `bad` | Negative values | `#D64554` (red) |
## 3. Gradient Defaults
Default colors for the conditional formatting color-scale dialog:
| Key | Purpose | Default |
|-----|---------|---------|
| `maximum` | Highest value | `#118DFF` |
| `center` | Middle value (diverging) | `#D9B300` |
| `minimum` | Lowest value | `#DEEFFF` |
| `null` | Null values | `#FF7F48` |
These keys only seed/default the color-scale picker. Custom theme JSON does not
author conditional formatting rules; apply rules on the target visual.
## 4. Structural Colors
Foundational UI colors. The 7 classes (with legacy aliases):
| Preferred Name | Alias | What It Formats |
|---------------|-------|-----------------|
| `firstLevelElements` | `foreground` | Labels, trend lines, textbox defaults, card data labels, slicer item color |
| `secondLevelElements` | `foregroundNeutralSecondary` | Legend labels, axis labels, table headers, gauge target |
| `thirdLevelElements` | `backgroundLight` | Gridlines, table grid, slicer header background, shape fill |
| `fourthLevelElements` | `foregroundNeutralTertiary` | Legend dimmed, card category labels, funnel conversion rate |
| `background` | — | Label background, slicer dropdown, donut stroke, button fill, tooltip bg |
| `secondaryBackground` | `backgroundNeutral` | Table grid outline, shape map default, ribbon fill, disabled button |
| `tableAccent` | — | Table and matrix grid outline |
**⚠️ Dark themes**: ALL structural colors must be adjusted together.
Setting `background` to dark without adjusting `firstLevelElements` makes text invisible.
Additionally, the **filter pane** does NOT inherit from structural colors —
you must always explicitly set `outspacePane` and `filterCard` in
`visualStyles["*"]["*"]` when changing themes. See [re-theming.md § Dark Mode Authoring Checklist](re-theming.md#dark-mode-authoring-checklist)
for the complete checklist (filter pane, stylePreset override, fillCustom+id selector, objects vs VCO).
## 5. Text Classes (`textClasses`)
**4 primary classes** (editable in Customize Theme dialog):
| Class | Key | Default Font | Default Size | Used By |
|-------|-----|-------------|-------------|---------|
| General | `label` | Segoe UI | 10pt | Table/matrix values, slicer items |
| Title | `title` | DIN | 12pt | Axis titles, multi-row card title |
| Cards & KPIs | `callout` | DIN | 45pt | Card data labels, KPI indicators |
| Tab headers | `header` | Segoe UI Semibold | 12pt | Key influencers headers |
**8 documented derived classes** (auto-derived from primaries):
| Key | Derives From | Modification |
|-----|-------------|-------------|
| `largeTitle` | `title` | 14pt — visual title |
| `semiboldLabel` | `label` | Segoe UI Semibold — key influencers profile |
| `largeLabel` | `label` | 12pt — multi-row card data |
| `smallLabel` | `label` | 9pt — reference lines, slicer date range |
| `lightLabel` | `label` | Color from `secondLevelElements` — legend, button text, axis labels |
| `boldLabel` | `label` | Segoe UI Bold — matrix subtotals, table totals |
| `largeLightLabel` | `label` | `secondLevelElements` color, 12pt — card category, gauge labels |
| `smallLightLabel` | `label` | `secondLevelElements` color, 9pt — data labels, value axis labels |
Other `textClasses` names may appear in exported PBIR or undocumented schema
variants, but they are not documented in Microsoft's theme guidance. Do not
author them unless you have validated the current `reportThemeSchema.json` for
the target Desktop version.
**Format:**
```json
"textClasses": {
"callout": { "fontSize": 45, "fontFace": "DIN", "color": "#252423" },
"title": { "fontSize": 12, "fontFace": "DIN", "color": "#252423" },
"header": { "fontSize": 12, "fontFace": "Segoe UI Semibold", "color": "#252423" },
"label": { "fontSize": 10, "fontFace": "Segoe UI", "color": "#252423" }
}
```
Properties: `fontFace` (string), `fontSize` (number, pt), `color` (hex string).
Optional: `"bold": true`.
## 6. Visual Styles (`visualStyles`)
Sets default formatting for any visual type — the central mechanism for
theme-driven appearance.
### Three-Level Hierarchy
```
visualStyles[Level 1: visual type][Level 2: style preset][Level 3: object name] → properties
```
| Level | Examples | Wildcard |
|-------|----------|----------|
| 1 — Visual type | `"barChart"`, `"tableEx"`, `"pivotTable"` | `"*"` = all types |
| 2 — Style preset | `"Bold"`, `"Minimal"`, `"Corporate"` | `"*"` = default (no preset) |
| 3 — Object name | `"legend"`, `"dataPoint"`, `"border"` | `"*"` = all objects |
**Resolution order** (first match wins):
1. `customTheme.visualStyles[exactType][activePreset][object]`
2. `customTheme.visualStyles[exactType]["*"][object]`
3. `customTheme.visualStyles["*"][activePreset][object]`
4. `customTheme.visualStyles["*"]["*"][object]`
5. `baseTheme.visualStyles[exactType]["*"][object]`
6. `baseTheme.visualStyles["*"]["*"][object]`
7. System defaults
Type-specific entries override wildcard entries at each level.
### Example — Universal + Type-Specific
```json
"visualStyles": {
"*": {
"*": {
"title": [{
"show": true,
"fontFamily": "'Segoe UI Semibold'",
"fontSize": 12,
"fontColor": { "solid": { "color": "#333333" } }
}],
"background": [{
"show": true,
"color": { "solid": { "color": "#FFFFFF" } },
"transparency": 0
}],
"border": [{
"show": true,
"color": { "solid": { "color": "#E0E0E0" } },
"radius": 4,
"width": 1
}]
}
},
"barChart": {
"*": {
"border": [{ "radius": 8 }],
"legend": [{ "position": "Top" }]
}
}
}
```
### Encoding in visualStyles
Values use **theme encoding** (plain JSON), not PBIR expression wrappers:
```json
"show": true,
"fontSize": 12,
"position": "Top",
"fontColor": "#252423",
"gridlineColor": { "solid": { "color": "#E0E0E0" } }
```
**Color properties**: Some accept plain hex strings, others require
`{ "solid": { "color": "#hex" } }`. When in doubt, use the structured format.
### Object Names for visualStyles
Common objects available under `"*"` (all types):
`title`, `subTitle`, `background`, `border`, `dropShadow`, `visualHeader`,
`visualTooltip`, `lockAspect`, `general`, `padding`, `divider`, `spacing`
Per-visual-type objects (examples):
| Visual Type | Objects |
|-------------|---------|
| Column/Bar | `categoryAxis`, `valueAxis`, `legend`, `dataPoint`, `labels` |
| Line/Area | `categoryAxis`, `valueAxis`, `legend`, `dataPoint`, `labels`, `lineStyles`, `markers` |
| Table (`tableEx`) | `grid`, `columnHeaders`, `values`, `total`, `columnFormatting`, `stylePreset` |
| Matrix (`pivotTable`) | `grid`, `columnHeaders`, `rowHeaders`, `values`, `subTotals`, `grandTotal` |
| Slicer | `data`, `selection`, `header`, `items`, `slider` |
| Card | `labels`, `categoryLabels`, `wordWrap` |
Use `powerbi-report-author formatting list-objects <type>` for the full list per visual.
### The `$id` Property — Instance Discrimination
Some objects have multiple instances discriminated by `$id`:
```json
"filterCard": [
{ "$id": "Applied", "backgroundColor": { "solid": { "color": "#E8F0FE" } } },
{ "$id": "Available", "backgroundColor": { "solid": { "color": "#FFFFFF" } } }
]
```
This is the theme-level equivalent of PBIR's `"selector": { "id": "..." }`.
Both use PascalCase values (`"Applied"`, `"Available"`).
## 7. Style Presets
Style presets are named formatting bundles selectable per-visual.
### Theme-Side: Registration
Register presets in `visualStyles` at level 2:
```json
"visualStyles": {
"columnChart": {
"*": {
"stylePreset": [{ "name": "Corporate Blue" }],
"legend": [{ "position": "BottomCenter" }]
},
"Corporate Blue": {
"legend": [{ "position": "Right", "fontColor": { "solid": { "color": "#003366" } } }]
},
"Minimal": {
"legend": [{ "show": false }],
"border": [{ "show": false }]
}
}
}
```
### Visual-Side: Selection
Each visual selects its preset via the `stylePreset` VCO (PBIR encoding):
```json
"visualContainerObjects": {
"stylePreset": [{
"properties": {
"name": { "expr": { "Literal": { "Value": "'Corporate Blue'" } } }
}
}]
}
```
If no `stylePreset` VCO is present, the `"*"` (default) preset applies.
### Limitations
- Per-visual-type only — presets can't span visual types
- No UI dropdown for custom presets yet (built-in presets only)
- Preset names are arbitrary strings (title-case convention: `"Bold"`, `"Minimal"`)
## 8. ThemeDataColor Resolution
PBIR formatting can reference theme palette colors dynamically:
```json
{ "ThemeDataColor": { "ColorId": 0, "Percent": 0.4 } }
```
### Resolution Algorithm
1. Look up `dataColors[ColorId]` from the effective theme
2. Apply `shadeColor(baseColor, Percent)` in RGB space:
```
if Percent > 0 (tint toward white):
channel = channel + (255 - channel) × Percent
if Percent < 0 (shade toward black):
channel = channel × (1 + Percent)
```
Use `powerbi-report-author theme shade-color <hex> <percent>` to compute this:
```bash
powerbi-report-author theme shade-color "#118DFF" 0.4 # → #70BBFF (lighter)
powerbi-report-author theme shade-color "#118DFF" -0.4 # → #0A5599 (darker)
```
### Common Percent Values
| Percent | Use |
|---------|-----|
| `0` | Primary data colors (series, fills) |
| `0.4` | Container backgrounds from palette |
| `0.6` | Subtle backgrounds, secondary text |
| `0.8` | Very light pastel backgrounds |
| `-0.2` | Slightly darker text, emphasis borders |
| `-0.4` | Darker headers, stronger contrast |
### When to Use ThemeDataColor vs Literal
| Scenario | Use | Why |
|----------|-----|-----|
| Data series colors | `ThemeDataColor(N, 0)` | Auto-updates with theme |
| Backgrounds from palette | `ThemeDataColor(N, 0.4)` | Consistent, theme-adaptive |
| Brand colors that must NOT change | Literal `"'#FF6B35'"` | Theme-independent |
| Semantic colors (red=bad) | Literal hex | Meaning is absolute |
| Explicit per-measure `dataPoint.fill` with a `metadata` selector | Literal hex | `ThemeDataColor` in this position silently resolves to white or black — see [SKILL.md Anti-Patterns](../SKILL.md#anti-patterns-and-pitfalls) |
## 9. Theme Defaults for Page Objects
Themes can set defaults for page-level formatting objects via `visualStyles`.
The property names and structure are the same as described in
[page-formatting.md](page-formatting.md); the encoding differs (theme encoding,
not PBIR):
```json
"visualStyles": {
"*": {
"*": {
"outspacePane": [{
"backgroundColor": { "solid": { "color": "#FFFFFF" } },
"foregroundColor": { "solid": { "color": "#252423" } },
"titleSize": 12,
"border": true,
"borderColor": { "solid": { "color": "#E0E0E0" } },
"checkboxAndApplyColor": { "solid": { "color": "#118DFF" } },
"inputBoxColor": { "solid": { "color": "#FFFFFF" } }
}],
"filterCard": [
{ "$id": "Applied", "backgroundColor": { "solid": { "color": "#E8F0FE" } } },
{ "$id": "Available", "backgroundColor": { "solid": { "color": "#FFFFFF" } } }
],
"background": [{
"color": { "solid": { "color": "#FFFFFF" } },
"transparency": 0
}],
"outspace": [{
"color": { "solid": { "color": "#F3F2F1" } },
"transparency": 0
}]
}
}
}
```
See [page-formatting.md](page-formatting.md) for the full property lists of
these objects and PBIR examples.
## 10. Custom Icons
Define custom icons for icon-set conditional formatting:
```json
"icons": {
"customFire": { "url": "https://example.com/fire.png", "description": "Fire indicator" },
"customStar": { "url": "https://example.com/star.svg", "description": "Star rating" }
}
```
These become available in the conditional formatting dialog icon set picker.
## Re-theming & Dark Mode
See [re-theming.md](re-theming.md) for the re-theming workflow (color mapping,
bulk hex sweep, polarity gate), dark mode authoring checklist, and preventive
authoring patterns.
## Theme Authoring Best Practices
1. **Always set ALL structural colors together** for dark themes — partial changes
create invisible text or clashing chrome
2. **Use `visualStyles["*"]["*"]`** for universal defaults, type-specific keys for overrides
3. **Test with multiple visual types** — wildcard `"*"` applies to all, including
visuals you may not expect (e.g., `"values"` banding applies to any visual with
a `values` object, not just tables)
4. **Use `{ "solid": { "color": "#hex" } }`** for color properties in visualStyles
when unsure — it always works
5. **Validate with schema** — see [Schema Version](#schema-version) above for
how to pick and reference the right `reportThemeSchema-<version>.json`.
`powerbi-report-author validate` does not infer a schema version or expand bare filenames
to GitHub URLs; if you want schema validation, set `"$schema"` to a full
published HTTPS URL or a local schema file beside the theme JSON.
6. **Row banding via theme** — set `tableEx` and `pivotTable` specific values, not `"*"`,
to avoid unexpected banding on non-table visuals (see
[table.md § Row Banding](table.md#row-banding-table--matrix))
## Common Pitfalls
| Pitfall | Fix |
|---------|-----|
| Using PBIR expression wrappers in theme JSON | Theme uses plain JSON: `true`, `12`, `"#hex"` |
| Setting only `background` for dark theme | Also set `foreground`/`firstLevelElements` for text contrast |
| Wildcard `"*"."*"."values"` for row banding | Use `"tableEx"` / `"pivotTable"` to avoid affecting non-table visuals |
| Lowercase `"applied"` for filter card $id | Must be PascalCase: `"Applied"`, `"Available"` |
| Assuming dataColors merge with base theme | Custom `dataColors` fully replaces the base array |
| Forgetting `reportVersionAtImport` | Preserve as-is — PBI Desktop manages this field |
| Putting conditional formatting rules in theme JSON | Apply conditional formatting separately on individual visuals |
| Dark theme but table/matrix rows still white | See [§ Style Presets](#7-style-presets) and [re-theming.md § Dark Mode Checklist](re-theming.md#dark-mode-authoring-checklist) |
| Setting `visualStyles["slicer"]` for dark slicer text | Modern slicers use `filterSlicer` / `advancedSlicerVisual` — the legacy `"slicer"` key has no effect. Add type-specific entries for both modern types |
| Shape text invisible after dark theme switch | Shapes relying on inherited foreground have no explicit `fontColor` — the bulk hex sweep can't add a property that didn't exist. Add explicit `text.fontColor` to every shape with `text.show: true` |
references/version-control.md
# Version Control for PBIR Editing
> Referenced from SKILL.md. Follow this workflow **before** modifying any report files.
## Pre-Flight: Check for Git Repo
Before making any changes to a PBIP folder, check if it has a git repo:
```bash
git -C "<pbip-folder>" rev-parse --is-inside-work-tree 2>&1
```
| Result | Meaning | Action |
|--------|---------|--------|
| `true` | Git repo exists | Proceed with branching workflow below |
| `fatal: not a git repository` | No repo | Ask user: "This report folder has no git repo. Want me to initialize one so I can safely track and revert changes?" |
If user declines initialization, warn that changes cannot be automatically reverted
and proceed with extra caution (validate before every write).
### Initialize a New Repo (if user approves)
```bash
cd "<pbip-folder>"
git init
git add -A
git commit -m "Initial commit: baseline report state"
```
This creates a clean baseline that all future edits branch from.
---
## Branching Workflow
**Always create a branch before editing report files.** This protects the
user's working state and enables clean revert.
### 1. Create a working branch
```bash
git -C "<pbip-folder>" checkout -b copilot/report-edits
```
Use descriptive branch names when the intent is specific:
- `copilot/add-sales-page`
- `copilot/fix-layout`
- `copilot/apply-dark-theme`
If a `copilot/*` branch already exists from a previous session, check with the
user before reusing or creating a new one.
### 2. Make changes
Edit report files (pages, visuals, filters, formatting) as requested.
### 3. Validate & verify
Run validation after every logical batch of changes:
```bash
powerbi-report-author validate "<path-to-.Report-dir>"
```
If the change affects rendered output, follow `references/powerbi-desktop.md` for Desktop
reload and screenshot verification. Do not proceed until structural validation
passes and any required visual verification is complete.
### 4. Ask user before committing
**Never auto-commit.** After validation and Desktop verification pass, ask the
user if they want to commit the changes. Show them what was changed and let
them decide.
If the user approves:
```bash
cd "<pbip-folder>"
git add -A
git commit -m "<descriptive message of what changed>"
```
Use clear commit messages that describe the user's intent:
- `"Add Executive Summary page with 4 KPI cards and trend chart"`
- `"Apply dark theme and fix font colors for contrast"`
- `"Add year slicer and region filter to Sales page"`
### 5. Continue or finish
- **More changes requested**: Continue editing on the same branch, validate and
verify after each batch, ask user before each commit.
- **User satisfied**: Inform user the changes are on branch `copilot/report-edits`
and they can merge to their main branch when ready.
---
## Reverting Changes
### Revert all uncommitted changes
If edits fail validation or user wants to undo current work-in-progress:
```bash
git -C "<pbip-folder>" checkout -- .
git -C "<pbip-folder>" clean -fd
```
This restores all files to the last committed state and removes any new
untracked files/directories.
### Revert the last commit
If the last committed batch of changes needs to be undone:
```bash
git -C "<pbip-folder>" reset --hard HEAD~1
```
### Revert to the original state (before any copilot edits)
To discard all changes made on the working branch and return to the starting point:
```bash
git -C "<pbip-folder>" checkout main
git -C "<pbip-folder>" branch -D copilot/report-edits
```
Replace `main` with whatever branch was active before the edits began. If unsure,
check with:
```bash
git -C "<pbip-folder>" log --oneline --all --graph | head -20
```
### Revert a specific file
If only one file needs to be restored:
```bash
git -C "<pbip-folder>" checkout HEAD -- "path/to/file.json"
```
---
## Decision Tree
```text
User requests a report change
│
▼
Is the PBIP folder a git repo?
│
├── NO → Ask user to initialize → git init + initial commit
│
▼ YES
│
Are there uncommitted changes?
│
├── YES → Warn user: "There are uncommitted changes. Shall I commit
│ them first as a checkpoint, or stash them?"
│ • Commit: git add -A && git commit -m "checkpoint before copilot edits"
│ • Stash: git stash push -m "stashed before copilot edits"
│
▼ NO (clean working tree)
│
Create working branch → copilot/<intent>
│
▼
Make changes → Validate → Commit
│
├── Validation fails → Revert uncommitted (git checkout -- .)
│ → Fix and retry
│
├── User requests revert → git reset --hard HEAD~N or checkout main
│
▼ Success
│
Inform user: changes committed on branch, ready to merge or use
```
---
## Important Notes
- **Never force-push or rewrite history** on branches the user created.
Only manage `copilot/*` branches.
- **Check for uncommitted changes** before creating a branch. Dirty working
trees can cause checkout failures.
- **Commit frequently** — after each validated change set, not at the end
of a long editing session. This gives fine-grained revert points.
- **The `.pbip` file is outside the `.Report/` directory** — make sure the
git repo covers the full PBIP folder (the directory containing the `.pbip` file),
not just the `.Report/` subdirectory.
- **`localSettings.json`** should be in `.gitignore` — it contains user-local
state that shouldn't be versioned.
SKILL.md
---
name: powerbi-report-authoring
description: >-
Create and modify Power BI report files in PBIR/PBIP format using the
`powerbi-report-author` and `powerbi-desktop` CLIs. Use when the user wants
to: (1) implement an approved report spec or design brief, (2) add or edit
pages, visuals, filters, slicers, bookmarks, themes, or formatting, (3)
validate PBIR and verify rendering in Power BI Desktop. For open-ended visual
design, use `powerbi-report-design` first. For end-to-end requirements and
approval workflow, use `powerbi-report-planning` first. Triggers: "edit PBIR",
"create Power BI report page", "add visual to PBIP", "format report visual",
"validate Power BI report", "reload Desktop screenshot", "implement an approved PBIP report spec", "edit PBIR pages/visuals".
metadata:
version: 0.1.0
---
> **CRITICAL NOTES**
> 1. To find the workspace details (including its ID) from workspace name: list all workspaces and, then, use JMESPath filtering
> 2. To find the item details (including its ID) from workspace ID, item type, and item name: list all items of that type in that workspace and, then, use JMESPath filtering
# Power BI Report Authoring Skill (PBIR/PBIP Format)
This skill enables reading, editing, and creation of Power BI report
definition files in the **PBIR (Power BI Report)** format used by **PBIP
(Power BI Project)** files.
## Must/Prefer/Avoid
### MUST
- Use this skill only for concrete PBIR/PBIP report-file mechanics such as pages, visuals, filters, slicers, navigation, bookmarks, themes, formatting, validation, Desktop reloads, and screenshots.
- Validate PBIR with `powerbi-report-author validate` after each logical batch.
- Use `powerbi-desktop` reload/screenshot workflows for rendered-output changes.
- Use CLI capability lookup before writing visual roles, formatting objects, enum values, selectors, or expression encodings.
### PREFER
- Start from an approved `Design Brief:` or `_brief/report-spec.md` for greenfield report builds.
- Route visual-design uncertainty to `powerbi-report-design` before writing files.
- For semantic model metadata or model-side changes, use a semantic-model authoring skill, Power BI Modeling MCP, or local TMDL files when available.
### AVOID
- Do not guess PBIR JSON from memory when CLI metadata or reference files are available.
- Do not use only this skill for open-ended design, report planning, or Fabric report item CRUD; pair it with `powerbi-report-design`, `powerbi-report-planning`, or `powerbi-report-management`.
## Quick Start Workflow
0. **Plan/design routing** → for greenfield builds, read `powerbi-report-planning`
first; for theming, visual style, layout, redesigns, or critiques, read
`powerbi-report-design`. Return here for PBIR mechanics. Before authoring,
use the `Design Brief:` yaml block from `_brief/report-spec.md` (or an
approved inline `Design Brief:` block in the conversation) as implementation
context.
1. **Set up/update CLIs** → before first use, confirm `powerbi-report-author`
and the global `powerbi-desktop` command are available; see
[CLI Setup](#cli-setup).
2. **Understand the model** → use the Semantic Model MCP Server/skill if available,
or read TMDL files directly for table/column/measure names
3. **Desktop context** → for live Desktop status, opening PBIP/PBIX files,
reloads, screenshots, or visual verification, use the
`powerbi-desktop` CLI from `@microsoft/powerbi-desktop-bridge-cli`; see
[Edit → Validate → Reload → Screenshot Loop](#edit--validate--reload--screenshot-loop).
4. **Route by intent** → use [Topic Files and Examples](#topic-files-and-examples) to pick the relevant
guide.
5. **Use CLI metadata** → use [Authoring Metadata & Validation CLI](#authoring-metadata--validation-cli)
for exact visual roles, formatting objects, property names, enum values, and
selector requirements; do not infer these from memory.
6. **Check common pitfalls** → read [Anti-Patterns and Pitfalls](#anti-patterns-and-pitfalls)
before editing or validating when the change touches visuals, bindings,
filters, formatting, layout, or Desktop rendering.
7. **Validate** → run `powerbi-report-author validate <path-to-.Report-dir>`
after every logical batch of PBIR changes; see [Validation](#validation).
8. **Verify rendering** → for any rendered-output change, use `powerbi-desktop`
reload + screenshots; see [Edit → Validate → Reload → Screenshot Loop](#edit--validate--reload--screenshot-loop)
and [Screenshot Review](#screenshot-review). Do not proceed until both
validation and visual review pass. For dashboard/report builds, page
scaffolding is not completion — each requested page needs data-bound visuals.
9. **Report back** → give the user a concise summary of what was done and any
issues encountered (major and minor).
## Topic Files and Examples
Use the user's intent to choose the relevant topic file(s) before editing:
| File | When to read |
|------|-------------|
| [`authoring.md`](references/authoring.md) | Adding/modifying pages, visuals, drillthrough, interactions — includes complete JSON examples |
| [`powerbi-desktop.md`](references/powerbi-desktop.md) | Live Desktop verification — `powerbi-desktop` commands, PID selection, reload, screenshots, errors, troubleshooting |
| [`screenshot-review.md`](references/screenshot-review.md) | Screenshot review checklist and rendered-output troubleshooting after Desktop screenshot capture |
| [`formatting-overview.md`](references/formatting-overview.md) | **Read first for appearance changes** — cascade model, encoding rules, selectors, routing to other formatting files |
| [`formatting.md`](references/formatting.md) | Editing `visual.json` appearance — selectors, VCOs, encoding mechanics, background-image routing, cascade |
| [`color-strategy.md`](references/color-strategy.md) | Chart data point colors — theme `dataColors` vs `dataPoint.defaultColor` vs `dataPoint.fill` with selectors, cross-visual measure-color consistency |
| [`conditional-formatting.md`](references/conditional-formatting.md) | Data-driven formatting — color gradients (FillRule), rules-based, icon sets, data bars, web URL, field value |
| [`page-formatting.md`](references/page-formatting.md) | Editing `page.json` appearance — canvas background, wallpaper, page background images |
| [`filter-pane.md`](references/filter-pane.md) | Filter pane (`outspacePane`) and filter card (`filterCard`) chrome — Applied/Available state styling, pane width, search/checkbox colors |
| [`theming.md`](references/theming.md) | Creating or editing `theme.json` — dataColors, textClasses, visualStyles, style presets, ThemeDataColor reference |
| [`re-theming.md`](references/re-theming.md) | **Switching themes on a report with existing visuals** — re-theming workflow (color mapping + bulk sweep), dark mode checklist, dark↔light polarity changes. Pair with `theming.md` when changing colors on a report with per-visual overrides. |
| [`expressions.md`](references/expressions.md) | Building field references (Column, Measure, Aggregation, Hierarchy) and sort definitions |
| [`filters.md`](references/filters.md) | Adding/modifying filters — includes complete JSON examples |
| [`slicers.md`](references/slicers.md) | **Read first** when adding/modifying slicers or slicer selections — agent workflow, JSON templates, selection config |
| [`cartesian.md`](references/cartesian.md) | Adding bar, column, line charts — families, roles, query patterns (multi-measure, drill hierarchy, date hierarchy), formatting |
| [`map.md`](references/map.md) | Adding map visuals — template, roles, geocoding workflow, handling render failures |
| [`card.md`](references/card.md) | Adding or formatting KPI/card visuals — `cardVisual`, id selectors, callout/value sizing, accent bars |
| [`table.md`](references/table.md) | Adding or formatting tables/matrices — `tableEx`, `pivotTable`, grow-to-fit columns, row banding |
| [`image.md`](references/image.md) | Adding image visuals — local resources, URLs, data-bound images, ImageUrl validation/refusal workflow; also plot area background images for chart visuals |
| [`shape.md`](references/shape.md) | Adding shape visuals — containers, dividers, backgrounds, reference-image matching |
| [`textbox.md`](references/textbox.md) | Adding static or dynamic textbox visuals — paragraphs, text runs, and bound value expressions |
| [`version-control.md`](references/version-control.md) | Git branching, committing, reverting — read when the task involves version control or safe rollback planning |
### Greenfield / Design Handoff
> This skill owns PBIR file mechanics once the work is concrete: page/visual
> JSON, bindings, filters, slicers, themes, formatting, navigation, bookmarks,
> validation, Desktop reloads, and screenshots.
>
> Use `powerbi-report-planning` before authoring for new report/dashboard
> requests, requirements gathering, dependency checks, approval, or end-to-end
> build sequencing. Use `powerbi-report-design` for open-ended visual design,
> redesign/restyle, brand/theme direction, chart selection, or layout critique.
> Return here once there is an approved spec/design brief or a concrete PBIR
> edit to implement — see Quick Start step 0 for how to consume the brief.
### Large Build Execution
For full report/PBIP builds, do **not** delegate complete PBIP generation to a
subagent — the owning agent must keep the design brief, model inventory,
cross-page consistency, validation loop, and Desktop verification coordinated.
When context or repetition is the constraint, prefer a deterministic Node.js
generator that reads the approved design brief and writes PBIR JSON. If
delegation is still useful, split it by page or visual family and give each
subagent the relevant brief excerpt, exact fields/measures, and layout/visual
contract; have it return scoped PBIR JSON or a patch for the owning agent to
integrate and validate.
## CLI Setup
**Prerequisite: Node.js 20 or later.** Check with `node --version`. If missing
or older, install from [nodejs.org](https://nodejs.org/) or via your package
manager — Windows: `winget install OpenJS.NodeJS.LTS`; macOS: `brew install node`;
Linux: distro package or [nodesource](https://github.com/nodesource/distributions).
Before using the CLIs in a session, ensure the latest global versions are
installed:
```bash
npm install -g @microsoft/powerbi-report-authoring-cli@latest @microsoft/powerbi-desktop-bridge-cli@latest
```
Confirm both are on `PATH`:
```bash
powerbi-report-author --version
powerbi-desktop --version
```
## PBIR File Layout
A PBIP project on disk looks like this:
```text
<Report>.pbip # Project manifest
├── <Report>.Report/
│ ├── .platform # Fabric metadata (type, logicalId)
│ ├── definition.pbir # Report → SemanticModel binding
│ ├── definition/
│ │ ├── version.json # Format version (e.g. "2.0.0")
│ │ ├── report.json # Report-level: themes, settings, resources
│ │ └── pages/
│ │ ├── pages.json # Page order + active page name
│ │ └── <pageId>/
│ │ ├── page.json # Page: displayName, size, type, filters
│ │ └── visuals/
│ │ └── <visualId>/
│ │ └── visual.json # Visual: type, position, query, formatting
│ ├── CustomVisuals/ # Third-party .pbiviz packages
│ └── StaticResources/
│ ├── SharedResources/BaseThemes/ # Built-in base themes
│ └── RegisteredResources/ # User images, custom theme JSON
└── <Report>.SemanticModel/ # OUT OF SCOPE
```
### Key Files
| File | Purpose | Agent rule |
|------|---------|------------|
| `.platform` | Fabric/PBIP report item metadata | Keep it with the `.Report` folder |
| `definition.pbir` | Report → semantic model binding via `byPath` or `byConnection` | Preserve schema/version unless intentionally migrating |
| `version.json` | PBIR format metadata | Preserve the full scaffolded file, including `$schema` |
| `report.json` | Report-level settings, themes, resources | Edit through references and validate after changes |
| `pages.json` | Page order and active page | Add every new page to `pageOrder`; preserve `activePageName` |
| `page.json` | Page metadata, size, filters | Preserve dimensions unless resizing is approved |
| `visual.json` | Visual type, position, query, formatting | Validate roles and formatting with CLI metadata |
| `localSettings.json` | User-local settings | Do not commit or rely on it |
Schema URLs use the prefix `developer.microsoft.com/json-schemas/fabric/item/report/definition/`.
The suffixes are versioned PBIR contracts that Power BI Desktop bumps with most
releases (e.g. `visualContainer/2.9.0`, `page/2.1.0`, `report/3.3.0` at the
time of writing — newer values may appear in any user's PBIP). When editing,
**always preserve the existing `$schema` value**; when adding a new file, copy
the `$schema` URL from an existing file of the same type in the same report.
Do not invent or bump versions on your own. Validate with `powerbi-report-author validate`.
---
## Authoring Metadata & Validation CLI
Use `powerbi-report-author` whenever you need PBIR facts that should not be
guessed: visual types, data roles, formatting objects, property names, enum
values, selectors, expression/value encodings, and report validation. The CLI is
the source of truth for PBIR authoring details; examples and memory are not.
| Command | Purpose | When to use |
|---------|---------|-------------|
| `catalog list` | List all built-in visual types (and any deprecated entries) | Choosing a visual type |
| `catalog describe <type>` | Roles, formatting keys, cardinality | Before creating/editing a visual |
| `formatting list-objects <type>` | Valid `objects.*` keys + VCO keys; flags objects needing id selectors | Before applying formatting |
| `formatting describe-object <type> <object>` | Property names, types, enum values, descriptions; `_selectorHint` when id selector required | Finding exact property names and allowed values |
| `formatting describe-property <type> <object> <prop>` | Focused single-property lookup | When you already know the object and want just one property |
| `formatting search <type> <regex>` | Regex search across all formatting objects + VCOs | **When you don't know which object a property belongs to** |
| `formatting list-vcos` | Enumerate shared visualContainerObjects | Auditing chrome/container formatting surface |
| `validate <path>` | Full validation of a `.pbip` or `.Report` directory: JSON Schema, structure, IDs, formatting properties, enum values, nesting, layout bounds, theme | **After every batch of changes** |
| `preview-* <path> [--with-derived]` | Report inventory: `preview-visuals`, `preview-pages`, `preview-filters`, `preview-themes` | Auditing existing report content |
| `--help` / `<command> --help` | Command syntax and available options | Before using an unfamiliar command or flag |
More commands: [`powerbi-report-author-cli.md`](references/powerbi-report-author-cli.md).
### Validation result handling
Run `powerbi-report-author validate <path-to-.Report-dir>` after every logical
batch of PBIR edits.
- `failed` / non-zero exit: fix every error before Desktop reload. Desktop may
reject or misrender invalid PBIR.
- `succeededWithWarnings`: review warnings before proceeding. Unknown visual
types or theme visual keys usually mean a typo unless the report intentionally
uses a custom `.pbiviz`.
- Diagnostics include file paths and JSON paths. Use them to jump directly to
the broken node.
- For large diagnostics, use `--pretty` for readable output or `--out <file>` to
write the full result to a file.
## Visual Capability Guardrails
Use these as pre-edit safety rails. Always confirm exact roles, formatting
objects, properties, enum values, and selectors with `powerbi-report-author`
before editing.
### Prefer modern visual types
Never create legacy visual types. If repairing an existing legacy visual,
migrate to the modern type and rebuild roles/formatting from CLI metadata.
| Do not create | Use instead |
|---|---|
| `card` | `cardVisual` |
| `multiRowCard` | `cardVisual` — use multi-value `cardVisual` (multiple projections in `Data`) for multiple KPIs |
| `table` | `tableEx` |
| `matrix` | `pivotTable` |
| `map`, `filledMap` | `azureMap` |
### Instance Selectors
Some formatting objects need `{ id: ... }` selectors. Run `formatting
list-objects` and `formatting describe-object`; follow `_selectorHint` and the
dual-entry pattern in `references/formatting.md`.
## Edit → Validate → Reload → Screenshot Loop
For rendered-output changes, follow this loop. Do not report completion until
validation, reload, and screenshot review are clean.
```text
┌──────────────────────────────────────────────────────────┐
│ 1. Edit PBIR files │
│ 2. Validate → errors? fix and go to 1 │
│ 3. Desktop status → choose the correct bridge PID │
│ 4. Desktop reload → error? fix PBIR and go to 1 │
│ 5. Screenshot/review → issues? fix and go to 1 │
│ 6. Clean → report completion │
└──────────────────────────────────────────────────────────┘
```
**Rules:**
- **Step 2** — `powerbi-report-author validate <path-to-.Report-dir>`. Pass the
report definition directory (e.g., `Sales.Report`), not the `.pbip` file or
project root. Fix all errors before reload — invalid PBIR errors will surface
in Desktop.
- **Steps 3–5** — use `powerbi-desktop` CLI: `status` to choose the PID, then
`reload --pid <pid>` for PBIP/PBIR current files and screenshots from the
same PID. Then perform the screenshot review below.
After `status`, if the selected instance has `hasUnsavedChanges: true`, do
not reload yet; ask the user to save or discard their Desktop UI changes,
rerun `status`, and continue only once it is false.
`reload` covers report/PBIR changes only. For semantic-model/TMDL changes,
use a semantic-model skill or Modeling MCP and reopen the PBIP if changes are
not reflected.
**Exception:** Theme JSON files are cache-keyed by name — Desktop may not
pick up edits on reload. Either rename the theme file with a random suffix
(and update `report.json`), or close and reopen Desktop.
**Desktop CLI commands:**
| Command | Purpose | When to use |
|---|---|---|
| `open "<path.pbip>"` | Launch Power BI Desktop for a PBIP/PBIX | Starting Desktop or opening the target report |
| `status` | List Desktop Bridge instances, current files, report dirs, and bridge state | Before reload/screenshot; choose the correct PID |
| `reload --pid <pid>` | Reload the selected Desktop instance's current PBIP report files | After validated PBIR edits in an open PBIP |
| `screenshot <page-id> --pid <pid> --output <file>` | Capture one page by PBIR page ID | Isolated page changes |
| `screenshot-all --pid <pid> --output-dir <dir>` | Capture every report page | Theme, navigation, page-order, or report-wide changes |
Use `powerbi-desktop screenshot <page-id> --pid <pid>` when only one PBIR
page needs review. `reload` is supported only for PBIP-backed reports. No `powerbi-desktop` command accepts `--report`; use `status`
to select the Desktop instance by PID because the same PBIP can be open in more
than one process. Screenshots default to scale `2`. Run reload and screenshot
operations serially per PID — never in parallel against the same PID, even as a
workaround for a slow or retryable error. Read `references/powerbi-desktop.md` for the
complete command reference and troubleshooting workflow.
**Common Desktop CLI outcomes:**
| Output/error | Meaning | Action |
|--------------|---------|--------|
| `"status": "not_connected"` | No Desktop Bridge discoverable | Run `powerbi-desktop open "<path.pbip>"` or ask the user to start Desktop. If still unreachable, ask them to enable the Desktop preview feature — see [docs](https://aka.ms/Report_Authoring_skill_LearnDocs) |
| `AMBIGUOUS_DESKTOP_INSTANCE` | More than one Desktop Bridge instance is available | Run `powerbi-desktop status`, choose the intended PID, retry with `--pid` |
| `METHOD_NOT_AVAILABLE` | Desktop build lacks a required production bridge method | Tell the user Desktop is stale/unsupported — see [docs](https://aka.ms/Report_Authoring_skill_LearnDocs) |
| `HostNotReady` / retryable bridge error | Desktop is up but the report host isn't ready (often briefly after a reload) | CLI auto-retries; rerun once if it surfaces. Do not add custom sleeps — rely on the CLI's retry path. |
| `Timeout` (bridge error) | A reload or screenshot exceeded the CLI's retry budget | Confirm `status` shows `bridgeStatus: "connected"`, then rerun once. If `Timeout` persists, raise the budget (e.g., `reload --pid <pid> --wait-seconds 120`). If `bridgeStatus: "error"` or `status` hangs, ask the user whether a Desktop modal dialog is blocking input. |
| `Cancelled` during screenshot/reload | A reload/screenshot was cancelled — usually a concurrent reload/screenshot on the same PID. Distinct from `Timeout` (operation ran too long). | Run reload and screenshot serially per PID. Wait for `connected` via `status`, retry one at a time. |
| `ReportDefinitionValidationFailed` | Desktop rejected the PBIR definition | Fix PBIR, run `powerbi-report-author validate <path>`, then reload again |
| `REPORT_DIR_REQUIRED` | Selected PID has no PBIP/PBIR current file; reload/screenshot-all need PBIP/PBIR state | Select the correct PID from `status` or open the target PBIP |
### Screenshot Review
After taking screenshots, perform an independent rendered-output review before reporting completion. Read [`screenshot-review.md`](references/screenshot-review.md), check layout, data rendering, formatting/theme, slicers, and common screenshot failure modes, then fix PBIR and repeat the loop until clean.
---
## Validation
Run `powerbi-report-author validate <path>` after every logical batch of PBIR
changes. Prefer the `.Report` directory; the CLI also accepts a `.pbip` file or
a project root containing a single `.Report` directory. Errors block Desktop
reload — fix them first. Review warnings and fix unless there's a clear reason
not to.
The validator is an offline preflight covering PBIR structure, JSON/schema
validity, cross-file references, IDs/names, visual types, role bindings,
filters, formatting objects/properties/enums/selectors, visualContainerObjects,
theme registration, layout bounds, and selected Desktop/rendering failure
patterns. It does not replace Desktop reload and screenshot review.
---
## Anti-Patterns and Pitfalls
| Pitfall | Consequence | Fix |
|---------|-------------|-----|
| Using `"Entity"` inside filter `Where` conditions | Filter silently fails | Use `"Source"` with the alias from `From` |
| Omitting `nativeQueryRef` | Visual calculations may break | Always include `nativeQueryRef` |
| Reusing visual/filter names | Unpredictable behavior | Generate unique IDs |
| Setting `visualType` to invalid string | Visual renders as error box | Run `powerbi-report-author catalog describe <type>` or `powerbi-report-author catalog list` |
| Wrong role name for visual type | Field is ignored; visual blank | Match role names from `powerbi-report-author catalog describe <type>` |
| Mixing `Column` and `Measure` types | Query fails; visual error | Columns use `Column`, measures use `Measure` |
| Forgetting to add page to `pages.json` | Page invisible | Add to `pageOrder` array |
| Booleans without correct format | Wrong type | `"true"` / `"false"` (no suffix, unquoted in Value) |
| Numbers without type suffix | Type mismatch | `D` for decimals, `L` for integers |
| Editing `$schema` version | PBI Desktop may reject | Preserve existing version |
| Stringified JSON in `paragraphs` | Textbox shows nothing | `paragraphs` is a native JSON array |
| Using textbox as a thin line/divider | Renders ~24px tall regardless of `height` | Use a `shape` visual (rectangle) instead — shapes respect small dimensions |
| `visualContainerObjects` as sibling of `visual` | Schema validation error in PBI Desktop | Must be **inside** `visual` object, as sibling of `objects` |
| Using `tableEx` with dimension columns and measures all in `Values` | Headers render but no data rows even when DAX confirms data exists | Use `pivotTable`; put dimensions in `Rows` and measures in `Values` |
| Using PowerShell `ConvertTo-Json` to edit visual JSON | Property reordering, nesting depth truncation (`-Depth` default is 2) | Use Node.js for JSON manipulation, or always pass `-Depth 20` and verify structure |
| Using regex or string replacement to modify JSON files | Corrupts nesting structure — properties end up inside sibling values, braces misalign | Read file → `JSON.parse` → modify object → `JSON.stringify` → write back. Or use the `edit` tool with exact old/new string matching |
| `dataPoint.fill` without a selector on single-series charts | Bars/columns invisible despite data in tooltips | Use `dataPoint.defaultColor` for a base color without a selector; `fill` requires a `metadata` selector |
| Using `dataPoint.defaultColor` on multi-series charts | All series/categories get the same color — no visual differentiation | Use theme `dataColors` for consistent palette across visuals, or `dataPoint.fill` with `metadata` selectors for per-series overrides — see [color-strategy.md § Color Strategy Quick Reference](references/color-strategy.md#color-strategy-quick-reference) |
| Clustered bar/column chart colors collapse into one legend color | The visual has a Series role but all bars and legend markers share the same hue | Use per-series `dataPoint.fill` selectors or a theme `dataColors` palette; do not use `defaultColor` on clustered charts |
| Relying on theme `dataColors` alone for cross-visual measure consistency | Same measure gets different colors on different visuals (index-based assignment varies with projection order) | Maintain a measure→color mapping and apply explicit `dataPoint.fill`/`defaultColor` per visual — see [color-strategy.md § Cross-Visual Measure-Color Consistency](references/color-strategy.md#pattern-cross-visual-measure-color-consistency) |
| Using `ThemeDataColor` for explicit per-measure `dataPoint.fill` with metadata selectors | Colors silently resolve to white or black instead of expected palette color | Use `Literal` hex values for explicit color assignments with metadata selectors — `ThemeDataColor` is unreliable in this context |
| Choosing bar/series colors without checking background contrast | Bars or lines invisible against page/card background (e.g., white bars on white canvas) | Always pick saturated, mid-to-dark hues that contrast with the page and VCO background colors |
| `show` property on page-level `background` | Schema error — page `background` only supports `color`, `image`, `transparency` | Only VCO `background` (on visuals) has `show`; page background is always visible |
| Copying property names from doc examples without verifying | Warnings or silent failures — property names vary by visual type | Always run `powerbi-report-author formatting describe-object <type> <object>` for exact property names |
| Guessing which object a property belongs to | Wasted calls checking wrong objects one by one | Run `powerbi-report-author formatting search <type> <regex>` to grep across all objects at once |
| Formatting property has no effect (no error) | Setting `show: false` on cardVisual outline without an id selector — validates but renders unchanged | Check `powerbi-report-author formatting describe-object <type> <object>` for `_selectorHint`; use the dual-entry pattern (static + id selector entries) |
| Using `cardCalloutArea` on a single-value card | Properties validate but have no visible effect — `cardCalloutArea` only renders on multi-value cards (2+ measures in Data) | Use `outline`/`accentBar`/`fillCustom` with `{ id: "default" }` selector for single-value cards. For multi-value cards, `cardCalloutArea` controls per-callout tile styling — see [card.md § Multi-Value Formatting](references/card.md#multi-value-formatting) |
| Using `"Fields"` as the `queryState` role for `cardVisual` | Cards render empty — PBI Desktop cannot resolve the binding. Validator reports `Unknown role "Fields"` and `Required role "Data" missing` | `cardVisual`'s only data role is `"Data"`. `"Fields"` is the legacy `card` visual's role name — never carry it over. Always verify role names with `powerbi-report-author catalog describe cardVisual` — see [card.md § Single-Value Template](references/card.md#single-value-template) |
| Creating separate single-value `cardVisual` instances for multiple related KPIs | Wastes canvas space and misuses the visual type — `cardVisual` natively supports multiple projections in one tile | Default to one multi-value `cardVisual` with all measures as `Data` projections when ≥2 related KPIs are requested. Only use separate cards when per-card styling differences are required — see [card.md § When to Consolidate vs. Keep Separate](references/card.md#when-to-consolidate-vs-keep-separate) |
| Adding multiple fields to button slicer Values or Label roles | Slicer breaks or shows unexpected results — each role accepts only 1 field | Put one field in Values, one in Label; additional fields go to Tooltips |
| Looking at `filterConfig` on other visuals to understand slicer selections | Slicer selections live **only** inside the slicer's own `visual.json` via `expansionStates` + `objects.general.filter`. Always read `references/slicers.md` first when modifying slicers |
| Creating an image visual without prompting for the source type | Wrong visual structure — URL vs local file vs data field each have different schemas and expression types | Always ask the user for the image source (local file / URL / data field) before creating the visual — see [image.md § Source Types Overview](references/image.md#source-types-overview) |
| Creating a data-bound image visual with a field that lacks `dataCategory: ImageUrl` | Visual renders blank or error | **Warn the user first** — the visual will render blank without `dataCategory: ImageUrl`. Present alternatives (other ImageUrl fields, local file, URL) and confirm before creating — see [image.md § Select from data](references/image.md#3-select-from-data) |
| Placing background image on page canvas instead of visual plot area | User asks for "background image" alongside a visual (e.g., "column chart with background image") but image is placed on `page.json → objects.background` instead of `visual.objects.plotArea` | When a background image is requested in the context of a specific visual, default to `plotArea.image`. Only use page-level `background.image` when the user explicitly says "page background" / "canvas background" or no visual context exists — see [image.md § Plot Area Background Image](references/image.md#plot-area-background-image-plotareaimage) |
| Creating a `multiRowCard` visual | Legacy multi-row card — deprecated; `powerbi-report-author validate` warns with `PBIR_VISUAL_TYPE_DEPRECATED`. Often triggered by user phrases like "multi-card", "cards for each metric", or "card per measure" | Always use `cardVisual`. For multiple KPIs, use a single multi-value `cardVisual` with all measures as projections in the `Data` role — see [card.md](references/card.md#multi-value-template) |
| Using `map` or `filledMap` instead of `azureMap` for map visuals | Legacy Bing Maps visuals — deprecated and must not be created; `powerbi-report-author validate` warns with `PBIR_VISUAL_TYPE_DEPRECATED` | Always use `azureMap` — see [map.md](references/map.md). If the map fails to render or geocode, debug the fields, try alternative geographic columns/coordinates, or ask the user — do **not** silently substitute a non-map visual without consulting the user first |
| Creating `tableEx`/`pivotTable` without `columnAdjustment: growToFit` | Columns shrink-wrap to content, leaving unused whitespace | Always set `columnHeaders.columnAdjustment` to `growToFit` and `autoSizeColumnWidth` to `true` — see [table.md](references/table.md#default-rule--grow-to-fit) |
| Custom table/matrix row colors with no effect (white background) | Default style preset overrides `objects`-level `backColorPrimary`/`backColorSecondary` | Set `stylePreset` VCO to `'None'` on every `tableEx`/`pivotTable` with custom colors — see [table.md § Style Presets](references/table.md#style-presets-for-tables) |
| Table cells white despite dark VCO background | `visualContainerObjects.background` only controls outer container — table cells paint on top | Set dark colors in `objects.values.backColorPrimary/Secondary` and `objects.columnHeaders.backColor`, not in VCO — see [re-theming.md § Dark Mode Checklist](references/re-theming.md#dark-mode-authoring-checklist) |
| Dark theme applied but cards/tables/slicers still white | Dark mode triggers every formatting trap simultaneously | Follow the full [re-theming.md § Dark Mode Authoring Checklist](references/re-theming.md#dark-mode-authoring-checklist) — covers stylePreset, fillCustom+id selector, objects vs VCO, and contrast audit |
| Theme JSON changes do not appear after Desktop reload | Desktop caches theme files by file name | Rename the theme JSON with a small random suffix, update the theme registration in `report.json`, then reload; otherwise close and reopen Desktop |
| Placing `sortDefinition` inside `visual` or at root of `visual.json` | Schema validation error; sort silently ignored — chart falls back to alphabetical | `sortDefinition` is a property of **`query`** — use `visual.query.sortDefinition`. Supported since `visualConfiguration/2.2.0` |
| Container shape fill doesn't match reference | Text invisible or wrong background color | Match the fill color and transparency to the reference image. If the page background already provides the color, skip the shape entirely. If the shape must be invisible, verify text color still contrasts with the page canvas — see [shape.md § Container Shapes](references/shape.md#container-shapes) |
| Shape text invisible after re-theme | Shape `text` object has no explicit `fontColor` — text inherits theme foreground, but when `fill` is a light color (e.g., white pill/button) on a light page canvas, inherited dark foreground may not render or the fill blends with canvas making text vanish | Always set explicit `fontColor` on shape `text` objects (in the `{ selector: { id: "default" } }` entry). During re-theming, audit all shapes with `text.show: true` — bulk hex-replacement misses shapes that need a *new* `fontColor` property added |
| Enabling `logAxisScale` on data with zero or negative values | PBI Desktop silently falls back to linear scale with a warning — log of zero/negative is undefined | **Warn the user before applying.** Use `ask_user` to present alternatives (filter negatives, switch measure, use `labelDisplayUnits`). Apply `logAxisScale: true` only after the user resolves negative values or confirms all bound values are positive — see [cartesian.md § Log Scale](references/cartesian.md#log-scale-logaxisscale) |
| Changing theme without sweeping inline overrides | Old colors remain on shapes, page backgrounds, nav buttons, textboxes — theme-only change has no effect on hardcoded `Literal` hex values at Priority 2 in the cascade | When the report has per-visual color overrides, follow [re-theming.md § Re-theming Workflow](references/re-theming.md#re-theming-an-existing-report) Steps 0–3: build a color mapping, update theme JSON, then bulk-sweep `definition/` files for old hex values before reload |
| Changing only `dataColors` in theme without sweeping | Shapes, accent bars, nav button borders retain old accent colors — they use hardcoded Literal hex from the old `dataColors` array, not `ThemeDataColor` references | Sweep ALL old `dataColors[N]` hex values across `definition/` files. Even same-polarity "just change the accent/data colors" requests need the full sweep — shapes and nav elements commonly hardcode `dataColors[0]` as accent fills/outlines. |
## Official Documentation
For Microsoft Learn setup guidance, support constraints, and feature
availability, use [Power BI report authoring docs](https://aka.ms/Report_Authoring_skill_LearnDocs).