
Pbi Report Design
- 67 installs
- 836 repo stars
- Updated July 29, 2026
- data-goblin/power-bi-agentic-development
Apply Power BI report design principles covering visual hierarchy, layout, spacing, color discipline, chart selection, KPI cards, tables, and accessibility.
About
The Power BI report design canon covering design identity, the 3-30-300 rule, layout, color, chart selection, KPI and card design, tables, and accessibility. A developer loads it when committing a design identity or making layout, color, chart, or accessibility decisions for a report.
- Covers visual hierarchy, spacing, color, and chart selection
- Shared design reference routed to by report-building skills
Pbi Report Design by the numbers
- 67 all-time installs (skills.sh)
- Ranked #1,191 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/data-goblin/power-bi-agentic-development --skill pbi-report-designAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 67 |
|---|---|
| repo stars | ★ 836 |
| Last updated | July 29, 2026 |
| Repository | data-goblin/power-bi-agentic-development ↗ |
What it does
Apply Power BI report design principles covering visual hierarchy, layout, spacing, color discipline, chart selection, KPI cards, tables, and accessibility.
Files
Power BI Report Design
Report modification requires tooling. Two paths exist:
1. `pbir` CLI (preferred) -- use thepbircommand and thepbir-cliskill. Install withuv tool install pbir-cliorpip install pbir-cli. Check availability withpbir --version.
2. Direct JSON modification -- ifpbiris not available, use thepbir-formatskill (pbip plugin) for PBIR JSON structure and patterns. Validate every change withjq empty <file.json>.
>
If neither thepbir-cliskill nor thepbir-formatskill is loaded, ask the user to install the appropriate plugin before proceeding with report modifications.
Best practices and guidelines for Power BI report design. Follow these guidelines strictly to avoid generic, poorly formatted reports ("Power BI Slop").
Be innovative, pushing boundaries while adhering to data visualization rules and guidelines. Work within Power BI's constraints, aiming for simplicity and effectiveness over aesthetics and decoration. Focus reports, pages, and visuals on answering specific questions and minimizing cognitive load -- not on looking "pretty and impressive".
When a user request contradicts these guidelines, push back and explain better alternatives. The goal is to inform the user of options that lead to improved outcomes.
Design identity (commit before layout)
Before any layout work, lock the report's identity so every later choice has something to cite:
1. Pick one tone ... a committed ink budget (restrained, corporate, editorial, or technical) that fixes accent count, saturation, gridline weight, and how forward annotation gets 2. Pick one signature (two at most) ... a single recurring element (header band, single-accent-hue rule, fixed nav rail, KPI silhouette, callout style, target-line convention) repeated identically on every page, so the report reads as one artifact 3. Route each page to one shape by the question it answers (summary, monitoring, exploration, comparison, narrative)
Recording tone and signature once is what turns "use muted colors" into a propagating decision. See references/design-identity.md for the tone budgets and signature catalog, and references/page-shapes.md for routing each page to a shape. Serialize the identity into the theme via the modifying-theme-json skill, not into per-visual overrides.
Core rules
1. 3-30-300 Rule: The most important and least detailed information should be in the top-left (KPIs, cards, etc.) while the least important and most detailed information should be in the bottom-right 2. Titles: All report pages should have a title using a textBox visualType or a title in a background image added to the report page canvas 3. Visual positioning, alignment, and spacing: All visuals must have equal spacing between them and equal spacing between the edge of the page (the margin). If visuals are unaligned or this spacing is unequal, fix it to ensure alignment and equal spacing 4. Themes: Reports should use a theme that differs from the default Power BI themes; a suggested theme is the sqlbi theme (see the modifying-theme-json skill for applying templates). Themes are preferred because they provide a set of default styles for all charts that can adhere to good design practices and brand or style guidelines 5. Semantic Models: Reports in Power BI are complex. They are dependant on an underlying semantic model (either in the .SemanticModel folder, called a "thick report" or a published model in Power BI/Fabric, called a "thin report"). Much of the functionality from a Power BI report comes from its semantic model design or DAX code 6. Report extensions, or thin report measures: It is possible to create calculation logic in Power BI report, called "thin report measures" or "visual calculations". These should be used sparingly and only for "report-specific" scenarios 7. Visual fields: All data visuals should have field bindings, and all field bindings should be for fields that actually exist in the model; there is no reason for visuals to exist that have no fields bound 8. Chart selection: Make smart choices about what visuals to use for each scenario. Visual vocabulary is essential for this skill. 9. Use of color: Color and formatting decisions must cite the locked identity (the chosen tone and signature), not be picked per visual. Colors come from the theme (themedataColor) rather than inline hex, so the identity propagates and re-themes cleanly. Colors should be muted and soft; colors that implicitly encode meaning (like red=bad, green=good) should be avoided unless using them for that encoding. Consider colorblindness and use accessible palettes (blues instead of greens with reds, for instance). 10. Pre-attentive attributes: Styles and colors should be used to steer and direct attention, and not to decorate charts. Formatting of visuals should be intentional and not purely aesthetic. Styles should where possible be stored in the theme and not in bespoke visual configuration. 11. Fonts: Prefer Segoe UI and Segoe UI Semibold. Do not use custom fonts, since they aren't guaranteed to render on user computers. Evaluate whether fonts are sufficiently large to be readable given the visual and page size. 12. Page intent: Route each page to exactly one shape by the question it answers (see references/page-shapes.md). A page trying to answer two questions is two pages.
Page Layout Guidelines
Check Page Size Before Modifying
Always query the actual page dimensions before adding or repositioning visuals. Do not assume a page is 1280x720 or 1920x1080 -- templates and existing reports vary. The object model validates that visuals fit within page bounds, so setting position or size without knowing the page dimensions will cause errors. Check the page's page.json file for width and height properties, or use the object model (page.width, page.height) to confirm dimensions first. When resizing visuals via the object model, set width/height before x/y to avoid intermediate states that exceed bounds.
Standard Page Size
- Width: 1280px (default)
- Height: 720px (16:9 aspect ratio)
- Alternative: 1920x1080 for high-resolution displays
Visual Spacing
- Minimum gap between visuals: 16px
- Edge margins: 24-32px from page edges
- Consistent alignment: Use grid-based positioning
- Equal spacing is mandatory: Every gap between adjacent visuals (horizontal and vertical) must be the same value. Every margin from the page edge must be the same value. Calculate positions arithmetically from (margin, gap, page_width, page_height) to guarantee alignment. If visuals are misaligned or gaps are unequal, fix immediately -- this is one of the most visible quality signals in a report.
Detail Gradient
Arrange content following the "detail gradient":
+------------------+------------------+
| KPIs/Cards | KPIs/Cards | <- Top: High-level, important
| (Summary) | (Summary) |
+------------------+------------------+
| |
| Charts/Trends | <- Middle: Context, trends
| (Analysis) |
| |
+------------------+------------------+
| |
| Tables/Details | <- Bottom: Detailed data
| (Drill-down) |
| |
+------------------+------------------+Visual Count Guidelines
- Maximum visuals per page: 12-15 (performance impact)
- Maximum KPIs/Cards: 4-6 at top
- Maximum slicers: 3 per page (use filter pane instead)
Page Titles
Every page should have a title. Create a textbox visual.json file manually (see pbir-format skill in the pbip plugin for JSON structure) with position x=20, y=20, width=400, height=60. Set the paragraph content in the visual's config:
{
"singleVisual": {
"visualType": "textbox",
"paragraphs": [
{"textRuns": [{"value": "Page Title"}]}
]
}
}Title positioning:
- Top-left corner: x=24, y=24
- Height: 48-64px
- Width: 400-600px (or page width minus margins)
Theme Guidelines
Always Check Theme First
Before modifying visual formatting:
1. Check theme wildcards: visualStyles["*"]["*"] 2. Check visual-type overrides: visualStyles.lineChart["*"] 3. Only override in visual.json if truly one-off
When to Modify Theme vs Visual
- All visuals of a type need change -> theme
- Establishing design standards -> theme
- Single visual exception to a theme rule -> visual
- Content-specific highlight (one callout, one reference line) -> visual
Theme Color Usage
Prefer theme colors over hex codes:
// Good - uses theme color
"expr": {"ThemeDataColor": {"ColorId": 1, "Percent": 0}}
// Avoid in visuals - use only in extension measures
"expr": {"Literal": {"Value": "'#118DFF'"}}Semantic colors (return from extension measures):
"good"- Positive values (green)"bad"- Negative values (red)"neutral"- Neutral/unchanged (gray/yellow)"minColor"- Gradient minimum"maxColor"- Gradient maximum
Accessibility (WCAG 2.1)
Alt Text
All visuals should include descriptive alt text:
"visualContainerObjects": {
"general": [{
"properties": {
"altText": {
"expr": {"Literal": {"Value": "'Line chart showing monthly sales trend from January to December 2024'"}}
}
}
}]
}Color Contrast
- Text on background: minimum 4.5:1 contrast ratio
- Large text (18pt+): minimum 3:1 contrast ratio
- Don't rely solely on color to convey meaning
Font Sizes
- Minimum readable: 12pt
- Recommended for charts: 14pt
- Titles: 16-24pt
- KPI values: 24-48pt
Shadows and Motion
- Minimize drop shadows (vestibular issues)
- Avoid animations where possible
- Use
dropShadow.show: falsein theme wildcards
Visual Best Practices
Cards and KPIs
A bare number lacks meaning. Every KPI must answer "Is this good or bad?" (target + gap) and "Is it getting better or worse?" (trend). Key rules:
- Position at top or left of page, maximum 5 per page
- Prefer `kpi` visual type over `card` when a target exists -- it has built-in indicator, goal, and trend line data roles
- Always include a target and gap (absolute + percentage). If no target measure exists, discuss with the user: propose adding a prior-year measure to the semantic model (use Tabular Editor CLI or the
tmdlskill), or creating an extension measure as a fallback. Common targets: prior year (CALCULATE([Measure], DATEADD('Date'[Date], -1, YEAR))), budget, or rolling average - If no clear target exists, ask the user -- do not leave KPIs bare. Discuss whether prior period, budget, or a custom threshold makes sense
- Apply conditional formatting to the gap, not the primary value
- Pair color with a secondary cue (arrow/icon) for accessibility
- Round aggressively at summary level ("518M" not "517,893,412")
- Choose actionable metrics that drive decisions over vanity metrics (apply the "20% change test": if this number changed 20%, should someone act differently?)
- Hide redundant auto-generated subtitles
- Use SVG extension measures for inline icons (see the
svg-visualsskill in the custom-visuals plugin)
For complete guidance on KPI design, targets, trends, formatting hierarchy, icon implementation, accessible palettes, and anti-patterns, consult `references/cards-and-kpis.md`.
Charts
- Sort by value descending (unless time-based)
- Minimize gridlines and axes clutter
- Use muted colors for non-essential elements
- Highlight key data points sparingly
For chart-type selection (encoding hierarchy, Cleveland-McGill ranking), data-label discipline, and small-multiples guidance, consult `references/chart-selection.md`.
Refuse common LLM defaults. Gauge-as-KPI, monochrome categorical bars, missing sort, card walls, raw field names as titles, inline hex, off-grid drift, dual y-axis, 3D, and oversized pies are plausible-looking attractors that defeat the reader. When a request reaches for one, push back with the better option; see `references/anti-patterns.md`.
Tables and Matrices
Tables require deliberate design -- "easy to create" differs from "easy to read." Key rules:
- Position at bottom for detail drill-down (3-30-300 rule)
- Decision-making first: define the question, audience, and action before building
- Use
matrixovertableExwhen 2+ categorical columns form a hierarchy (Key Account > Account > Product) - Subtract, don't add: remove gridlines and heavy banding; let whitespace separate rows
- Sort by the most important measure (often variance), not alphabetically
- Apply data bars to the primary measure column for magnitude scanning
- Apply color scales to variance columns only -- formatting everything means formatting nothing
- Add sparklines where temporal context ("improving or declining?") matters
- Show full precision (no display units) -- tables are where readers go for detail
For complete guidance on table vs matrix selection, formatting philosophy, conditional formatting techniques, sparklines, hierarchy design, and anti-patterns, consult `references/tables-and-matrices.md`.
Slicers
- Maximum 3 per page
- Position consistently (top or left)
- Use filter pane for additional filters
- Consider sync slicers across pages
Filter Pane
The filter pane has its own information architecture beyond color and chrome. Key decisions: lock vs hide per filter card, how to name cards without renaming model fields, and report-level settings (Apply button, search, allow-change-filter-type). See `references/filter-pane.md`.
Mobile
Power BI does not reflow a desktop page for portrait; the phone layout is a hand-picked subset re-placed on a narrow grid. Nothing renders on a phone until explicitly opted in with a mobile.json. See `references/mobile.md` for the subset-selection model and file mechanics.
Report Evaluation Criteria
When asked to evaluate or audit a report, focus on objective criteria. Subjective evaluation is difficult for AI -- the report cannot be "seen" directly, and there is no intuitive sense of aesthetics, cognitive load, or effectiveness. Emphasize this limitation to users.
Objective Checklist
1. Page count: More than 5-8 pages is typically excessive 2. Visuals per page: Count is a proxy; query cost per visual is the driver (see references/layout-guidelines.md performance section). Textboxes, images, shapes, and buttons do not emit queries. 3. Theme usage: Reports should use a custom theme for consistent formatting 4. Layout consistency:
- Equal spacing between visuals?
- Page has a title?
- Follows detail gradient (important top-left, detailed bottom-right)?
- Cards/KPIs at top or left, fewer than 4-6?
- Fewer than 3 slicers (use filter pane instead)?
- Helpful elements (refresh date, links, context)?
5. Color effectiveness:
- Conditional formatting used sparingly (not causing overload)?
- Colors muted and passive (not loud and bright)?
- Negative-sentiment colors (red, orange) only for negative values?
6. Information:ink ratio: Non-essential elements reduced (lighter gridlines, disabled unnecessary axes/labels)? 7. Readability: Font sizes sufficient for all elements? 8. Accessibility: Minimal shadows to avoid vestibular issues? 9. Font consistency: Limited font sizes, simple readable fonts that work on all devices? 10. Sorting: Visuals sorted descending unless there's a reason otherwise (time-based, etc.)?
Evaluation Output
When evaluating, provide:
- Issues found with specific locations
- Severity (critical, warning, suggestion)
- Recommended fixes with commands or patterns
The design gate
Before declaring a design done, run the closing gate in `references/quality-gate.md`. It is not the planning-stage checklist that runs before the build; it runs against the finished artifact and decides whether it ships. It verifies the identity propagated, every page has one intent, spacing and margins are equal and on-grid, callouts are backed by model evidence, and accessibility is met. Because an agent cannot see the canvas, pair the gate with the screenshot-review loop in the `pbir-cli` skill: render the pages, look, and confirm what the JSON inferred.
Common Design Issues
Issue: Cognitive Overload
Symptoms: Too many colors, visuals, or data points
Fix:
- Reduce visual count
- Use muted color palette
- Apply detail gradient
- Hide non-essential elements
Issue: Inconsistent Spacing
Symptoms: Uneven gaps, misaligned visuals
Fix:
- Use grid-based positioning
- Standardize visual sizes
- Apply consistent margins
Issue: Poor Readability
Symptoms: Small fonts, low contrast
Fix:
- Increase font sizes (minimum 12pt)
- Check color contrast ratios
- Use appropriate font weights
References
- `references/design-identity.md` -- Commit-first identity: tone budgets (restrained/corporate/editorial/technical), the signature catalog, and serializing identity into the theme
- `references/page-shapes.md` -- Routing each page to one shape by intent (summary/monitoring/exploration/comparison/narrative): what belongs, what stays off, layout lean
- `references/anti-patterns.md` -- Cross-cutting defaults to refuse (gauge-as-KPI, monochrome bars, missing sort, card walls, raw titles, inline hex, off-grid, dual-axis, 3D, oversized pie) with the repair
- `references/quality-gate.md` -- The design gate: closing checks (identity propagated, one intent per page, equal spacing, evidence-backed callouts, accessibility) in issue/location/severity/fix form
- `references/cards-and-kpis.md` -- KPI card design: targets, gaps, trends, formatting hierarchy, icons, accessible palettes, anti-patterns, review checklist
- `references/tables-and-matrices.md` -- Table and matrix design: decision-making framework, subtract-don't-add philosophy, conditional formatting, sorting, sparklines, matrix hierarchies, anti-patterns
- `references/layout-guidelines.md` -- Canvas dimensions, spacing tiers (intra-group/inter-group/margin), alignment rules, performance cost model
- `references/visual-colors.md` -- Color principles, CF basis decision (gradient vs rules vs field-value vs icons), semantic tokens, accessibility
- `references/page-titles.md` -- Title implementation, accessible title wording, hidden-title/alt-text rule
- `references/chart-selection.md` -- Encoding hierarchy (Cleveland-McGill ranking), chart type routing, data-label discipline, small multiples
- `references/tooltips-and-annotations.md` -- Report-page tooltip design, when not to use one, annotation primitives for guided analytics
- `references/filter-pane.md` -- Lock vs hide, card naming, card order, Applied/Available styling, report-level settings
- `references/mobile.md` -- Phone layout as a curated subset,
mobile.jsonmechanics, what to include/exclude - `references/custom-visuals.md` -- Build-vs-buy ranking, AppSource/org-store tradeoffs, licensing gaps
Related Skills
Report Structure and Format
- `pbir-format` (pbip plugin) -- PBIR JSON format reference for visual.json, page.json, report.json structure
- `pbip` (pbip plugin) -- PBIP project structure, table/measure renames, project forking
Custom Visuals
Reports often need visuals beyond what Power BI provides natively. Always consider in-repo code paths before reaching for a packaged third-party visual. See `references/custom-visuals.md` for the build-vs-buy decision and AppSource/org-store tradeoffs.
Skill routing for in-repo code visuals (all in the custom-visuals plugin; add with claude plugin install custom-visuals@power-bi-agentic-development):
- `deneb-visuals` -- Vega/Vega-Lite declarative visuals; preferred for advanced custom interactive charts (cross-filtering, tooltips, hover)
- `svg-visuals` -- SVG via DAX measures; preferred for inline table/matrix/card graphics with no row cap issues
- `python-visuals` -- matplotlib/seaborn scripts (static PNG); for statistical visualizations that must compute at render time
- `r-visuals` -- ggplot2 scripts (static PNG); where R's statistical ecosystem has no Python peer (forecast, pheatmap, corrplot)
Semantic Model
Reports are highly dependent on the underlying semantic model for their functionality. Most report capabilities -- measures, calculated columns, relationships, hierarchies, row-level security -- are defined in the semantic model, not the report. When designing or modifying reports, you will frequently need to understand or modify the model. Key skills:
- `tmdl` (pbip plugin) -- Direct TMDL file editing for measures, columns, relationships
- `te-docs` (tabular-editor plugin) -- Tabular Editor CLI for model operations
- `c-sharp-scripting` (tabular-editor plugin) -- C# scripts for bulk model changes
- `bpa-rules` (tabular-editor plugin) -- Best Practice Analyzer rules for model quality
- `connect-pbid` (pbi-desktop plugin) -- Connect to Power BI Desktop's local Analysis Services instance for live model queries and modifications
Anti-Patterns: Defaults to Refuse
These are the choices a model reaches for because they look plausible, not because they are right; attractor states that satisfy the literal request while defeating the reader. This list is cross-cutting and whole-report. It does not replace the per-visual anti-pattern tables in references/cards-and-kpis.md and references/tables-and-matrices.md; it sits alongside them.
When a user asks for one of these, do not silently comply. Refuse and redirect: name the better option in the skill's voice, explain the cost in one line, and offer the repair. The user can still override; the point is that they choose it knowingly.
Each entry: the attractor, why it loses (tied to a canon principle), and the repair with the pbir move where one exists.
gauge-as-KPI
A gauge spends a large area encoding one value on a curved scale that the eye reads worse than a straight one (Cleveland-McGill: position on a common scale beats angle). Repair: a kpi or card with a target and an explicit gap. Swap the type and bind the target role; see references/chart-selection.md for the value-vs-target routing.
monochrome categorical bars
One hue across unordered categories spends the color channel and returns nothing; hue carries no information when every bar is the same. Repair: sort by value descending and let bar length do the comparing, then reserve one accent hue for the single bar you want highlighted. This is the single-accent rule from references/design-identity.md applied to a bar chart.
pbir visuals sort "Page/Visual.Visual" -f "<Table.Measure>" -d Descendingmissing sort
Categorical bars left in load or alphabetical order defeat the length comparison that is the chart's only reason to exist; the reader cannot rank what is not ranked. Repair: sort by the measure descending unless the axis is genuinely time-ordered. Sorting is discussed further in references/chart-selection.md.
pbir visuals sort "Page/Visual.Visual" -f "<Table.Measure>" -d Descendingtoo many cards
A wall of bare cards is data without a question; ten numbers with no hierarchy ask the reader to find the point themselves. Repair: cap the card row to the few that drive a decision (the "20% change test" in references/cards-and-kpis.md), and demote the rest into one table or chart. This is also a page-shape signal: a card wall usually means a summary page that never decided what it summarizes.
raw field names as titles
Sum of SalesAmount as a visual title narrates the query, not the question. A title should state what the visual answers so the reader knows why it is there. Repair: rewrite the visual title to the question ("Sales vs target, by region"); leave the underlying model field name untouched, the title is a report-side string.
pbir set "Page/Visual.Visual.title.text" --value "Sales vs target, by region"inline hex instead of theme references
A literal hex value in visual.json pins a color to one visual; the identity stops propagating and a re-theme leaves it stranded. Repair: use a ThemeDataColor or a semantic token so the color cites the identity and re-themes cleanly. The theme-vs-hex decision and the conversion path live in references/visual-colors.md.
pbir visuals cf "Page/Visual.Visual" --theme-colors "dataPoint.fill" # convert existing hex assignments to tokensoff-grid drift
Positions that are not on the grid unit, and gaps that are close but not equal, read as sloppy even at a 4-8px difference; the eye catches the misalignment before it reads the data. Repair: snap every position to the grid unit and recompute gaps arithmetically from margin, gap, and canvas size. The arithmetic and the continuous-gutter rule are in references/layout-guidelines.md.
dual y-axis abuse
Two unrelated measures on two independent scales invite a false reading: the lines cross or diverge by axis choice, not by the data. Repair: use a second axis only when two genuinely different units share one category axis. Otherwise split into two visuals, or index both measures to a common base so one axis serves both.
3D anything
Perspective distorts the length and area the reader is trying to compare; a 3D bar is taller or shorter by viewing angle, not by value. Repair: flat 2D, always. There is no analytical task a 3D chart does better than its flat version.
pie beyond a few slices
A pie forces angle and area, the weakest channels, and past roughly four parts the slices stop being distinguishable. Repair: a sorted bar chart with the long tail grouped into "Other". The slice-count threshold and the Top-N-plus-Other pattern are in references/chart-selection.md.
pbir set "Page/Visual.Visual.visualType" --value "clusteredBarChart" # swapping the type may need fields rebound to the new rolesCards and KPIs: Design Best Practices
Cards and KPI visuals occupy the most prominent position on a report page (top-left, per the 3-30-300 rule). A bare number lacks meaning -- without context, readers cannot determine whether a metric is good or bad. Human cognition judges magnitude through comparison, not in isolation. Every KPI earning dashboard space must answer two questions without requiring the reader to think:
1. "Is this good or bad?" -- answered by a target and gap 2. "Is it getting better or worse?" -- answered by a trend
Limiting KPI Quantity
Working memory holds approximately 3-4 information chunks. A page with 4 KPIs allows readers to retain the complete picture; 12 cards force exhausting loops of scanning, forgetting, and re-scanning. 5 represents a practical ceiling for most pages.
Selection should be driven by the page's central question. Every KPI must directly serve that question. Metrics that don't contribute constitute noise, regardless of their individual interest.
Choosing Actionable Metrics
Vanity metrics describe activity but don't drive decisions (e.g., total orders always increases). Actionable metrics create decision forks where different values suggest different actions.
A useful test: "If this number changed 20%, should someone act differently?" If the answer is no, the metric hasn't earned its dashboard space.
Comparative metrics (orders vs. prior year) outperform absolute ones because they immediately signal relative performance.
Sourcing Targets
Every KPI needs a target. Where the target comes from depends on the model and context:
| Target Source | When to Use | Example DAX |
|---|---|---|
| Prior year (1YP) | Default choice when no budget exists | CALCULATE([Measure], DATEADD('Date'[Date], -1, YEAR)) |
| Prior month/period | Short-term operational metrics | CALCULATE([Measure], DATEADD('Date'[Date], -1, MONTH)) |
| Budget/forecast | When budgets exist in the model | Direct measure reference |
| Rolling average | Smoothing volatile metrics | CALCULATE([Measure], DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -3, MONTH)) |
Preferred: add targets to the semantic model using Tabular Editor CLI or the tmdl skill. Model-level measures are reusable across reports and benefit from server-side evaluation.
Fallback: extension measures in the report via reportExtensions.json. Use only when the target is report-specific (e.g., a custom threshold unique to one dashboard).
If no clear target exists, use `AskUserQuestion` to discuss options with the user. Do not leave KPIs bare -- a number without context is not actionable.
The Three Elements of a Good KPI
A complete KPI card shows three elements working together:
| Element | Purpose | Example |
|---|---|---|
| Actual value | Shows magnitude | 518M |
| Target / comparison | Establishes the benchmark | Target: 483M |
| Gap (delta) | Explicitly answers "good or bad?" | +35.4M (+7.3%) |
Without gaps, readers must perform mental arithmetic while processing other KPIs. Express gaps in both absolute and percentage terms -- the absolute shows scale, the percentage shows relative significance.
Always label the target -- "Target: 483M" is ambiguous. Set goals.goalText to describe what the comparison actually is: "1YP", "Budget", "3M Avg", etc. This tells the reader what they're comparing against without requiring them to look it up.
Set goals.goalText to the comparison label in the visual.json objects (e.g., "1YP", "Budget", "3M Avg").
Implementation
To build a contextualized KPI card, create a kpi visual.json file manually (see pbir-format skill in the pbip plugin for JSON structure) with title "Revenue MTD", positioned at x=20, y=100, width=300, height=160. Bind field roles: Indicator: __Measures.Actuals MTD, Goal: __Measures.Sales Target MTD, TrendLine: Date.Date.
Alternatively, use a card visual with extension measures that compute the gap. Add these to reportExtensions.json:
// Variance measure
Revenue vs Target =
VAR _actual = [Actuals MTD]
VAR _target = [Sales Target MTD]
VAR _gap = _actual - _target
RETURN FORMAT(_gap, "+#,##0;-#,##0") & " (" & FORMAT(DIVIDE(_gap, _target), "+0.0%;-0.0%") & ")"
// Conditional formatting measure for the gap color
Revenue vs Target Color = IF([Actuals MTD] >= [Sales Target MTD], "good", "bad")Adding Trends
Trends answer: "Is this typical or is something changing?" A sparkline showing performance against target reveals directional movement.
Options for adding trends to KPI cards:
1. KPI visual type: Has built-in trend line support via the TrendLine data role 2. SVG sparkline: An extension measure generating inline SVG (see below) 3. Adjacent line chart: A small line chart positioned next to or below the card
Formatting with Intent
Every visual choice should reinforce the KPI's message.
Size Hierarchy
Primary values should be the largest element. Targets and gaps are smaller. Trends should be compact. The eye naturally follows this hierarchy:
1. Headline number (largest, boldest) 2. Verdict / gap (medium, colored) 3. Supporting context -- target, trend (smallest, muted)
Conditional Color and Symbols
Color signals above/below target instantaneously. Apply conditional formatting to the gap -- the judgment indicator -- not the primary value. Pairing color with directional symbols (arrows) ensures the message doesn't depend on color perception alone.
Apply conditional color to the card's labels.color property in visual.json by binding it to the Revenue vs Target Color extension measure.
Accessible palettes: Some users struggle distinguishing green from red. Blue/orange is a more accessible alternative. Always pair color with a secondary cue (arrow, icon). Configure accessible colors in the theme:
Set accessible sentiment colors in the theme.json file: good: "#2B7A78", bad: "#D4602E".
Display Units and Number Formatting
Round aggressively at the KPI level: "518M" beats "517,893,412". Precision belongs in detail tables, not headline cards. Maintain consistent decimal places and units across all cards on a page.
"Auto" display units do not work reliably when measures have custom format strings (e.g., #,##0). The format string overrides Auto, resulting in raw unrounded numbers. Instead of relying on Auto, query the actual values with the report's active filters, then pick the display unit explicitly per visual.
Display Unit Selection Rule
Pick the largest unit where the displayed integer part is >= 1. The goal is 2-3 visible digits with no leading zero. Apply this algorithm:
value = query result with active filters
if value >= 1,000,000,000,000: unit = 1000000000000 (Trillions)
elif value >= 1,000,000,000: unit = 1000000000 (Billions)
elif value >= 1,000,000: unit = 1000000 (Millions)
elif value >= 1,000: unit = 1000 (Thousands)
else: unit = 1 (None)Then set precision based on digit count of value / unit:
- 1 digit (e.g., 3.8M): precision = 1
- 2+ digits (e.g., 35bn, 338K): precision = 0
Percentage measures (OTD %, Margin %) always use unit = 1 (None) with precision 1 -- the format string handles the % symbol.
Workflow
1. Query values with the report's active filters using DAX Studio or Tabular Editor CLI 2. Apply the selection rule:
- 3,768,335 -> >= 1M -> Millions, 3.8 -> 1 digit -> precision 1 -> "3.8M"
- 35,312,992,122 -> >= 1B -> Billions, 35.3 -> 2 digits -> precision 0 -> "35bn"
- 0.719 (%) -> None, precision 1 -> "71.9%"
3. Set per-visual in visual.json objects (overrides theme default):
indicator.indicatorDisplayUnits= 1000000indicator.indicatorPrecision= 1
KPI indicatorDisplayUnits Enum
| Value | Label |
|---|---|
| 0 | Auto (unreliable with custom format strings -- avoid) |
| 1 | None |
| 1000 | Thousands |
| 1000000 | Millions |
| 1000000000 | Billions |
| 1000000000000 | Trillions |
Labels and Titles
Keep descriptors concise: "Orders MTD" is superior to "Month-to-Date Order Value (EUR, excl. returns)". Overly complex labels suggest unclear definitions in the semantic model.
Title vs. callout/category label -- show one, not both. Card visuals have two places where the metric name appears: the visual title (top of the card) and the category label / callout label (below the value). Showing both is redundant and wastes vertical space. Choose one:
- Category label only (preferred): Hide the visual title (
--no-show). The category label sits below the large number and naturally reads as "3.8M Order Lines". This is cleaner and leaves more room for the value to render at a large font size. - Title only: Hide the category label by setting
categoryLabels.showtofalsein the visual.json objects. Use when the page title textbox already establishes context and the card just needs a small header.
To hide the title, set title.show to false in the visual.json visualContainerObjects. To hide the category label, set categoryLabels.show to false in the visual.json objects.
Card sizing: Ensure cards have sufficient height to render the value, label, and any gap/target text without clipping. Minimum recommended height: 130-150px for a card with value + category label. If the value or label is clipped, increase height before reducing font size.
Hide auto-generated subtitles -- they repeat field binding names and add no information:
Set subtitle.show to false in the visual.json visualContainerObjects.
Icons in KPI Cards
Icons can reinforce the KPI message -- a directional arrow, a warning triangle, or a category icon. In Power BI, icons must be added as extension measures using SVG code following the ImageUrl data category pattern.
Refer to the `svg-visuals` skill (custom-visuals plugin) for the proper logic of creating SVG extension measures. The general pattern:
1. Create a DAX measure that returns an SVG string 2. Set the measure's data category to ImageUrl 3. Bind the measure to a visual role that supports images
Add an extension measure to reportExtensions.json with dataCategory: ImageUrl:
Trend Arrow SVG =
VAR _gap = [Actuals MTD] - [Sales Target MTD]
VAR _color = IF(_gap >= 0, "#2B7A78", "#D4602E")
VAR _rotation = IF(_gap >= 0, "0", "180")
RETURN
"data:image/svg+xml;utf8," &
"<svg xmlns=""http://www.w3.org/2000/svg"" width=""24"" height=""24"" viewBox=""0 0 24 24"">" &
"<polygon points=""12,4 20,16 4,16"" fill=""" & _color & """ transform=""rotate(" & _rotation & " 12 12)""/>" &
"</svg>"Icons should be used sparingly and only when they add information beyond what color and numbers already convey. Do not use icons purely for decoration.
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Bare number with no target | Reader cannot judge performance | Add target and gap |
| Too many cards (>5) | Exceeds working memory | Prioritize by page question |
| Loud colors on primary value | Distracts from the judgment | Color the gap, not the value |
| Excessive precision (517,893,412) | Cognitive overhead at summary level | Round to "518M" |
| Title + category label both visible | Redundant metric name, wastes space | Show one or the other |
| Redundant subtitle | Repeats field name, wastes space | Hide subtitle |
| Card too short (< 130px) | Value or label clipped | Min height 130-150px |
| No trend context | Reader doesn't know direction | Add sparkline or trend indicator |
| Red/green only (no secondary cue) | Inaccessible to colorblind users | Pair with arrow/icon |
| Relying on Auto display units | Custom format strings override Auto, showing raw numbers | Query values, set explicit display units per visual |
| Vanity metrics | Don't drive decisions | Apply the "20% change" test |
Visual Type Selection
| Scenario | Visual Type | Notes |
|---|---|---|
| Value + target + trend line | kpi | Built-in support for all three elements |
| Simple headline number | card | Add extension measures for gap and color |
| Multiple related metrics | multiRowCard | Groups related KPIs compactly |
| Custom layout with sparkline | card + SVG measure | Maximum control, higher complexity |
| Rich formatting with icons | card + SVG extension measures | Refer to svg-visuals skill (custom-visuals plugin) |
Checklist for KPI Card Review
When evaluating or creating KPI cards, verify:
- [ ] Each card answers the page's central question
- [ ] Maximum 5 cards per page
- [ ] Each card has a target or comparison value
- [ ] Gap is shown in both absolute and percentage terms
- [ ] Conditional formatting applied to gap (not primary value)
- [ ] Color paired with secondary cue (arrow/icon) for accessibility
- [ ] Numbers rounded appropriately for summary level
- [ ] Subtitles hidden (no redundant auto-generated labels)
- [ ] Font size hierarchy: value > gap > label > trend
- [ ] Consistent units and formatting across all cards on the page
Chart Selection, Data Labels, and Small Multiples
Encoding Hierarchy: Match the Task to the Channel
Encode the value the reader needs to compare most precisely on the highest perceptual channel available. The Cleveland-McGill accuracy ranking: position on a common scale > length > angle/slope > area > color hue/saturation.
Useful inverse for an agent: given a visual.json on disk, decide whether visualType matches the analytical task and what the minimal repair is.
ranking / magnitude compare -> length on common axis -> barChart / columnChart
trend over time -> position + slope -> lineChart / areaChart
two measures correlated -> 2D position -> scatterChart
part-to-whole, <=3-4 parts -> angle/area (accept) -> pie/donut OR stacked bar
part-to-whole, >4 parts -> length (prefer) -> stackedBarChart, not pie
single value vs target -> position vs marker -> kpi / card + reference lineRead the actual type and roles before judging:
pbir get "Page/MyVisual.Visual" .visual.visualType
pbir visuals bind "Page/MyVisual.Visual" --list-rolesRepair is usually a type swap preserving bindings (pie, donut, and stackedBarChart share Category/Y roles):
pbir set "Page/MyVisual.Visual" .visual.visualType '"clusteredBarChart"'Then sort and validate.
The sampling gate
A type can be schema-valid and role-correct yet wrong because the data shape defeats the encoding. Sample with pbir model -q before trusting a line chart (are there enough distinct x-points to form a trend?) or a bar chart (are there more than two bars?). Two rows back means a line chart is wasted; one dominant slice plus a long tail means the pie should be a sorted bar with the tail grouped into "Other".
Pitfalls
- Pie/donut beyond ~4 slices forces the weakest channels; apply Top N + "Other" rather than adding colors
- A combo chart second axis is justified only when two genuinely different units share a category axis
- Hue is last; do not solve a comparison problem by adding more colors
---
Data-Label Discipline
Labels earn their place only when the exact value matters and cannot be read off the axis: line endpoints, a single highlighted bar, KPI deltas. Per-segment labels and stacked totals are two different objects:
pbir visuals format "Page.Page/Visual.Visual" labels --on # per-data-point labels
pbir set "Page.Page/Visual.Visual" \
'.visual.objects.totals[0].properties.show.expr.Literal.Value' "true" # stacked totalsThe stacked-total "incorrect number" trap
The recurring symptom where a stacked total does not match a hand-summed expectation is almost always a measure problem, not a label problem. The engine's stacked-group sum diverges from the expected total when the underlying measure is non-additive: a ratio, distinct count, or time-intelligence expression. Verify with pbir model -q "EVALUATE SUMMARIZECOLUMNS(...)" before touching the label. If the total is the "wrong" number, the label is honest and the measure needs an explicit aggregation strategy.
Preferred pattern: endpoint-only labels
Prefer binding a measure that returns the value at the last/relevant point and BLANK elsewhere, rather than global labels plus conditional-formatting-based hiding. The chart stays clean and only the number that matters is annotated. This is the extension-measure label pattern described in thin-report-measures references.
Pitfalls
- Small multiples disable stacked total labels entirely (see below)
- Label display units must match the axis units
- A measure returning BLANK beats global-labels-plus-CF: less JSON, less fragile
---
Small Multiples
Choose small multiples over a legend when the question is "does this pattern hold across a dimension" and there are more series than a legend reads cleanly (roughly more than 3-4). A legend overlays series in one frame for value comparison; small multiples separate them into a synchronized grid so you compare shapes across many categories.
Supported only on bar, column, line, and area charts. The grid synchronizes axes and fills left-to-right then top-to-bottom in sort order, with overflow scrolling beyond the visible grid.
The Series vs SmallMultiples role distinction
Series/Legend overlays series in a single frame; true small multiples use the dedicated SmallMultiples role that partitions into the grid. The smallMultiplesLayout object only takes effect when the SmallMultiples role is populated. Confirm before editing:
pbir visuals bind "Page/MyVisual.Visual" --list-rolesFeatures that are inert once a visual is trellised
Do not waste edits on these; they silently have no effect in a small-multiples context:
- Total labels for stacked charts
- Trend lines and forecasting
- Zoom sliders
- Line high-density sampling
- Concatenated axis labels and hierarchical axis (falls back to concatenated)
- Scroll-to-load-more
- Small-multiple cell title display units/decimals/format (control via the model's format string instead)
Check for the SmallMultiples role before adding any analytics overlays.
Pitfalls
- A 6x6 grid of dense charts defeats the purpose; apply Top N on the partition field and group the remainder into "Other"
- Synchronized axes are a feature, not a bug; do not give each cell its own scale unless the analytical intent is explicitly within-series comparison
Custom Visual Selection: Build vs Buy
Default Ranking
Prefer in-repo code paths over packaged third-party visuals. In-repo code (Deneb, SVG-via-DAX, Python/R) lives inside the report or model, travels with it, renders anywhere the engine is enabled, and requires no per-visual admin approval.
1. Core Power BI visual -- no dependencies, fastest
2. Deneb (Vega/Vega-Lite) -- declarative; preferred for advanced interactive charts
3. SVG-via-DAX measure -- preferred for table/card inline graphics; no row cap issues
4. Python/R script visual -- statistical plots that must compute at render time
5. Packaged AppSource visual -- only when the interaction model genuinely needs it
6. Private .pbiviz -- avoid; external risk plus you own the build pipelineReach for a packaged custom visual only when the interaction model genuinely requires it: a rich pre-built hierarchy/network slicer, a specialized gauge type, or a visual category with no reasonable in-repo alternative.
AppSource and Org-Store Tradeoffs
An AppSource visual is an external dependency on a third party's code, AppSource availability, and three tenant settings (allow custom visuals, allow specific visual, allow uncertified). It can vanish, lose certification, or be policy-blocked at any time.
When a packaged custom visual is warranted:
- Prefer the org store over ad-hoc AppSource (centralizes the approved version, single admin toggle)
- Prefer certified visuals (Microsoft-reviewed, sandboxed)
- Record the dependency in a PBIR annotation: name, source, certification status, approver
Licensing and Deployment Gaps
Licensed AppSource visuals do not enforce or report licensing in:
- Power BI Report Server (sovereign clouds, on-premises)
- App-owns-data embed
- Publish-to-web
Org-store/AppSource visuals are unavailable in Report Server entirely. If the report will be distributed via any of these paths, an AppSource dependency is a portability blocker.
The Real Cost of a Custom Visual Dependency
Reaching for a custom visual to avoid learning Deneb or SVG trades a one-time authoring cost for a permanent governance and portability cost. A Deneb spec is version-controlled, diffable, and testable; an AppSource registration is not.
Design Identity
Before any layout work, lock two decisions that propagate to every page and every visual: a tone and a signature. Recording them once turns vague asks like "use muted colors" into a concrete decision the rest of the report has to obey. A report without a locked identity drifts into N independently styled pages that happen to share a file.
Lock these first, then make every later formatting choice cite them rather than picking per visual.
- If the brief does not name a tone or signature, infer them from the audience and purpose, state the pick, and confirm with the user before locking; this is the same "ask when it is not given" stance the KPI guidance takes for targets.
- Record the locked identity where later work will read it: the design brief (the
create-pbi-reportskill) when one exists, otherwise a note in the scratchpad and a comment in the theme. Identity that lives only in one agent's head does not survive a fresh context or a second page. - Every later page inherits the locked identity. Do not re-decide tone or signature per page; re-deciding is the drift the design gate exists to catch.
Tone: the ink budget
Pick exactly one tone. Tone is a budget commitment, not a mood word; it fixes how much ink the report spends, how saturated accents get, how many accent colors are in play, and how aggressively pages are annotated. It draws on the spirit of established practice: Few on data:ink, IBCS on disciplined standards, the FT Visual Vocabulary on explanatory framing.
restrained:
for: execs who want the number, not the chrome
ink: low; generous whitespace; near-monochrome
accents: one hue, low saturation; everything else neutral grey
annotation: minimal; let the headline number carry the page
leans toward: summary and monitoring shapes
corporate:
for: recurring standard reporting, brand-disciplined audiences
ink: predictable; consistent conventions across pages
accents: brand palette, applied to encode (variance, standards), not decorate
annotation: convention-driven (IBCS-style variance/sign markers)
leans toward: comparison and monitoring shapes
editorial:
for: a finding you are walking someone through
ink: moderate, concentrated on one focal point per page
accents: one highlight hue against neutral context
annotation: forward; labels, callouts, and reference lines explain the point
leans toward: narrative and comparison shapes
technical:
for: analysts who want density and want to dig
ink: high information:ink; tables and small multiples dominate
accents: sparse; reserved for the one series under inspection
annotation: terse; precision over framing
leans toward: exploration and comparison shapesMap the chosen tone to the knobs the skill already controls:
accent count: restrained 1, editorial 1, corporate 2, technical 1-2
gridline weight: lighter for restrained/editorial; visible for technical tables
label density: sparse for restrained; forward for editorial; terse-dense for technical
accent saturation: low for restrained; mid for editorial/corporate; reserved for technicalSignature: the recurring element
Pick one signature (two at most). A signature is a single element repeated identically on every page so the report reads as one designed artifact. It is the thing that makes "use muted colors" a propagating decision instead of a one-page whim.
header band: title + filter context + refresh stamp, same place every page
single-accent rule: exactly one hue carries emphasis; everything else neutral grey
fixed nav rail: left rail or top rail; pick one convention and never alternate
kpi silhouette: one card shape, size, and label arrangement reused for every KPI
callout style: one annotation treatment (same font, weight, leader-line style)
target-line rule: targets always rendered the same way (e.g. dashed grey reference line)The signature is a contract: once chosen, every page honors it. A header band on three pages and absent on the fourth is a broken signature, and the design gate will flag it.
Serialize the identity into the theme
The identity belongs in the theme, not in per-visual overrides. A tone expressed as fifty inline hex values is not propagated; it is duplicated, and it will not survive a re-theme. Route execution through the `modifying-theme-json` skill:
- the single-accent rule becomes the theme's
dataColorsordering (accent first, neutrals after) so emphasis is one decision - gridline weight, label density, and accent saturation become theme wildcards (
visualStyles["*"]["*"]) or visual-type overrides, not per-visual edits - semantic sentiment colors (
good/bad/neutral) live in the theme so callouts and CF cite tokens, never literals
When a later choice needs a color or a formatting value, it cites the locked identity ("accent hue per the single-accent rule") and reads it from the theme. If a choice cannot cite the identity, the identity is incomplete; fix the identity, not the visual.
Route each page to a shape
Tone and signature set the look; page shapes set the structure. After locking identity, route every page to exactly one shape by the question it answers. See references/page-shapes.md. Shapes sit above the per-visual chart-selection guidance; they decide what belongs on a page before chart selection decides how each value is drawn.
Pitfalls
- Picking a tone word ("clean", "modern") that maps to no knob; if it does not change accent count, ink, or annotation, it is decoration, not a tone
- More than one signature competing for attention; one designed element repeated beats three half-applied ones
- Declaring an identity then choosing colors per visual anyway; the citation rule exists precisely to catch this
- Saturation creep: a "restrained" tone whose accents brighten page by page has lost the budget
Filter Pane Information Architecture
The decisions behind a filter pane are distinct from its color and chrome. Three core decisions per filter card: should it be locked, hidden, or visible? What should it be called? Where in the card order does it sit?
Lock vs Hide
These encode different intents and are not interchangeable:
- Lock: keeps a filter card visible but read-only. Use for scope guards the reader should know about ("Region = EMEA", "Status = Active"). The reader can see why numbers are filtered.
- Hide: removes the card entirely. Use for data-cleanup filters (exclude nulls, exclude test SKUs). These exist to prevent junk from appearing; the reader does not need to know about them.
Rule: lock to inform, hide to clean. Never hide a business filter the reader expects to see; they will not know why the numbers look filtered.
Renaming Filter Cards
The card title is editable independently of the field it filters; renaming does not rename the model field. Replace jargon field names like D_SHOP[Reporting Row Name] with a readable label like "Report Line". Renamed cards do not automatically rename matching slicers; rename the slicer header too.
Card Order
The pane always groups cards by scope: report-level, then page-level, then visual-level. Custom ordering is only possible within a scope level. If a page-level filter needs to be the reader's first reach, promote it to a slicer on the canvas.
Applied vs Available Card Styling
A filter card's Applied state (a filter is set) should be visually distinct from Available (no value set). This is the built-in restatement mechanism; readers scanning the pane can tell at a glance what is active. Set distinct styles in the theme via the $id discriminator (Available/Applied); see the modifying-theme-json skill for the filterCard theme pattern.
Report-Level Settings
These persist in the report definition and affect all readers:
- Search in filter pane: leave on when the pane has many cards; turn off on a curated 2-3 card pane
- Allow users to change filter types: off when a card is deliberately set to Top-N or relative-date (prevent readers from overriding it)
- Apply filters button: on for slow DirectQuery or large Import models where instant feedback creates multiple unnecessary queries; off for snappy Import models where immediate cross-filter is the better experience
Pitfalls
- Hiding a business filter removes the reader's ability to see why numbers look filtered; lock it instead
- Publish-to-web does not render the filter pane at all; move essential filters to on-canvas slicers
- Renaming a filter card does not propagate to slicers on the same field; update both
Layout Guidelines
Detailed specifications for Power BI report page layouts.
Page Dimensions
Standard Page (16:9)
Width: 1280px
Height: 720pxAlternative Sizes
| Type | Width | Height | Use Case |
|---|---|---|---|
| Standard | 1280 | 720 | Desktop (PBI default) |
| Full HD | 1920 | 1080 | High-resolution displays, presentations |
| Letter | 816 | 1056 | Print, portrait |
| 4:3 | 1280 | 960 | Legacy displays |
| Custom | Variable | Variable | Specific requirements |
Margins and Spacing
On a fixed PBIR canvas, white space is a finite budget spent explicitly through gaps between position rectangles. Two failure modes both come from treating gaps as residual: cramming (proximity collapses, groups stop reading) or scattering (page feels empty).
Spend at three tiers, all multiples of the grid unit:
intra-group: ~8-16px # within a group (a KPI row)
inter-group: ~24-32px # between groups; this gap creates visual proximity grouping
margin: ~24-32px # canvas edge to first/last visual, all four sidesThe inter-group gap must be strictly larger than the intra-group gap; that inequality is what turns spacing into hierarchy.
Reserve the margin first (usable area = canvas - 2*margin). Avoid double-spending a gap and a box border for the same separation; prefer the gap.
Padding (inside a visual container) and gaps (between visual containers) are different budgets controlled in different places; tightening one does not buy the other.
Page Margins
Top: 24-32px
Bottom: 24-32px
Left: 24-32px
Right: 24-32pxVisual Spacing
Intra-group (within a visual group): 8-16px
Inter-group (between logical groups): 24-32pxGrid System
Use 8px or 16px grid for consistent alignment:
Positions: 0, 16, 32, 48, 64, 80...
Sizes: 200, 300, 400, 500...Visual Zones
Zone Layout (Detail Gradient)
+------------------+------------------+
| ZONE 1 | ZONE 1 | y: 24 - 200
| KPIs / Cards | KPIs / Cards | (Important, summary)
+------------------+------------------+
| |
| ZONE 2 | y: 216 - 600
| Charts / Analysis | (Context, trends)
| |
+------------------+------------------+
| |
| ZONE 3 | y: 616 - 1056
| Tables / Details | (Drill-down, detail)
| |
+------------------+------------------+Zone Specifications
| Zone | Purpose | Height | Visual Types |
|---|---|---|---|
| 1 | Summary | 150-200px | Cards, KPIs, Slicers |
| 2 | Analysis | 350-450px | Charts, Maps, Gauges |
| 3 | Detail | 350-450px | Tables, Matrix, Lists |
Common Visual Sizes
Cards/KPIs
Width: 200-300px
Height: 100-150pxCharts
Small: Width: 400px, Height: 300px
Medium: Width: 600px, Height: 400px
Large: Width: 900px, Height: 500px
Full: Width: 1872px, Height: 500pxTables
Width: Variable (fill available space)
Height: 300-500pxSlicers
Horizontal: Width: 200-400px, Height: 60-80px
Vertical: Width: 150-200px, Height: 200-400pxTitle Area
Page Title Specifications
Position: x: 24, y: 24
Width: 400-600px
Height: 48-64px
Font: 24pt boldSubtitle (Optional)
Position: x: 24, y: 72
Width: 400-600px
Height: 32-48px
Font: 14pt regularSample Layouts
Dashboard Layout
+--------------------------------------------------+
| Title [Slicer] | y: 24
+--------+--------+--------+--------+--------------+
| KPI | KPI | KPI | KPI | | y: 96
+--------+--------+--------+--------+ +
| | |
| Line Chart | Bar Chart | y: 232
| | |
+-------------------------+------------------------+
| |
| Table | y: 616
| |
+--------------------------------------------------+Analysis Layout
+--------------------------------------------------+
| Title | y: 24
+-------------------------+------------------------+
| | Slicer | y: 96
| +------------------------+
| Main Chart | Supporting Chart 1 | y: 180
| +------------------------+
| | Supporting Chart 2 | y: 440
+-------------------------+------------------------+
| Detail Table or Additional Analysis | y: 700
+--------------------------------------------------+KPI Dashboard
+--------------------------------------------------+
| Title [Date Slicer] | y: 24
+--------+--------+--------+--------+--------------+
| Big | Big | Big | Big | | y: 96
| KPI | KPI | KPI | KPI | |
+--------+--------+--------+--------+--------------+
| |
| Trend Chart (Sparklines) | y: 280
| |
+--------------------------------------------------+
| |
| Comparison Table | y: 540
| |
+--------------------------------------------------+Positioning Rules
Alignment
1. Vertical alignment: Left edges of visuals in same column should align 2. Horizontal alignment: Top edges of visuals in same row should align 3. Consistent spacing: Equal gaps between all visuals -- this is critical
Symmetrical Spacing (Critical)
All gaps between visuals must be equal. Uneven spacing creates visual tension and signals misalignment, even when visuals are technically positioned correctly. This is one of the most common layout mistakes.
When calculating positions for a row of visuals:
1. Decide the page margin (e.g., 24px) and the gap between visuals (e.g., 16px) 2. Calculate available content width: page_width - (2 * margin) 3. Calculate total gap space: gap * (num_visuals - 1) 4. Distribute remaining width proportionally: (content_width - total_gaps) / num_visuals
Example for 4 equal visuals on a 1280px page with 24px margins and 16px gaps:
content_width = 1280 - 48 = 1232
total_gaps = 16 * 3 = 48
visual_width = (1232 - 48) / 4 = 296
x positions: 24, 336, 648, 960For visuals of different widths (e.g., a KPI + a chart sharing a row), the gap between them must still match the gap between other visual pairs on the page. Verify by checking: visual_B.x - (visual_A.x + visual_A.width) is the same for all adjacent pairs.
Anti-pattern: Visuals that are close but not quite aligned, or where the gap between the left pair is 16px but the gap between the right pair is 24px. This is visually jarring even at small differences (4-8px).
Vertical Column Alignment Across Rows (Critical)
When visuals in different rows share a vertical split (e.g., two charts side-by-side below two KPI pairs), the column boundaries must align vertically across rows. The gap between the left visual and right visual in row 2 must line up with the gap in row 1.
WRONG (misaligned vertical split):
+------ 648px ------+--16--+---- 584px ----+ Row 1
+------- 632px ------+--16--+--- 600px ----+ Row 2
^ ^
These don't align -- looks sloppy
RIGHT (aligned vertical split):
+------ 648px ------+--16--+---- 584px ----+ Row 1
+------ 648px ------+--16--+---- 584px ----+ Row 2
^
Same column edge in both rowsWhen calculating: if row 1 has visual A ending at x=648 and visual B starting at x=664 (16px gap), row 2 must use the same split: visual C ends at x=648, visual D starts at x=664. The widths of C and D will differ from A and B, but the gap position is identical.
This applies to any multi-row layout where visuals share implicit column boundaries. Even if the visuals are different types and sizes, the vertical gutters must form continuous lines from top to bottom of the page.
Z-Order
- Base visuals: z = 0-999
- Overlays/highlights: z = 1000-1999
- Tooltips/popups: z = 2000+
Performance Considerations
Opening a page refreshes every visible visual; each emits at least one DAX query. Parallelism is capped (DirectQuery default 10 concurrent connections), so latency grows non-linearly past that cap. Visual COUNT is a proxy; query COST per visual is the actual driver. 12 cheap card visuals are fine; 8 matrices with totals and measure filters may not be.
Visuals that emit more than one query per page load (multipliers):
- Tables/matrices with totals or subtotals (one query per band; DistinctCount and Median are worst)
- Measure filters (two queries)
- Top N filters (two queries; can hit the 1M-row intermediate limit in DirectQuery)
- Field parameters (an extra evaluated-parameters phase)
- Custom/Deneb/Python/R visuals (render phase dominates)
Exceptions
These do not emit queries and do not count toward visual density:
- Textboxes
- Images
- Shapes
- Buttons
Mobile Layout: Phone View as a Curated Subset
Power BI does not reflow a desktop page onto a phone. The portrait view is a hand-picked subset re-placed on a fixed narrow portrait grid. A visual appears in portrait only if it has a mobile.json alongside its visual.json. This inverts the desktop default: on the web every visual renders; on a phone nothing renders until explicitly opted in.
A page with no mobile-placed visuals falls back to rotated landscape, which is usually unreadable on a phone held in portrait.
Design Principles
Pick the few visuals that answer the headline question for a mobile reader: KPI cards, the one primary chart, the key slicer. Wide tables, scatter charts, and dense matrices rarely survive the narrow portrait grid. Stack vertically with the answer at the top. Lay slicers and navigation buttons horizontally so they consume one short band rather than stacking tall.
Build the phone layout alongside the desktop page so a page copy duplicates placements from the start rather than being retrofitted.
File Mechanics
The phone layout is stored per-visual, not per-page. Each visual folder may hold a mobile.json beside its visual.json (validated by the visualContainerMobileState schema). Required keys are $schema and position; the position block uses the same field names as desktop (x, y, z, height, width, tabOrder, angle) but in the phone-canvas coordinate space, which is independent of the desktop canvas.
"The page has a phone layout" is emergent: it is true if at least one visual has a mobile.json.
Inspect and set via the CLI:
pbir visuals mobile "Report.Report/Page.Page/Visual.Visual"When hand-writing a mobile.json, copy the $schema URL from another mobile.json in the same report; a stale schema version is the most common validation failure.
What to Include and Exclude
Rank visuals by their value for the mobile reader's primary question, then place the top 4-8 in a single column. Record which visuals you intentionally excluded. The over-faithful miniature anti-pattern (placing everything) produces a portrait page that scrolls forever and is harder to read than the rotated landscape fallback.
Strip non-essential chrome for the mobile copy via mobile-only formatting overrides in mobile.json: smaller axis titles, drop legends, remove gridlines. These overrides are deltas; list only properties that must differ from the desktop visual. The theme-first cascade still applies: a formatting change that should apply on all surfaces belongs in the theme, not in mobile.json.
Pitfalls
- A page that looks mobile-ready in the editor but has zero
mobile.jsonfiles silently falls back to rotated landscape - Mobile-optimized views render only in native iOS/Android apps; a browser (Service, Playwright, Chrome) always shows the landscape layout, so you cannot verify the portrait layout by browser screenshot
- Editing
visual.jsonposition does nothing to the phone layout and vice versa; they are independent coordinate records - Deleting a visual's folder must also drop its
mobile.json - Absence of
mobile.jsonis not automatically a defect; tie severity to intent (a headline KPI page with no mobile layout is high severity; a back-of-house detail page is usually fine)
Page Shapes by Intent
Route every page to exactly one shape, chosen by the question the page answers, not by a generic dashboard mold. A page is a unit of intent: it exists to answer one question for one reader. Naming the question first decides what belongs, what stays off, and how the page is laid out.
Shapes sit above the per-visual guidance in references/chart-selection.md: the shape decides what goes on the page, then chart selection decides how each value is drawn. Each shape reuses the detail-gradient and spacing-tier vocabulary in references/layout-guidelines.md rather than inventing new layout rules.
Rule: one shape per page. A page trying to answer two questions is two pages. If a page reads as both a summary and an exploration surface, split it.
The five shapes
summary
question: where do we stand right now?
belongs:
- a few framed KPIs top-left, each with a target and a trend
- one supporting chart for context
keep off:
- raw detail tables; drill belongs on its own page or a drillthrough
- slicer walls; a summary states the position, it does not invite slicing
layout lean: high end of the detail gradient; KPI row in the top band, one chart belowmonitoring
question: what needs attention today?
belongs:
- status-first, exception-driven encoding (RAG / threshold)
- a triage table sorted by severity, not by name
- sparse cards for the few headline counts
keep off:
- full history; monitoring is about now and what breached
- decorative trend charts that bury the exceptions
layout lean: status signals top, triage table filling the lower band; sort by severity descendingexploration
question: let me slice this myself
belongs:
- slicers and filters forward, placed by the signature's nav convention
- a primary chart that responds to selection
- room to breathe; fewer pre-baked conclusions
keep off:
- asserted callouts; the reader draws the conclusion here, not the author
- a crowded KPI row competing with the interactive area
layout lean: filter rail per the signature, large responsive chart as the focal areacomparison
question: how does A differ from B?
belongs:
- paired or small-multiple layout with shared, synchronized axes
- variance made explicit (a delta column, a difference series)
- side-by-side placement, not stacked
keep off:
- independent scales per panel; that hides the difference being compared
- unrelated context that dilutes the A-vs-B focus
layout lean: equal-weight panels across one band; axes synchronized so length carries the comparisonnarrative
question: here is the finding, and why
belongs:
- one dominant focal visual
- annotation forward; callouts and reference lines explain the point
- subordinate supporting context around the focal visual
keep off:
- co-equal visuals fighting for the focal role; a narrative has one lead
- dense detail that competes with the story
layout lean: one large focal visual, smaller supporting visuals subordinate to it; editorial-tone-friendlyRouting and tone
Tone (see references/design-identity.md) and shape reinforce each other but are independent decisions. A restrained tone leans toward summary and monitoring; an editorial tone leans toward narrative and comparison; a technical tone leans toward exploration and comparison. The lean is a prior, not a rule: a technical report can still have a summary page. Assign the shape by the page's question first, then let tone set its look.
Pitfalls
- A page with no nameable question; if you cannot state the one question it answers, it has no shape and probably should not exist
- Two shapes on one page (a summary KPI band stapled to an exploration surface); split it
- Choosing a shape by what visuals are handy rather than by the reader's question; intent leads, inventory follows
- A monitoring page sorted alphabetically; severity ordering is the whole point of the shape
Page Titles
Guidelines for implementing page titles in Power BI reports.
Why Page Titles Matter
- Provide context for report consumers
- Improve navigation and orientation
- Support accessibility (screen readers)
- Professional appearance
Implementation Options
Option 1: Textbox Visual (Recommended)
{
"name": "title-guid",
"position": {
"x": 24,
"y": 24,
"z": 1000,
"width": 500,
"height": 48
},
"visual": {
"visualType": "textbox",
"objects": {
"general": [{
"properties": {
"paragraphs": {
"expr": {
"Literal": {
"Value": "[{\"textRuns\":[{\"value\":\"Sales Overview\",\"textStyle\":{\"fontSize\":\"24pt\",\"fontWeight\":\"bold\"}}]}]"
}
}
}
}
}]
},
"visualContainerObjects": {
"background": [{"properties": {"show": {"expr": {"Literal": {"Value": "false"}}}}}],
"border": [{"properties": {"show": {"expr": {"Literal": {"Value": "false"}}}}}],
"title": [{"properties": {"show": {"expr": {"Literal": {"Value": "false"}}}}}]
}
}
}Option 2: Shape with Text
Use a rectangle shape with text overlay for styled backgrounds.
Option 3: Card Visual
For dynamic titles that include measure values.
Title Specifications
Standard Title
Position: x: 24, y: 24
Size: width: 400-600px, height: 48-64px
Font: 24pt, bold
Color: Dark gray (#333) or theme foreground
Alignment: LeftWith Subtitle
Title: x: 24, y: 24, height: 40px, font: 24pt bold
Subtitle: x: 24, y: 64, height: 32px, font: 14pt regularFull-Width Title Bar
Position: x: 0, y: 0
Size: width: 1920px, height: 72px
Background: Theme color or gradientDynamic Titles
Include Current Filter Context
Title Text =
"Sales by Region - " &
SELECTEDVALUE('Date'[Year], "All Years")Include Last Refresh
Title Text =
"Sales Dashboard - Updated: " &
FORMAT(MAX('Refresh Log'[Timestamp]), "MMM DD, YYYY")Textbox Paragraph Structure
Textbox content uses a specific JSON structure:
{
"paragraphs": {
"expr": {
"Literal": {
"Value": "[{\"textRuns\":[{\"value\":\"Title Text\",\"textStyle\":{\"fontSize\":\"24pt\",\"fontWeight\":\"bold\",\"fontColor\":\"#333333\"}}]}]"
}
}
}
}Multiple Runs (Mixed Formatting)
[{
"textRuns": [
{"value": "Sales ", "textStyle": {"fontSize": "24pt"}},
{"value": "Overview", "textStyle": {"fontSize": "24pt", "fontWeight": "bold"}}
]
}]Multiple Paragraphs
[
{"textRuns": [{"value": "Main Title", "textStyle": {"fontSize": "24pt"}}]},
{"textRuns": [{"value": "Subtitle here", "textStyle": {"fontSize": "14pt"}}]}
]Creating Title Textboxes
Create a textbox visual.json file manually (see pbir-format skill in the pbip plugin for JSON structure) with position x=24, y=24, width=500, height=48. Set the paragraph content with the desired title text and font size (e.g., 24pt).
Theme Considerations
Disable Container Properties
For titles, typically disable:
- Background
- Border
- Title (visual title)
- Drop shadow
In Theme Wildcards
"visualStyles": {
"textbox": {
"*": {
"title": [{"show": false}],
"background": [{"show": false}],
"border": [{"show": false}],
"dropShadow": [{"show": false}]
}
}
}Accessible Titles
A screen reader speaks a visual's title and type before any alt text. This means an acronym or jargon title is unintelligible spoken aloud; the accessibility constraint is stronger than the visual-design constraint.
- Spell out the subject ("Current year sales vs prior year", not "CY Sales vs PY")
- Reserve abbreviations for axis labels inside the chart, not the title
- Do not encode the chart type in the title; the reader announces it already
- A dynamic measure-bound title is spoken on every filter change; keep it a plain readable phrase with no glyphs or unit-suffix soup
Scan titles across a page:
pbir visuals format "MyPage/*" -p title.textTitle visibility and alt text
A hidden title (title.show=false) makes the reader fall back to a worthless auto name ("chart 4"). If the title is hidden for layout reasons, supply descriptive alt text on the visual's general.altText property instead.
Decorative title textboxes (section headers, visual labels) should be removed from the tab order (set tabOrder to -1) so readers skip them rather than announcing them as navigation stops.
Best Practices
1. Consistent positioning - Same x, y across all pages 2. Consistent sizing - Same width, height, font size 3. Descriptive text - Clearly describe page purpose, spell out abbreviations 4. Avoid redundancy - Don't repeat report name if obvious 5. Consider mobile - Ensure readable on smaller screens; see mobile.md
The Design Gate
Final checks run before declaring a design done. This is a closing gate, not a planning-stage checklist: the planning-stage checks belong to the report-creation workflow and run before the build; the design gate runs after, against the finished artifact, and decides whether it ships.
Run all five checks. Report each finding in the same shape as the Evaluation Output in SKILL.md: issue, location, severity (critical / warning / suggestion), fix.
The honesty caveat
An agent cannot see the canvas. It can read positions, types, colors, and bindings from JSON, but it cannot judge whether the rendered page looks balanced or whether a color reads as muted in context. Pair this gate with the screenshot-review loop in the `pbir-cli` skill: render the pages, look, and let the visual check confirm what the JSON check inferred. A gate passed on JSON alone is provisional until the screenshots agree.
1. Identity propagated
The locked tone and signature actually appear on every page.
- the signature element is in the same place on every page (header band, nav rail, KPI silhouette per
references/design-identity.md); absent or moved on any page is a broken signature - accents obey the single-accent rule: one hue carries emphasis, everything else is neutral
- colors live in the theme, not in per-visual overrides; inline hex that should be a token is a finding (see the inline-hex entry in
references/anti-patterns.md) - tone is consistent: a "restrained" report has not crept brighter or denser page by page
2. Every page has one intent
Each page resolves to exactly one shape from references/page-shapes.md.
- every page answers a nameable question; a page with no statable question is a finding
- no page is trying to be two shapes; a summary band stapled to an exploration surface is split into two pages
3. Spacing and margins equal
Gaps and margins are uniform and on the grid. Reuse the vocabulary in references/layout-guidelines.md; do not re-derive it here.
- every gap between adjacent visuals is the same value; every edge margin is the same value
- positions sit on the grid unit
- vertical gutters are continuous across rows: the column splits in row two line up with row one
4. Callouts backed by evidence
Every annotation, highlighted point, and written conclusion is verifiable against the model.
- sample the data with
pbir model -qand confirm each asserted number, ranking, or trend actually holds - no callout states a figure the data does not support; an annotation the model contradicts is a critical finding
- a highlighted "top" or "worst" point is the actual extreme, not an assumed one
5. Accessibility
- contrast ratios met (text 4.5:1, large text 3:1) per
references/visual-colors.md - color is never the only signal; status and sentiment pair with an icon, shape, or label
- fonts are readable at the rendered page size
- alt text is present on data visuals
Output
issue: <what is wrong>
location: <page / visual>
severity: critical | warning | suggestion
fix: <command or pattern>Critical findings (identity not propagated, a page with two intents, a callout the data contradicts, a contrast failure) block "done". Warnings and suggestions are reported and can ship with the user's acknowledgement.
Tables and Matrices: Design Best Practices
Tables and matrices sit at the bottom of the detail gradient (3-30-300 rule) -- they provide drill-down detail for readers who need specifics beyond what KPIs and charts convey. "Easy to create" is not the same as "easy to read." A well-formatted table answers a specific question; a poorly formatted one is a wall of numbers that nobody uses.
The core principle: format tables to answer specific reader questions, not to display all available data. Before touching formatting, identify what the reader needs to know and why. Content selection and decision-making come first; formatting amplifies the signal.
Decision-Making First
Before creating a table or matrix, answer these questions:
1. What question does this table answer? (e.g., "Which products are behind target?") 2. Who reads it and what action do they take? (e.g., "Sales managers re-allocate resources") 3. What columns are essential to answer that question? Remove everything else 4. What should the reader see first? This determines sort order and emphasis
Tables are valid visualizations when readers need precise numerical values, comparisons across many dimensions, or specific row lookups. The challenge is that human perception excels at visual pattern recognition but struggles with large numerical grids requiring mental calculation. Formatting must offload cognitive work from memory to visual perception.
Table vs Matrix: When to Use Which
| Scenario | Visual Type | Why |
|---|---|---|
| Flat list of records, no grouping | tableEx | Simple rows, no hierarchy needed |
| Hierarchical categories (e.g., Region > Country > City) | matrix (pivotTable) | Rows expand/collapse, subtotals per level |
| Cross-tab / pivot (categories on both axes) | matrix | Row headers + column headers + values |
| Two or more categorical columns that form a natural hierarchy | matrix | Avoids repeating parent values in every row |
Rule of thumb: If the table has 2+ categorical columns where one is a parent of the other (Key Account > Account > Product), use a matrix. The expand/collapse behavior reduces visual clutter and lets readers drill into relevant sections without scrolling through thousands of flat rows. A flat table with repeating parent values is one of the most common anti-patterns.
Column Selection
Include only columns that serve the question. Every column competes for horizontal space and reader attention. If the reader's question is "which products are behind target?", the variance column matters most -- showing separate actual and target columns alongside it may be redundant.
- Leading columns: The primary dimension(s) the reader groups by (customer, product, date)
- Measure columns: The KPIs that matter for this page -- typically the same measures shown in the KPI cards above
- Avoid: Internal IDs, keys, redundant names, measures unrelated to the page question, or columns that can be derived from others already shown
Column Ordering
Order columns by importance, left to right:
1. Row labels / hierarchy (leftmost) -- what the reader scans first 2. Primary measure -- the metric that answers the page question (e.g., Order Lines) 3. Secondary measures -- supporting metrics (Net Orders, OTD %) 4. Variance / delta columns -- if applicable (vs 1YP, vs Budget)
Sorting
Always sort by the most important measure, descending. Alphabetical sorting rarely answers useful questions. The top rows should show the largest/most significant items -- often the variance or gap column rather than the absolute value. This aligns with how business users read tables: they care about top contributors or biggest deviations first.
Set the sort configuration in the visual.json query block by specifying the sort field and direction (Descending).
For time-based detail tables (e.g., daily breakdown), sort ascending by date instead.
Formatting
Philosophy: Subtract, Don't Add
The default Power BI table styling includes gridlines, banded rows, and borders that compete with the data for visual attention. The recommended approach is to remove visual noise and let whitespace do the separation work:
- Strip or minimize gridlines (horizontal only if any)
- Remove banded row shading (or use an extremely subtle tint, 2-3% opacity)
- Reduce border complexity
- Increase row padding to let whitespace separate rows naturally
This is counterintuitive -- many designers add formatting elements to "improve" tables. Better tables result from removing clutter so the data signal stands out.
Theme-First Approach
Most table formatting should come from the theme. Only override at the visual level for genuinely one-off cases.
Check what the theme already provides by inspecting the theme.json visualStyles for tableEx and pivotTable entries (grid, columnHeaders, values properties).
Key Formatting Properties
| Property | Recommended | Notes |
|---|---|---|
| Grid lines | Horizontal only, or none | Vertical lines add clutter; let column spacing separate. Horizontal lines aid row scanning when rows are dense |
| Banded rows | Off or extremely subtle (2-3% opacity) | Heavy banding competes with data; whitespace is better |
| Row padding | 6-10px | More breathing room than default. Let whitespace, not gridlines, separate rows |
| Header font | Segoe UI Semibold, 10-12pt | Distinguishable from values but not dominant |
| Value font | Segoe UI, 10-12pt | Consistent across all value columns |
| Column width | Auto or proportional | Avoid truncation; let measures be narrower than text columns |
| Borders | Minimal or none | Let content structure speak for itself |
Number Formatting in Tables
Unlike KPI cards, tables should show more precision -- this is where readers go for detail:
- Measures: Use the model's format string (e.g.,
#,##0for integers,#,##0.0%for percentages) - Do NOT apply display units (thousands/millions) in tables -- show full values
- Align numbers right, text left (Power BI default)
Conditional Formatting
Conditional formatting is the primary tool for offloading cognitive work from the reader's memory to visual perception. But it must be applied strategically -- formatting on every column creates visual overload where nothing stands out.
Data Bars
Apply data bars to the primary measure column (orders, revenue, volume). Data bars let readers compare magnitudes at a glance without reading numbers. They transform a column of numbers into a scannable visual pattern.
Configure data bars in the visual.json objects by setting the dataBar property on the relevant measure column.
Color Scales on Variance Columns
Apply color scales to variance/delta columns only -- not to absolute values. Use an intuitive diverging scheme:
- Red/warm tones for negative/underperformance
- Blue/cool tones for positive/overperformance (avoid green for accessibility)
Create an extension measure for color (e.g., in reportExtensions.json):
OTD Color = IF([OTD % (Lines)] >= 0.9, "good", IF([OTD % (Lines)] >= 0.8, "neutral", "bad"))Then bind it as a conditional formatting rule for values.fontColor in the visual.json objects.
Directional Indicators
Triangle or arrow symbols with color coding can indicate direction (up/down) alongside magnitude. These are especially effective on variance columns where the direction of change matters as much as the size.
What to Format and What Not To
| Column Type | Formatting | Rationale |
|---|---|---|
| Primary measure | Data bars | Enables magnitude comparison without reading numbers |
| Variance / delta | Color scale or font color | Instantly signals good/bad performance |
| Status indicators (OTD %, quality) | Color when above/below threshold | Only when the threshold matters for decisions |
| Dimension columns | None | Text labels need no emphasis |
| Secondary measures | None | Formatting everything means formatting nothing |
Sparklines and Inline Trends
Sparklines add temporal context that answers "is this improving or declining?" -- information that a single number cannot convey. They distinguish between a product that is currently behind target but improving vs. one that is declining.
Add a native Power BI sparkline by binding a measure to the Values role with a sparkline date field in the visual.json query block.
For richer inline visuals (dumbbell charts, bullet charts, progress bars), use SVG extension measures via the svg-visuals skill (custom-visuals plugin). The trade-off: higher development and maintenance overhead vs. richer context. Use only when benefits justify added complexity.
Matrix-Specific Guidance
Row Hierarchy
Bind categories in order from broadest to most granular:
Create a matrix visual.json file manually (see pbir-format skill in the pbip plugin for JSON structure) with title "Detail" and field bindings: Rows: Customers.Key Account Name, Rows: Customers.Account Name, Rows: Products.Product Name, Values: Orders.Order Lines, Values: Orders.Net Orders.
Subtotals
Matrix visuals show subtotals at each hierarchy level by default. This is usually desirable -- it answers "how much for this Key Account across all products?" However, for very deep hierarchies (4+ levels), consider hiding intermediate subtotals to save space.
Expand/Collapse
By default, matrices start collapsed to the top level. This is the preferred behavior -- it respects the detail gradient principle. Readers expand only the rows they care about.
Column Hierarchy (Pivot)
Use column headers for time periods or categorical pivots:
Add a column hierarchy binding for Columns: Date.Calendar Quarter (ie Q1) in the visual.json query block.
Sizing
- Minimum height: 180-200px (enough for header + 5-8 visible rows)
- Full width: Tables/matrices typically span the full page width (margin to margin)
- Pagination: Power BI handles pagination automatically; ensure enough height for meaningful data density
Auto-Size Width
Turn off auto-size width when the table/matrix is placed in a constrained container (i.e., not full page width). Auto-size width calculates column widths based on content, which can exceed the visual container width and produce a horizontal scrollbar. Horizontal scrollbars are a bad practice -- they hide columns, break scannability, and signal that the visual doesn't fit its layout.
When auto-size is off, columns distribute proportionally within the visual's width. This may truncate long text values, but truncation with a tooltip is better than a scrollbar that hides entire columns off-screen.
Set columnHeaders.autoSizeColumnWidth to false in the visual.json objects:
columnHeaders.autoSizeColumnWidth = false -> columns fit container proportionally
columnWidth.value = <pixels> -> fixed width (only when autoSize is off)Rule of thumb: If the table is full-width (margin to margin), auto-size is usually fine. If the table shares a row with another visual (e.g., bar chart left, matrix right), disable auto-size width.
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Skipping the decision-making phase | Table shows data without answering a question | Define the question, audience, and action before building |
| Flat table with repeating parent values | Redundant data, hard to scan | Use matrix with hierarchy |
| Too many columns (>8) | Horizontal scroll, cognitive overload | Remove non-essential columns; disable auto-size width if constrained |
| Alphabetical sort | Rarely answers useful questions | Sort by primary measure or variance descending |
| Conditional formatting on every column | Visual overload, nothing stands out | Apply data bars to primary measure, color to variance only |
| Heavy gridlines + banded rows | Visual noise competes with data | Remove gridlines, use whitespace to separate rows |
| Display units in tables | Loses the detail readers came for | Show full precision |
| Same title as page title | Redundant information | Use differentiating title (e.g., "by Account and Product") |
| Unformatted data dump | Creates unused reports; nobody scans raw number walls | Apply the full formatting workflow |
| Showing actual + target + variance | Redundant when variance alone answers the question | Show variance; remove actual/target if not needed |
Checklist
- [ ] Question defined: what does this table answer and for whom?
- [ ] Visual type matches data structure (table for flat, matrix for hierarchical)
- [ ] Only essential columns included (no redundant or derived columns)
- [ ] Sorted by most important measure or variance descending
- [ ] Column order: dimensions left, primary measure next, variance right
- [ ] Visual noise removed: minimal gridlines, no heavy banding, adequate whitespace
- [ ] Data bars on primary measure column for magnitude scanning
- [ ] Color scales on variance columns only (not on every column)
- [ ] Number formatting shows appropriate detail (no display units)
- [ ] Sparklines added where temporal context matters
- [ ] Subtitle hidden
- [ ] Title differentiates from page title
- [ ] Auto-size width disabled if visual shares row with other visuals (prevents horizontal scrollbar)
- [ ] Height sufficient for 5-8 visible rows minimum
Tooltips, Annotations, and Guided Analytics
Tooltip Page Design
A report-page tooltip is a designed mini-canvas, not a bigger hover list. Three legitimate intents:
- Different perspective: same data pivoted (hover a monthly bar, see the full-year trend)
- Add detail: same grain, attributes the source visual omitted
- Add help: static explainer wired to the visual header icon, not data points
When not to build one
- One or two extra numbers: use the Tooltip field well, not a report page
- Anything the user must interact with: use drillthrough, not a tooltip
Design rules
Keep the page small and ActualSize (320x240 default; 240x180 for a single chart); design at final pixels since ActualSize does not scale. Strip chrome harder than a normal page:
pbir visuals format "TooltipPage/*" background --show falseTitles off, one focal visual plus at most a card or two. One tooltip page can serve many source visuals; reuse rather than clone.
Pitfalls
- Tooltip pages count toward page/visual budgets even though hidden; consolidate near-duplicates
- A tooltip that restates the data point already visible adds a query per hover for zero insight; delete it
- Do not reach for a tooltip when the need is comparison or navigation; those want drillthrough or a page link
- Never place a page-level
filterConfigon a tooltip page; the hover is the filter (seepbir-clireferences for filter-compatibility grain checks) - "Tooltip size affected by canvas size" autoscale is report-level, not settable per page from PBIR; design for actual pixels and flag for the owner
---
Annotations and Guided Analytics
Annotation-as-design is the chrome that carries the analytical argument. This describes how to compose existing PBIR primitives into a storytelling layer.
Building blocks
Shaded "what happened here" band: a reference line with shade* properties spanning a date range behind the hero series:
pbir reference-line add "Page/Hero.Visual" --type constant --value "2024-01-01" --shade-to "2024-03-31"
# reference-line adds are not idempotent; capture the returned idTarget line stating the finding: a reference line bound to a target measure with dataLabel on, label driven by the measure ("Plan: 11.0M").
Highlight-and-grey: hero series in a brand data color, others grey via per-series selectors (series(col=val)) or a measure-driven CF that returns grey for non-hero categories. The measure-driven approach is more robust; per-series selectors break if the series set changes.
Callout with leader: a chrome-off textbox (title/background/border/shadow all show=false) plus a thin shape as the leader line. Keep callouts as separate visuals so they are independently cp-able and themeable.
Reveal pacing: prefer a page sequence plus a page navigator over bookmarks for step-by-step story reveals. Bookmarks are fragile and capped; reserve them for in-page state toggles.
Reading order / scent: set tabOrder so traversal is headline -> hero visual -> callouts -> detail panel.
Review checklist: flag weak narrative
These are not caught by pbir bpa:
- Headline textbox is a chart-type label ("Bar Chart: Revenue") rather than a finding
- Every series at full saturation with no grey context series
- A reference or target line exists but
dataLabelis off - Callouts present but
tabOrderdoes not follow the story - Story carried entirely by a Copilot Narrative visual with no deterministic fallback
Pitfalls
- Reference-line and error-bar adds are not idempotent; always capture the returned id and use it for subsequent edits
- A wall of callouts harms accessibility; decorative chrome is budget-exempt but a screen reader still walks the
tabOrdersequence
Visual Colors
Guidelines for effective color usage in Power BI reports.
Color Principles
Use Theme Colors
Prefer theme colors over hex codes:
// Good - uses theme color
"expr": {"ThemeDataColor": {"ColorId": 1, "Percent": 0}}
// Avoid in visuals - use only in extension measures
"expr": {"Literal": {"Value": "'#118DFF'"}}Semantic Colors
Use these theme color names in extension measures:
| Color Name | Meaning | Typical Color |
|---|---|---|
"good" | Positive, on-target | Green |
"bad" | Negative, off-target | Red |
"neutral" | Unchanged, baseline | Gray/Yellow |
"minColor" | Gradient minimum | Red/Orange |
"midColor" | Gradient midpoint | Yellow/White |
"maxColor" | Gradient maximum | Green/Blue |
Extension Measure Pattern
// Return theme color names, not hex codes
Color Measure =
IF([Value] >= [Target], "good",
IF([Value] >= [Target] * 0.9, "neutral", "bad"))Color Contrast
WCAG 2.1 Requirements
| Element | Minimum Ratio |
|---|---|
| Normal text | 4.5:1 |
| Large text (18pt+) | 3:1 |
| UI components | 3:1 |
Common Contrast Issues
| Background | Text | Ratio | Status |
|---|---|---|---|
| White (#FFF) | Dark gray (#333) | 12.6:1 | Pass |
| White (#FFF) | Medium gray (#777) | 4.5:1 | Pass (barely) |
| White (#FFF) | Light gray (#AAA) | 2.9:1 | Fail |
| Light blue (#E3F2FD) | Blue (#1976D2) | 4.8:1 | Pass |
Color Categories
Data Colors (dataColors)
Primary series colors in theme:
"dataColors": [
"#118DFF", // Blue (primary)
"#12239E", // Dark blue
"#E66C37", // Orange
"#6B007B", // Purple
"#E044A7", // Pink
"#744EC2" // Violet
]Background Colors
Use muted, light colors:
- White:
#FFFFFF - Light gray:
#F5F5F5,#FAFAFA - Light blue:
#F0F8FF,#E3F2FD
Accent Colors
For highlights and emphasis:
- Use sparingly
- Reserve bright colors for important data
- Don't use red/orange unless indicating problems
Choosing the CF Basis
Pick the basis before touching pbir visuals cf; picking wrong produces valid JSON that misleads readers.
Each basis encodes a claim about the data:
- Gradient: the measure is continuous and comparable across rows ("more is darker"). Valid only for a magnitude on a single scale. A signed variance with no explicit center miscolors the midpoint and re-stretches on every refresh.
- Rules: discrete business-defined bands (RAG, SLA met/missed) whose cut points come from policy, not the data's min/max. These survive refresh without shifting.
- Field value (measure-driven): the color or icon is itself data that a measure computed. Most flexible; prefer it for anything non-trivial. Rule: if the logic has more than two thresholds or depends on another measure, make it measure-driven rather than an inline rules array.
- Icons: a status faster to scan than a number. Use sparingly on a triage/status column; never on the primary value column.
Apply CF to the secondary signal (variance, gap, status), not the headline value. Data bars on the one primary magnitude, color scale on the variance column; never both on the same column.
Prefer theme tokens over hex in every basis (pbir visuals cf ... --theme-colors to convert existing hex assignments).
CF Pitfalls
- A signed-measure gradient with no center colors zero as mid-gray noise; set the center explicitly to 0 or use rules
IconOnlyhides the number; only use it where the number itself is irrelevant- Overlapping or gappy rule bounds silently leave rows uncolored; promote fiddly logic to a measure you can test with
pbir model -q
Conditional Formatting Colors
Best Practices
1. Theme tokens over hex -- use --theme-colors to convert existing hex CF assignments to tokens; changing the theme then cascades everywhere 2. Measure-driven conversion -- use --to-measure to promote built-in gradient/rules CF to a measure expression; logic becomes testable and versionable 3. Sparingly applied -- CF should highlight exceptions; formatting every column means nothing stands out 4. Accessible -- use blue/orange instead of red/green; always pair color with a secondary cue (icon, text, shape) 5. Theme-first -- check that good, bad, and neutral sentiment colors exist in the theme before applying CF; add them if missing (e.g., good="#00B050", bad="#FF0000", neutral="#FFC000")
Positive/Negative Pattern
// In extension measure (preferred)
"expression": "IF([Value] >= 0, \"good\", \"bad\")"Theme defines actual colors:
"good": "#00B050", // Green
"bad": "#FF0000", // Red
"neutral": "#FFC000" // Yellow/OrangeGradient Pattern
For continuous scales, use theme tokens not hex:
minColor -> bad end of scale (e.g., "minColor" or "bad")
midColor -> neutral midpoint (e.g., "midColor" or "neutral")
maxColor -> good end of scale (e.g., "maxColor" or "good")Traffic Light Pattern
| Range | Color Name | Meaning |
|---|---|---|
| < 50% | "bad" | Critical |
| 50-80% | "neutral" | Warning |
| > 80% | "good" | On track |
Data Bars
Data bars provide magnitude scanning in tables/matrices. Apply to primary measure columns. Use muted colors that don't overwhelm the text values.
Color Don'ts
Avoid
1. Too many colors - Maximum 6-8 distinct colors per visual 2. Pure black - Use dark gray (#333) instead 3. Neon/bright colors - Cause eye strain 4. Red for positive - Confuses users 5. Color-only meaning - Always pair with text/icons
Never Use
- Rainbow gradients
- Clashing color combinations
- Low contrast combinations
- Brand colors on data points (unless intentional)
Accessibility Tips
Color Blindness
Test with color blindness simulators:
- Protanopia (red-blind): ~1% of males
- Deuteranopia (green-blind): ~1% of males
- Tritanopia (blue-blind): rare
Safe combinations:
- Blue + Orange (instead of Red + Green)
- Blue + Yellow
- Dark + Light variants of same hue
Alternative Indicators
Pair colors with:
- Icons (up/down arrows)
- Patterns (solid/hatched)
- Text labels
- Shapes (markers)