
R Visuals
- 33 installs
- 836 repo stars
- Updated July 29, 2026
- data-goblin/power-bi-agentic-development
Create R visuals in Power BI PBIR reports using ggplot2 patterns, injecting R scripts into the report.
About
Guidance for creating R visuals in PBIR reports with ggplot2 patterns. A developer uses it to add an R-scripted chart to a Power BI report via the pbir CLI or direct JSON editing.
- Creates R visuals with ggplot2 in PBIR reports
- Applies via pbir CLI or direct PBIR JSON edits
R Visuals by the numbers
- 33 all-time installs (skills.sh)
- Ranked #1,088 of 2,064 Data Science & ML 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 r-visualsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33 |
|---|---|
| repo stars | ★ 836 |
| Last updated | July 29, 2026 |
| Repository | data-goblin/power-bi-agentic-development ↗ |
What it does
Create R visuals in Power BI PBIR reports using ggplot2 patterns, injecting R scripts into the report.
Files
R Visuals in Power BI (PBIR)
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.
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.
Visual Identity
- visualType:
scriptVisual - Data role:
Values(columns and measures, multiple allowed) - Data variable:
dataset(data.frame, auto-injected) - Row limit: 150,000 rows
- Output: Static PNG at 72 DPI -- no interactivity
Workflow: Creating an R Visual
Step 1: Add the Visual
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.
Step 2: Write the Script
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 ggplot2Critical rules:
print(p)is mandatory for ggplot2 objects -- they do not auto-display in Power BIdatasetis auto-injected as a data.frame; do not create it- Access columns by index (
dataset[,1]) to avoid name escaping issues - Use backticks for column names with spaces: `
dataset$Order Lines`
Step 2b: Review
Before presenting the script to the user, dispatch the r-reviewer agent to validate correctness and provide design feedback.
Step 3: Inject the Script
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:
- Newlines in the script become
\nin the JSON string - Double quotes inside the script (e.g.,
"#5B8DBE") become\"in the JSON string - The entire script is wrapped in single quotes:
'library(ggplot2)\n...\nprint(p)' - See
examples/visual/for a complete real-world visual.json showing this encoding
Step 4: Validate
Validate JSON syntax with jq empty <visual.json> and inspect the visual.json to confirm script content and field bindings.
PBIR Format
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'.
Supported Packages
Power BI Service (R 4.3.3)
| 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
Desktop
Any locally installed R package works without restriction. R must be installed separately.
Best Practices
1. Always call `print(p)` -- ggplot2 objects require explicit printing 2. Guard against empty data -- if (nrow(dataset) == 0) { plot.new(); text(0.5, 0.5, "No data") } 3. Use index-based column access -- dataset[,1] avoids name escaping issues 4. Use `theme_minimal()` -- clean aesthetic that works well with Power BI 5. Factor categorical variables -- control sort order explicitly with factor() 6. Use hex colors matching the report theme 7. Set margins -- plot.margin=margin(t, r, b, l) to prevent clipping 8. Keep scripts concise -- 5-min timeout Desktop, 1-min Service
Limitations
| 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 |
Script Structure Template
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)
}R vs Python Syntax Reference
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 |
When to Use a Script Visual
Reach for an R visual only when all of the following hold:
- The chart has no native equivalent and no reasonable Deneb spec
- The value is in a statistical computation that must run at render time (model fit, kernel density, forecast band), not just a shape Vega could draw
- The visual does not need to be a cross-filter source, hover tooltips, publish-to-web, or app-owns-data embed
- The report is served in a Pro/PPU or higher capacity with a Fabric-enabled region
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
- `references/data-model.md` --
datasetgrouping 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 escaping
- `examples/visual/bullet-chart.json` -- PBIR visual.json: bullet chart with conditional coloring, error handling, and extensive escaping
- `examples/visual/bar-chart.json` -- PBIR visual.json: horizontal bar with PY comparison lines and colored account labels
- `examples/visual/trend-line.json` -- PBIR visual.json: area chart with ribbon plot and month factor handling
Fetching Docs
To 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.
Related Skills
- `pbi-report-design` -- Layout and design best practices
- `python-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
library(ggplot2)
# dataset is auto-injected as a data.frame
if (nrow(dataset) == 0) {
plot.new()
text(0.5, 0.5, "No data available", cex=1.5)
} else {
df <- data.frame(category=dataset[,1], value=dataset[,2])
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()
)
print(p)
}
library(ggplot2)
# dataset is auto-injected as a data.frame
if (nrow(dataset) == 0) {
plot.new()
text(0.5, 0.5, "No data available", cex=1.5)
} else {
p <- ggplot(dataset, aes(x=seq_len(nrow(dataset)), y=dataset[,1])) +
geom_ribbon(aes(ymin=0, ymax=dataset[,1]), fill="#5B8DBE", alpha=0.12) +
geom_line(color="#5B8DBE", size=1.2) +
geom_point(color="#5B8DBE", fill="white", size=3, shape=21, stroke=1.5) +
theme_minimal(base_size=10) +
theme(panel.grid.minor=element_blank())
print(p)
}
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.2.0/schema.json",
"name": "h8i9j0k1l2m3n4o5",
"position": {
"x": 20,
"y": 250,
"z": 2,
"height": 450,
"width": 720,
"tabOrder": 2
},
"visual": {
"visualType": "scriptVisual",
"visualContainerObjects": {
"title": [
{
"properties": {
"show": {
"expr": {
"Literal": {
"Value": "true"
}
}
},
"text": {
"expr": {
"Literal": {
"Value": "'Key Account vs. PY'"
}
}
}
}
}
]
},
"query": {
"queryState": {
"Values": {
"projections": [
{
"field": {
"Measure": {
"Expression": {
"SourceRef": {
"Entity": "Orders"
}
},
"Property": "Order Lines"
}
},
"queryRef": "Orders.Order Lines",
"nativeQueryRef": "Order Lines"
},
{
"field": {
"Measure": {
"Expression": {
"SourceRef": {
"Schema": "extension",
"Entity": "Orders"
}
},
"Property": "Order Lines (PY)"
}
},
"queryRef": "Orders.Order Lines (PY)",
"nativeQueryRef": "Order Lines (PY)"
},
{
"field": {
"Column": {
"Expression": {
"SourceRef": {
"Entity": "Customers"
}
},
"Property": "Key Account Name"
}
},
"queryRef": "Customers.Key Account Name",
"nativeQueryRef": "Key Account Name"
}
]
}
},
"sortDefinition": {
"sort": [
{
"field": {
"Measure": {
"Expression": {
"SourceRef": {
"Entity": "Orders"
}
},
"Property": "Order Lines"
}
},
"direction": "Descending"
}
],
"isDefaultSort": true
}
},
"objects": {
"script": [
{
"properties": {
"source": {
"expr": {
"Literal": {
"Value": "'library(ggplot2)\n\ndataset_top <- head(dataset[order(-dataset[,1]),], 8)\ndataset_top$order_lines <- dataset_top[,1]\ndataset_top$order_lines_py <- dataset_top[,2]\ndataset_top$account <- dataset_top[,3]\ndataset_top$account <- factor(dataset_top$account, levels=rev(dataset_top$account))\ndataset_top$pct_change <- (dataset_top$order_lines - dataset_top$order_lines_py) / dataset_top$order_lines_py\ndataset_top$arrow <- ifelse(dataset_top$pct_change > 0, \"\\u25B2\", \"\\u25BC\")\ndataset_top$label_color <- ifelse(dataset_top$pct_change > 0, \"#1971c2\", \"#c2255c\")\n\nformat_thousands <- function(val) {\n if (val >= 100000) {\n return(paste0(round(val/1000), \"K\"))\n } else if (val >= 10000) {\n return(paste0(sprintf(\"%.1f\", val/1000), \"K\"))\n } else {\n return(paste0(sprintf(\"%.2f\", val/1000), \"K\"))\n }\n}\n\ndataset_top$data_label <- paste0(sapply(dataset_top$order_lines, format_thousands), \" (\", dataset_top$arrow, sprintf(\"%.0f%%\", abs(dataset_top$pct_change)*100), \")\")\n\nmax_val <- max(dataset_top$order_lines, na.rm=TRUE)\nx_range <- c(-max_val * 0.35, max_val * 1.18)\n\np <- ggplot(dataset_top, aes(x=order_lines, y=account)) +\n geom_col(fill=\"#7FA9C7\", alpha=0.65, width=0.65) +\n geom_segment(aes(x=order_lines_py, xend=order_lines_py, y=as.numeric(account)-0.4, yend=as.numeric(account)+0.4), color=\"#6C757D\", size=1.5, lineend=\"round\") +\n geom_text(aes(label=data_label, color=label_color), hjust=-0.05, size=4, fontface=\"bold\", show.legend=FALSE) +\n scale_color_identity() +\n scale_x_continuous(labels=function(x) ifelse(x >= 0, paste0(round(x/1000),\"K\"), \"\"), limits=x_range, expand=c(0,0)) +\n scale_y_discrete() +\n coord_cartesian(clip=\"off\") +\n theme_minimal(base_size=12) +\n theme(\n panel.grid=element_blank(),\n axis.title.x=element_text(color=\"#495057\", size=11),\n axis.title.y=element_blank(),\n axis.text.x=element_text(color=\"#6C757D\", size=10),\n axis.text.y=element_blank(),\n axis.ticks.y=element_blank(),\n plot.margin=margin(t=5, r=5, b=5, l=5, unit=\"pt\")\n ) +\n labs(x=\"Order Lines\")\n\nfor(i in 1:nrow(dataset_top)) {\n y_pos <- nrow(dataset_top) - i + 1\n label_col <- dataset_top$label_color[i]\n p <- p + annotate(\"text\", x=-max_val * 0.02, y=y_pos, label=dataset_top$account[i], hjust=1, size=4.5, fontface=\"bold\", color=label_col, family=\"Segoe UI Semibold\")\n}\n\nprint(p)'"
}
}
},
"provider": {
"expr": {
"Literal": {
"Value": "'R'"
}
}
}
}
}
]
},
"drillFilterOtherVisuals": true
}
}
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.2.0/schema.json",
"name": "f6a7b8c9d0e1f2a3",
"position": {
"x": 15,
"y": 260,
"z": 5,
"height": 400,
"width": 617
},
"visual": {
"visualType": "scriptVisual",
"visualContainerObjects": {
"general": [
{
"properties": {
"altText": {
"expr": {
"Literal": {
"Value": "'Bullet chart showing Order Lines by Key Account compared to prior year'"
}
}
}
}
}
],
"title": [
{
"properties": {
"show": {
"expr": {
"Literal": {
"Value": "true"
}
}
},
"text": {
"expr": {
"Literal": {
"Value": "'Order Lines by Key Account'"
}
}
}
}
}
],
"subTitle": [
{
"properties": {
"show": {
"expr": {
"Literal": {
"Value": "false"
}
}
}
}
}
]
},
"query": {
"queryState": {
"Values": {
"projections": [
{
"field": {
"Column": {
"Expression": {
"SourceRef": {
"Entity": "Customers"
}
},
"Property": "Key Account Name"
}
},
"queryRef": "Customers.Key Account Name",
"nativeQueryRef": "Key Account Name"
},
{
"field": {
"Measure": {
"Expression": {
"SourceRef": {
"Entity": "Orders"
}
},
"Property": "Order Lines"
}
},
"queryRef": "Orders.Order Lines",
"nativeQueryRef": "Order Lines"
},
{
"field": {
"Measure": {
"Expression": {
"SourceRef": {
"Schema": "extension",
"Entity": "Orders"
}
},
"Property": "Order Lines (PY)"
}
},
"queryRef": "Orders.Order Lines (PY)",
"nativeQueryRef": "Order Lines (PY)"
},
{
"field": {
"Measure": {
"Expression": {
"SourceRef": {
"Schema": "extension",
"Entity": "Customers"
}
},
"Property": "Key Account Color"
}
},
"queryRef": "Customers.Key Account Color",
"nativeQueryRef": "Key Account Color"
},
{
"field": {
"Measure": {
"Expression": {
"SourceRef": {
"Schema": "extension",
"Entity": "Orders"
}
},
"Property": "Order Lines Comparison Color"
}
},
"queryRef": "Orders.Order Lines Comparison Color",
"nativeQueryRef": "Order Lines Comparison Color"
}
]
}
},
"sortDefinition": {
"sort": [
{
"field": {
"Measure": {
"Expression": {
"SourceRef": {
"Entity": "Orders"
}
},
"Property": "Order Lines"
}
},
"direction": "Descending"
}
],
"isDefaultSort": true
}
},
"objects": {
"script": [
{
"properties": {
"source": {
"expr": {
"Literal": {
"Value": "'library(ggplot2)\n\nif (nrow(dataset) == 0) {\n plot.new()\n text(0.5, 0.5, \"No data available\", cex=1.5)\n} else {\n format_k <- function(val) {\n k_val <- val / 1000\n if (k_val >= 100) {\n return(paste0(round(k_val), \"K\"))\n } else if (k_val >= 10) {\n return(paste0(sprintf(\"%.1f\", k_val), \"K\"))\n } else {\n return(paste0(sprintf(\"%.2f\", k_val), \"K\"))\n }\n }\n \n df <- data.frame(\n key_account = dataset[,1],\n order_lines = dataset[,2],\n order_lines_py = dataset[,3],\n account_color = dataset[,4],\n comparison_color = dataset[,5]\n )\n \n df <- df[order(df$order_lines, decreasing=FALSE), ]\n df$key_account <- factor(df$key_account, levels=df$key_account)\n df$label_formatted <- sapply(df$order_lines, format_k)\n \n theme_colors <- c(\"good\" = \"#1971c2\", \"bad\" = \"#c2255c\")\n df$comparison_color_hex <- ifelse(df$comparison_color %in% names(theme_colors), \n theme_colors[df$comparison_color], \n \"#212529\")\n df$bar_color_hex <- ifelse(df$comparison_color == \"bad\", \n \"#c2255c\", \n \"#CCCCCC\")\n \n p <- ggplot(df, aes(x=key_account, y=order_lines)) +\n geom_col(aes(fill=bar_color_hex), width=0.6) +\n scale_fill_identity() +\n geom_point(aes(y=order_lines_py), color=\"#495057\", size=9, shape=124) +\n geom_text(aes(label=label_formatted, color=comparison_color_hex), \n hjust=-0.2, size=5, fontface=\"bold\") +\n scale_color_identity() +\n coord_flip() +\n theme_minimal() +\n theme(\n axis.text.y = element_text(color=df$comparison_color_hex, size=10, face=\"bold\"),\n axis.text.x = element_text(size=9, color=\"#6C757D\"),\n axis.title = element_blank(),\n panel.grid.major.y = element_blank(),\n panel.grid.minor = element_blank(),\n plot.margin = margin(10, 40, 10, 10)\n ) +\n scale_y_continuous(expand = expansion(mult = c(0, 0.15)))\n \n print(p)\n}'"
}
}
},
"provider": {
"expr": {
"Literal": {
"Value": "'R'"
}
}
}
}
}
]
},
"drillFilterOtherVisuals": true
}
}
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.2.0/schema.json",
"name": "a1b2c3d4e5f6g7h8",
"position": {
"x": 300,
"y": 65,
"z": 1,
"height": 165,
"width": 600,
"tabOrder": 1
},
"visual": {
"visualType": "scriptVisual",
"visualContainerObjects": {
"title": [
{
"properties": {
"show": {
"expr": {
"Literal": {
"Value": "true"
}
}
},
"text": {
"expr": {
"Literal": {
"Value": "'Trend by month'"
}
}
}
}
}
],
"subTitle": [
{
"properties": {
"show": {
"expr": {
"Literal": {
"Value": "false"
}
}
}
}
}
]
},
"query": {
"queryState": {
"Values": {
"projections": [
{
"field": {
"Measure": {
"Expression": {
"SourceRef": {
"Entity": "Orders"
}
},
"Property": "Order Lines"
}
},
"queryRef": "Orders.Order Lines",
"nativeQueryRef": "Order Lines"
},
{
"field": {
"Column": {
"Expression": {
"SourceRef": {
"Entity": "Date"
}
},
"Property": "Calendar Month (ie Jan)"
}
},
"queryRef": "Date.Calendar Month (ie Jan)",
"nativeQueryRef": "Calendar Month (ie Jan)"
}
]
}
}
},
"objects": {
"script": [
{
"properties": {
"source": {
"expr": {
"Literal": {
"Value": "'library(ggplot2)\n\nmonth_order <- c(\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\")\n\ndataset$month_factor <- factor(dataset[,2], levels=month_order)\ndataset <- dataset[order(dataset$month_factor),]\ndataset$month_index <- as.numeric(dataset$month_factor)\n\np <- ggplot(dataset, aes(x=month_index, y=dataset[,1])) +\n geom_ribbon(aes(ymin=0, ymax=dataset[,1]), fill=\"#5B8DBE\", alpha=0.12) +\n geom_line(color=\"#5B8DBE\", size=1.2) +\n geom_point(color=\"#5B8DBE\", fill=\"white\", size=3, shape=21, stroke=1.5) +\n scale_x_continuous(breaks=1:12, labels=month_order, expand=c(0.02,0.02)) +\n scale_y_continuous(labels=function(x) paste0(round(x/1000),\"K\"), expand=c(0,0.08)) +\n theme_minimal(base_size=10) +\n theme(\n panel.grid.major.y=element_line(color=\"#ADB5BD\", size=0.3),\n panel.grid.minor=element_blank(),\n panel.grid.major.x=element_blank(),\n axis.title.x=element_blank(),\n axis.title.y=element_text(color=\"#495057\", size=9),\n axis.text=element_text(color=\"#6C757D\", size=8)\n ) +\n labs(y=\"Order Lines\")\n\nprint(p)'"
}
}
},
"provider": {
"expr": {
"Literal": {
"Value": "'R'"
}
}
}
}
}
]
},
"drillFilterOtherVisuals": true
}
}
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.7.0/schema.json",
"name": "9f8e7d6c5b4a3210",
"position": {
"x": 440,
"y": 80,
"z": 2,
"height": 635,
"width": 400,
"tabOrder": 2
},
"visual": {
"visualType": "scriptVisual",
"visualContainerObjects": {
"title": [
{
"properties": {
"show": {
"expr": {
"Literal": {
"Value": "true"
}
}
},
"text": {
"expr": {
"Literal": {
"Value": "'R (ggplot2)'"
}
}
}
}
}
],
"subTitle": [
{
"properties": {
"show": {
"expr": {
"Literal": {
"Value": "false"
}
}
}
}
}
]
},
"query": {
"queryState": {
"Values": {
"projections": [
{
"field": {
"Measure": {
"Expression": {
"SourceRef": {
"Entity": "Orders",
"Schema": "extension"
}
},
"Property": "Order Lines YTD"
}
},
"queryRef": "Orders.Order Lines YTD",
"nativeQueryRef": "Order Lines YTD"
},
{
"field": {
"Measure": {
"Expression": {
"SourceRef": {
"Entity": "Orders",
"Schema": "extension"
}
},
"Property": "Order Lines (PY) YTD"
}
},
"queryRef": "Orders.Order Lines (PY) YTD",
"nativeQueryRef": "Order Lines (PY) YTD"
},
{
"field": {
"Column": {
"Expression": {
"SourceRef": {
"Entity": "Date"
}
},
"Property": "Calendar Month (ie Jan)"
}
},
"queryRef": "Date.Calendar Month (ie Jan)",
"nativeQueryRef": "Calendar Month (ie Jan)"
}
]
}
}
},
"objects": {
"script": [
{
"properties": {
"source": {
"expr": {
"Literal": {
"Value": "'library(ggplot2)\nlibrary(dplyr)\n\n# Colors\ncolor_current <- \"#5B8DBE\"\ncolor_py <- \"#D4A574\"\n\n# Month order\nmonth_order <- c(\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\")\n\n# Prepare data\ndataset$month_factor <- factor(dataset[,3], levels=month_order)\ndataset <- dataset[order(dataset$month_factor),]\ndataset$month_index <- as.numeric(dataset$month_factor)\n\n# Reshape to long format for ggplot\ndata_long <- data.frame(\n month_index = rep(dataset$month_index, 2),\n month = rep(dataset[,3], 2),\n value = c(dataset[,1], dataset[,2]),\n series = rep(c(\"Order Lines YTD\", \"Order Lines (PY) YTD\"), each=nrow(dataset))\n)\n\n# Last index\nlast_idx <- nrow(dataset)\n\n# Last points only\ndata_last <- data_long %>%\n filter(month_index == max(month_index))\n\n# Format function\nformat_thousands <- function(val) {\n paste0(round(val/1000), \"K\")\n}\n\n# Max value for y-axis limit\nmax_val <- max(data_long$value, na.rm=TRUE)\n\n# Create plot\np <- ggplot(data_long, aes(x=month_index, y=value, color=series, linetype=series)) +\n geom_line(size=1.8) +\n geom_point(\n data=data_last,\n aes(x=month_index, y=value, color=series),\n size=5,\n shape=21,\n fill=\"white\",\n stroke=2.5\n ) +\n geom_text(\n data=data_last,\n aes(x=month_index + 0.4, y=value, label=format_thousands(value), color=series),\n hjust=0,\n size=6,\n fontface=\"bold\",\n show.legend=FALSE\n ) +\n scale_color_manual(\n values=c(\"Order Lines YTD\"=color_current, \"Order Lines (PY) YTD\"=color_py),\n name=\"\"\n ) +\n scale_linetype_manual(\n values=c(\"Order Lines YTD\"=\"solid\", \"Order Lines (PY) YTD\"=\"dashed\"),\n name=\"\"\n ) +\n scale_x_continuous(\n breaks=1:12,\n labels=month_order,\n limits=c(0.5, 15),\n expand=c(0, 0)\n ) +\n scale_y_continuous(\n labels=function(x) paste0(round(x/1000),\"K\"),\n limits=c(0, max_val * 1.15),\n expand=c(0, 0)\n ) +\n theme_minimal(base_size=16) +\n theme(\n panel.grid.major.y=element_line(color=\"#ADB5BD\", size=0.5),\n panel.grid.minor=element_blank(),\n panel.grid.major.x=element_blank(),\n axis.title.x=element_blank(),\n axis.title.y=element_text(color=\"#495057\", size=14),\n axis.text.x=element_text(angle=45, hjust=1, color=\"#6C757D\", size=13),\n axis.text.y=element_text(color=\"#6C757D\", size=13),\n legend.position=\"top\",\n legend.text=element_text(size=13),\n plot.margin=margin(10, 40, 10, 10, \"pt\")\n ) +\n labs(y=\"Order Lines YTD\")\n\nprint(p)'"
}
}
},
"provider": {
"expr": {
"Literal": {
"Value": "'R'"
}
}
}
}
}
]
},
"drillFilterOtherVisuals": true
}
}
R / ggplot2 Community Examples
ggplot2 is the preferred charting library for R visuals in Power BI. Use these gallery references when building R visual scripts.
R Graph Gallery (r-graph-gallery.com)
Comprehensive ggplot2 examples organized by chart type. Each page contains multiple examples with code.
Distribution
| Chart Type | URL |
|---|---|
| Violin | https://r-graph-gallery.com/violin.html |
| Density | https://r-graph-gallery.com/density-plot.html |
| Histogram | https://r-graph-gallery.com/histogram.html |
| Boxplot | https://r-graph-gallery.com/boxplot.html |
| Ridgeline | https://r-graph-gallery.com/ridgeline-plot.html |
| Beeswarm | https://r-graph-gallery.com/beeswarm.html |
Correlation
| Chart Type | URL |
|---|---|
| Scatter | https://r-graph-gallery.com/scatterplot.html |
| Heatmap | https://r-graph-gallery.com/heatmap.html |
| Correlogram | https://r-graph-gallery.com/correlogram.html |
| Bubble | https://r-graph-gallery.com/bubble-chart.html |
| Connected Scatter | https://r-graph-gallery.com/connected-scatterplot.html |
| 2D Density | https://r-graph-gallery.com/2d-density-chart.html |
Ranking
| Chart Type | URL |
|---|---|
| Barplot | https://r-graph-gallery.com/barplot.html |
| Spider / Radar | https://r-graph-gallery.com/spider-or-radar-chart.html |
| Wordcloud | https://r-graph-gallery.com/wordcloud.html |
| Parallel | https://r-graph-gallery.com/parallel-plot.html |
| Lollipop | https://r-graph-gallery.com/lollipop-plot.html |
| Circular Barplot | https://r-graph-gallery.com/circular-barplot.html |
Part of a Whole
| Chart Type | URL |
|---|---|
| Stacked Barplot | https://r-graph-gallery.com/stacked-barplot.html |
| Treemap | https://r-graph-gallery.com/treemap.html |
| Doughnut | https://r-graph-gallery.com/doughnut-plot.html |
| Pie Chart | https://r-graph-gallery.com/pie-plot.html |
| Dendrogram | https://r-graph-gallery.com/dendrogram.html |
| Circular Packing | https://r-graph-gallery.com/circle-packing.html |
| Waffle | https://r-graph-gallery.com/waffle.html |
Evolution
| Chart Type | URL |
|---|---|
| Line Plot | https://r-graph-gallery.com/line-plot.html |
| Area | https://r-graph-gallery.com/area-chart.html |
| Stacked Area | https://r-graph-gallery.com/stacked-area-graph.html |
| Streamchart | https://r-graph-gallery.com/streamgraph.html |
| Time Series | https://r-graph-gallery.com/time-series.html |
Flow
| Chart Type | URL |
|---|---|
| Chord Diagram | https://r-graph-gallery.com/chord-diagram.html |
| Network | https://r-graph-gallery.com/network.html |
| Sankey | https://r-graph-gallery.com/sankey-diagram.html |
| Edge Bundling | https://r-graph-gallery.com/hierarchical-edge-bundling.html |
Additional Resources
| Resource | URL |
|---|---|
| ggplot2 official docs | https://ggplot2.tidyverse.org/reference/ |
| ggplot2 book (Hadley Wickham) | https://ggplot2-book.org/ |
| R Graph Gallery (full site) | https://r-graph-gallery.com/ |
R Visual Data Model
Behavior of dataset and the row cap; traps that cause silent wrong input.
Distinct-row grouping
The 150,000-row cap applies to the deduplicated set, not the raw fact table. Power BI groups the dataset exactly like a table visual before handing it to the script: identical rows across all bound Values columns collapse to one. A script bound to Region, Category over a million-row fact table receives at most as many rows as there are distinct Region + Category combinations.
This is a property of the field bindings, not the script. Consequences:
- Per-observation charts (jitter, strip, raw scatter, ECDF) silently receive aggregated input if no unique key is bound
- Scripts expecting row-level variation (bootstrapping, individual observations) produce wrong output without error
- The cap is hit much faster when a unique key is included; pre-filter at the visual or page level first
To force per-row input, bind a guaranteed-unique column to the Values role alongside your other fields:
pbir visuals bind Page/StripPlot -t Column -d "Values:Sales.Region"
pbir visuals bind Page/StripPlot -t Column -d "Values:Sales.Amount"
pbir visuals bind Page/StripPlot -t Column -d "Values:Sales.TransactionKey"Do not use a measure as the unique key; a measure changes the projection kind and does not produce distinct-row expansion. If the model has no natural key, add an index column in Power Query or accept grouped input by design. Verify the effective row count with nrow(dataset) during development.
Two byte caps the skill header omits:
- Input is capped at 250 MB regardless of row count (Desktop and Service)
- Output is additionally capped at 2 MB on Desktop; a dense ggplot (many
ggrepellabels, finegeom_tile) can blow it silently; reduce mark density first - Any single string value over 32,766 chars is silently truncated
dataset arrives in arbitrary order; sort inside the script (reorder(), factor(levels=...)) and never rely on positional joins.
R-specific traps (no Python equivalent)
Time data type unsupported
A Time column (not Date/Time) errors the visual. Cast or model the field as Date/Time before binding; this is a model fix, not a script fix.
Text rendering in Service
R text-based output requires powerbi_rEnableShowText = 1 set as the first line of the script in Service. Desktop silently ignores it; Service silently drops text without it. This is the most common Desktop-passes / Service-fails trap for R visuals.
CJK fonts in Service
CJK characters render blank in Service unless the script sets powerbi_rEnableShowTextForCJKLanguages = 1 and loads showtext. Both flags must appear before any library() call:
powerbi_rEnableShowText = 1
powerbi_rEnableShowTextForCJKLanguages = 1
if (!requireNamespace("showtext", quietly = TRUE)) install.packages("showtext")
library(showtext)
showtext_auto()
library(ggplot2)showtext is on the approved CRAN list; the install.packages() call works in Service. Placing either flag after library() or inside a function makes it a no-op.
ggplot2 Chart Patterns for Power BI
Common ggplot2 patterns for R visuals. All scripts assume dataset is auto-injected as a data.frame. Use index-based access (dataset[,1]) to avoid name escaping issues.
Bar Chart (Vertical)
library(ggplot2)
df <- data.frame(category=dataset[,1], value=dataset[,2])
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()
)
print(p)Horizontal Bar Chart with Comparison Markers
library(ggplot2)
df <- head(dataset[order(-dataset[,1]),], 8)
df$category <- factor(dataset[seq_len(nrow(df)),2], levels=rev(dataset[seq_len(nrow(df)),2]))
p <- ggplot(df, aes(x=dataset[,1], y=category)) +
geom_col(fill="#7FA9C7", alpha=0.65, width=0.65) +
geom_segment(aes(x=dataset[,2], xend=dataset[,2],
y=as.numeric(category)-0.4, yend=as.numeric(category)+0.4),
color="#6C757D", size=1.5) +
geom_text(aes(label=sprintf("%.0f", dataset[,1])),
hjust=-0.05, size=4, fontface="bold") +
theme_minimal(base_size=12) +
theme(panel.grid=element_blank(), axis.title.y=element_blank())
print(p)Line Chart with Area Fill
library(ggplot2)
month_order <- c("Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec")
dataset$month_factor <- factor(dataset[,2], levels=month_order)
p <- ggplot(dataset, aes(x=as.numeric(month_factor), y=dataset[,1])) +
geom_ribbon(aes(ymin=0, ymax=dataset[,1]), fill="#5B8DBE", alpha=0.12) +
geom_line(color="#5B8DBE", size=1.2) +
geom_point(color="#5B8DBE", fill="white", size=3, shape=21, stroke=1.5) +
theme_minimal(base_size=10) +
theme(panel.grid.minor=element_blank())
print(p)Donut Chart
library(ggplot2)
color_palette <- c("#5B8DBE","#D4A574","#8BA888","#C97C7C","#9B87B8","#6B9B9E")
dataset$percentage <- dataset[,1] / sum(dataset[,1]) * 100
dataset$ymax <- cumsum(dataset$percentage)
dataset$ymin <- c(0, head(dataset$ymax, n=-1))
p <- ggplot(dataset, aes(ymax=ymax, ymin=ymin, xmax=4, xmin=2.5, fill=dataset[,2])) +
geom_rect(color="white", size=2.5) +
coord_polar(theta="y") +
xlim(c(1, 4.5)) +
scale_fill_manual(values=color_palette) +
theme_void() +
theme(legend.position="bottom")
print(p)Dual-Series YTD Comparison
library(ggplot2)
data_long <- data.frame(
month_index = rep(1:nrow(dataset), 2),
value = c(dataset[,1], dataset[,2]),
series = rep(c("Current YTD", "PY YTD"), each=nrow(dataset))
)
p <- ggplot(data_long, aes(x=month_index, y=value, color=series, linetype=series)) +
geom_line(size=1.8) +
scale_color_manual(values=c("Current YTD"="#5B8DBE", "PY YTD"="#D4A574")) +
scale_linetype_manual(values=c("Current YTD"="solid", "PY YTD"="dashed")) +
theme_minimal(base_size=14) +
theme(legend.position="top", legend.title=element_blank())
print(p)Bullet Chart with DAX-Driven Colors
library(ggplot2)
df <- data.frame(
key_account = dataset[,1],
value = dataset[,2],
target = dataset[,3],
bar_color = dataset[,4] # Color from DAX measure
)
p <- ggplot(df, aes(x=key_account, y=value)) +
geom_col(aes(fill=bar_color), width=0.6) +
scale_fill_identity() +
geom_point(aes(y=target), color="#495057", size=9, shape=124) +
coord_flip() +
theme_minimal(base_size=12) +
theme(panel.grid.major.y=element_blank(), axis.title=element_blank())
print(p)Correlation Heatmap
library(ggplot2)
# For numeric columns only
num_data <- dataset[sapply(dataset, is.numeric)]
cor_matrix <- cor(num_data, use="complete.obs")
melted <- reshape2::melt(cor_matrix) # reshape2 available on Service
p <- ggplot(melted, aes(x=Var1, y=Var2, fill=value)) +
geom_tile(color="white") +
geom_text(aes(label=sprintf("%.2f", value)), size=3) +
scale_fill_gradient2(low="#C97C7C", mid="white", high="#5B8DBE", midpoint=0) +
theme_minimal(base_size=10) +
theme(axis.text.x=element_text(angle=45, hjust=1), axis.title=element_blank())
print(p)Box Plot
library(ggplot2)
p <- ggplot(dataset, aes(x=dataset[,1], y=dataset[,2])) +
geom_boxplot(fill="#5B8DBE", alpha=0.5, outlier.color="#C97C7C") +
theme_minimal(base_size=12) +
theme(panel.grid.major.x=element_blank())
print(p)Faceted Small Multiples
library(ggplot2)
p <- ggplot(dataset, aes(x=dataset[,2], y=dataset[,3])) +
geom_line(color="#5B8DBE", size=1) +
facet_wrap(~dataset[,1], scales="free_y", ncol=3) +
theme_minimal(base_size=10) +
theme(strip.text=element_text(face="bold"))
print(p)Empty Data Guard Pattern
Wrap any script with this guard:
if (nrow(dataset) == 0) {
plot.new()
text(0.5, 0.5, "No data available", cex=1.5, col="#6C757D")
} else {
# ... chart code here ...
}