Report modification requires tooling. Two paths exist:
pbirCLI (preferred) -- use thepbircommand and thepbir-cliskill. Install withuv tool install pbir-cliorpip install pbir-cli. Check availability withpbir --version.- Direct JSON modification -- if
pbiris not available, use thepbir-formatskill (pbip plugin) for PBIR JSON structure and patterns. Validate every change withjq empty <file.json>.If neither the
pbir-cliskill nor thepbir-formatskill is loaded, ask the user to install the appropriate plugin before proceeding with report modifications.
R visuals execute R scripts (primarily ggplot2) to render static PNG images on the Power BI canvas. ggplot2 is the preferred library -- its grammar of graphics approach produces clean, publication-quality statistical visualizations with less code. R is particularly strong for statistical visualizations.
scriptVisual
Values (columns and measures, multiple allowed)dataset (data.frame, auto-injected)Create the visual.json file manually (see pbir-format skill in the pbip plugin for JSON structure) with visualType: scriptVisual, field bindings for the columns and measures you need (use Values:Table.Column or Values:Table.Measure format), and position/size as required.
library(ggplot2)
p <- ggplot(dataset, aes(x=Date, y=Sales)) +
geom_col(fill="#5B8DBE") +
theme_minimal(base_size=12) +
theme(panel.grid.major.x=element_blank())
print(p) # MANDATORY for ggplot2
Critical rules:
print(p) is mandatory for ggplot2 objects -- they do not auto-display in Power BIdataset is auto-injected as a data.frame; do not create itdataset[,1]) to avoid name escaping issuesdataset$`Order Lines`
Before presenting the script to the user, dispatch the r-reviewer agent to validate correctness and provide design feedback.
Set the script content in the visual's objects.script[0].properties.source literal value (see PBIR Format section below).
Escaping rules for visual.json injection:
The script must be encoded as a single-quoted DAX literal string inside expr.Literal.Value:
\n in the JSON string"#5B8DBE") become \" in the JSON string'library(ggplot2)\n...\nprint(p)'
examples/visual/ for a complete real-world visual.json showing this encodingValidate JSON syntax with jq empty <visual.json> and inspect the visual.json to confirm script content and field bindings.
Scripts are stored in visual.objects.script[0].properties:
{
"source": {"expr": {"Literal": {"Value": "'library(ggplot2)\\n...\\nprint(p)'"}}},
"provider": {"expr": {"Literal": {"Value": "'R'"}}}
}
Identical structure to Python visuals except visualType is scriptVisual and provider is 'R'.
| Package | Version | Purpose |
|---|---|---|
| ggplot2 | 3.5.1 | Grammar of graphics |
| dplyr | 1.1.4 | Data manipulation |
| tidyr | 1.3.1 | Data tidying |
| ggrepel | 0.9.5 | Non-overlapping labels |
| patchwork | 1.2.0 | Compose multiple plots |
| cowplot | 1.1.3 | Publication-quality plots |
| corrplot | 0.94 | Correlation matrices |
| viridis | 0.6.5 | Color scales |
| RColorBrewer | 1.1-3 | Color palettes |
| forecast | 8.23.0 | Time series forecasting |
| pheatmap | 1.0.12 | Heatmaps |
| treemap | 2.4-4 | Treemaps |
| lattice | 0.22-6 | Trellis graphics |
~1000 CRAN packages available. Not supported: packages requiring networking (RgoogleMaps, mailR).
Full package list: https://learn.microsoft.com/power-bi/connect-data/service-r-packages-support
Any locally installed R package works without restriction. R must be installed separately.
print(p) -- ggplot2 objects require explicit printingif (nrow(dataset) == 0) { plot.new(); text(0.5, 0.5, "No data") }
dataset[,1] avoids name escaping issuestheme_minimal() -- clean aesthetic that works well with Power BIfactor()
plot.margin=margin(t, r, b, l) to prevent clipping| Constraint | Desktop | Service |
|---|---|---|
| Output | Static PNG, 72 DPI | Static PNG, 72 DPI |
| Timeout | 5 minutes | 1 minute |
| Row limit | 150,000 | 150,000 |
| Output size | 2 MB | 30 MB |
| Networking | Unrestricted | Blocked |
| Gateway | Personal only | Personal only |
| Cross-filter FROM | Not supported | Not supported |
| Receive cross-filter | Yes | Yes |
| Publish to web | Not supported | Not supported |
| Embed (app-owns-data) | Not supported | Not supported |
library(ggplot2)
# 1. Guard against empty data
if (nrow(dataset) == 0) {
plot.new()
text(0.5, 0.5, "No data available", cex=1.5)
} else {
# 2. Data preparation (index-based access)
df <- data.frame(
category = dataset[,1],
value = dataset[,2]
)
# 3. Create visualization
p <- ggplot(df, aes(x=reorder(category, -value), y=value)) +
geom_col(fill="#5B8DBE", width=0.7) +
theme_minimal(base_size=12) +
theme(
panel.grid.major.x = element_blank(),
axis.title = element_blank()
)
# 4. Render
print(p)
}
For the language-choice decision, see the "When to Use a Script Visual" section above. This table covers only mechanical syntax differences for scripts already committed to R:
| Aspect | R (scriptVisual) |
Python (pythonVisual) |
|---|---|---|
| Render call | print(p) |
plt.show() |
| Column access | dataset[,1] or dataset$col |
dataset.iloc[:,0] or dataset["col"] |
| Empty guard | if (nrow(dataset) == 0) |
if len(dataset) == 0: |
| Factor/category order | factor(x, levels=...) |
pd.Categorical(x, categories=...) |
| Runtime (Service) | R 4.3.3 | Python 3.11 |
Reach for an R visual only when all of the following hold:
If interactivity or cross-filtering matters, use Deneb (a static PNG cannot be a selection source). If the need is a small inline mark (sparkline, bar, status pill), use an SVG measure (no row cap, no timeout, no licensing/region gate, renders under publish-to-web). The script visual's niche is narrow: compute-at-render statistical plots for internal or org consumption.
R vs Python once a script visual is the right call: use R for publication-quality statistical defaults and packages with no Python peer (forecast, corrplot, pheatmap, ridgeline/violin). Use Python when the computation leans on scikit-learn, statsmodels, or scipy, or when surrounding report logic is already Python. Where equal, default to whichever language the report's other scripts use; mixing doubles the publish-time package surface to validate.
Do not default to a script visual because a chart type "looks statistical." A box plot, lollipop, or dumbbell is an SVG-measure or Deneb job; reserve scripts for charts that genuinely compute.
references/data-model.md -- dataset grouping mechanic, row/byte caps, forcing per-row input, and R-specific traps (Time type, text rendering flags, CJK fonts)references/community-examples.md -- R Graph Gallery examples organized by chart type (distribution, correlation, ranking, evolution, flow)references/ggplot2-patterns.md -- Common ggplot2 chart patterns (bar, donut, line, heatmap, bullet)examples/script/ -- Standalone R scripts (bar-chart, trend-line) -- ready to inject into visual.json after escapingexamples/visual/bullet-chart.json -- PBIR visual.json: bullet chart with conditional coloring, error handling, and extensive escapingexamples/visual/bar-chart.json -- PBIR visual.json: horizontal bar with PY comparison lines and colored account labelsexamples/visual/trend-line.json -- PBIR visual.json: area chart with ribbon plot and month factor handlingTo retrieve current R visual / package support docs, use microsoft_docs_search + microsoft_docs_fetch (MCP) if available, otherwise mslearn search + mslearn fetch (CLI). Search based on the user's request and run multiple searches as needed to ensure sufficient context before proceeding.
pbi-report-design -- Layout and design best practicespython-visuals -- Python Script visuals (same concept, different language)deneb-visuals -- Vega/Vega-Lite visuals (interactive, vector-based alternative)svg-visuals -- SVG via DAX measures (lightweight inline graphics)pbir-format (pbip plugin) -- PBIR JSON format reference