ReadMe
A long standing pain point of mine has been documentation in PowerBI.
When I say this, I don't mean that I hate writing it, I hate that I don't have the ability to convey information in an easy to understand format with basic styling tools that are widely availible in other applications. I'm talking actual rich-text styling, tables, code blocks, linkable indexes, detail dropdowns, embedded images, all with modern styling! Nothing exists on the market that would let me do this... so I made it myself.
The Vision
With the introduction of .pbip files, Power BI now offers source control for reporting.
So why not extend that same source control approach to documentation in a format developers are familiar with?
Write it in markdown, store it as the README.md
in the same repository as the report, set up a connection with your report, and the visual will render the new data source without any additional tinkering.
Documentation evolves with the codebase, teams can update documentation without even opening the report, and everything stays in sync.
Want to understand how this is even possible? A step-by-step connection guide can be found here.
Dependencies
Per Microsoft's documentation, it's a shortlist of dependencies to get started. However, by far the most surprising part... the project files are all typescript. I was effectively building a web component for PowerBI, so having a bit of experience in web dev helped a lot.
I only needed:
- Node.js ≥ 18
- Download LTS from https://nodejs.org
- Power BI Visual Tools
npm install -g powerbi-visuals-tools
- Then I just needed to create an initial scaffold/template:
pbiviz new projectname cd projectname
This generated what would be my main files:
src/visual.ts
– main visual logic and rendering pipelinecapabilities.json
– data roles and formatting pane configurationpbiviz.json
/package.json
– project metadata and dependencies
Development Environment Woes
Sometimes getting a dev environment set up is half the battle, and this experience was no exception.
I could run pbiviz start
or npm run start
to start up my local development server... and then nothing happened. Nothing was visible via localhost in a browser. Microsoft's documentation states you can only view your visual inside of PowerBI.
Unfortunately I followed the default recommendation, which proved to be a frustrating time!
Microsoft recently deprecated the ability to develop locally in PowerBI Desktop itself, but haven't removed the instructions to do so. I can only imagine this is a strong-armed attempt in making sure custom visuals work in PowerBI Service as opposed to just Desktop.
Now to actually view the visual in development, I needed to go into PowerBI service, and turn on developer mode in settings. Now I could open a report, press edit, add the "developer" visual that should now appear in the side panel, and the contents of the custom visual would appear (assuming the development server was still running). If I wanted hot-reloading of my visual, an additional setting would need to be toggled.
Whew. Interesting start.
Actually Building The Thing
At its heart, this custom visual takes Markdown text from Power BI’s data model and converts it to HTML with some GitHub-flavor right inside the report. But making that happen securely and reliably required a careful, multi-step process.
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ Power BI Visual Host │
├─────────────────────────────────────────────────────────────┤
│ Constructor Phase: │
│ • FormattingSettingsService ──-----┐ │
│ • SelectionManager │ │
│ • TooltipServiceWrapper │ │
│ • Target Element Setup │ │
│ ▼ │
│ Update Phase: ┌──────────┐ │
│ • Extract Markdown from │ Core │ │
│ DataView │ Renderer │ │
│ • Parse with marked() └──────────┘ │
│ • Sanitize with DOMPurify │ │
│ • Create DOM Fragment │ │
│ • Apply Formatting │ │
│ ▼ │
│ Feature Setup: ┌───────────┐ │
│ • setupInternalLinks() │ Enhanced │ │
│ │ Features │ │
│ └───────────┘ │
└─────────────────────────────────────────────────────────────┘
1. Extracting and Parsing Markdown
First, the visual retrieves the Markdown string from the Power BI data model. Then, it uses the popular marked
library to convert that Markdown into HTML, enabling support for tables, code blocks, and all the formatting you’d expect from a README.
// Extract Markdown text from Power BI's DataView
const markdown = options?.dataViews?.[0]
?.categorical?.categories?.[0]
?.values?.[0] as string || "";
// Configure marked for GitHub-flavored markdown
marked.use({
gfm: true,
breaks: true,
pedantic: false
});
const html = marked.parse(markdown) as string;
2. Sanitizing for Security
Rendering HTML in Power BI comes with security risks. To prevent malicious code from sneaking in, every bit of generated HTML is sanitized using DOMPurify
. This step strips out anything dangerous, while still allowing all the formatting and features I wanted.
// Sanitize HTML to prevent XSS attacks
const safeHtml = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'blockquote', 'code', 'pre', 'a', 'img', 'table',
'thead', 'tbody', 'tr', 'th', 'td', 'hr', 'del', 'span', 'div'],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'id']
});
3. Rendering in the Visual (the ESLint-Compliant Way)
Power BI's strict linting rules mean I couldn't just inject HTML with innerHTML
. Instead, the visual carefully removes any old content, then uses a DOM fragment to safely insert the sanitized HTML. This ensures the visual updates cleanly and securely every time the data changes.
// Remove existing content
while (this.target.firstChild) {
this.target.removeChild(this.target.firstChild);
}
// Convert sanitized HTML to DOM nodes and append
const fragment = document.createRange().createContextualFragment(safeHtml);
this.target.appendChild(fragment);
4. Styling with GitHub Markdown CSS
This step is arguably the most important part. Power BI users are accustomed to horrible styling and don't know any better.
To fix this, I imported the github-markdown-css
library to give the visual that clean, iconic GitHub README.md
look that developers know and love.
The base styling comes from this library, and a few tweaks were made to allow users to customize font type.
However a hardline was drawn over font size. Instead of allowing users to disrespect good typography by changing each elements size individually, I set up everything to scale proportionally based on a single --base-font-size
variable.
This means users can adjust the base font size, and all headings, paragraphs, lists, and code blocks will scale together to fit in whatever canvas size being used.
Thankfully it was a simple implementation to maintain the integrity of the typography system against the chaos that can come from Power BI's theming system.
(I used !important
about 20 times. Sometimes you have to fight for what you believe in.)
// Import GitHub markdown CSS
@import (less) "~github-markdown-css/github-markdown.css";
:host {
// Container setup: full width/height, hidden overflow
}
.markdown-body {
// Base layout: scrollable container with padding
// Theme integration: Power BI CSS custom properties
// Typography: responsive font sizing, user font selection
// Accessibility & Focus States
&:focus { /* container focus outline */ }
code, pre { /* high contrast code blocks */ }
table, th, td { /* accessible table borders */ }
blockquote { /* themed blockquotes */ }
// Responsive Typography Scale
h1 { font-size: calc(base-font-size * 2) }
h2 { font-size: calc(base-font-size * 1.5) }
h3 { font-size: calc(base-font-size * 1.25) }
h4, h5, h6 { /* proportional scaling */ }
// List Styling System
ul, ol { /* consistent list base styles */ }
li { /* list item formatting */ }
// Nested list type variations (disc → circle → square, decimal → alpha)
// Interactive Navigation
a[href^="#"] { /* internal link styling with hover/focus states */ }
h1-h6 { /* header navigation with anchor indicators and target highlighting */ }
// Animation & Feedback
@keyframes highlight-fade { /* navigation target animation */ }
// Power BI Integration States
&.selected { /* selection state styling */ }
&:not(.text-selection-mode):hover { /* hover effects */ }
&.text-selection-mode { /* text selection mode */ }
}
5. Internal Navigation
To make large documents easy to navigate, the visual automatically detects anchor links and ensures that every heading has a unique ID. This means I could create clickable indexes or tables of contents, and users can jump to any section—just like on GitHub.
private setupInternalLinks(): void {
// Find all anchor tags with href starting with #
const internalLinks = this.target.querySelectorAll('a[href^="#"]');
internalLinks.forEach((link: HTMLAnchorElement) => {
link.addEventListener('click', (event: Event) => {
const targetId = link.getAttribute('href')!.substring(1);
// Ensure target element exists, create ID if needed
this.ensureTargetExists(targetId);
// Use native hash navigation
setTimeout(() => {
window.location.hash = targetId;
}, 10);
});
});
}
private ensureTargetExists(targetId: string): void {
let targetElement = this.target.querySelector(`#${targetId}`);
if (!targetElement) {
// Find heading by text content and add ID
const headings = this.target.querySelectorAll('h1, h2, h3, h4, h5, h6');
for (let i = 0; i < headings.length; i++) {
const heading = headings[i];
const headingText = heading.textContent?.toLowerCase()
.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
if (headingText === targetId) {
heading.id = targetId;
break;
}
}
}
}
Packaging
Before I could publish a custom visual to the Power BI marketplace, it had to pass Microsoft's audit process. This is a (reasonably) comprehensive security and functionality review that ensures the visual is safe and performs well.
It's was simple as running pbiviz package
or npm run package
, and Power BI Visual Tools printed listed out a checklist of compliance pass/fails.
Once critical checks were passed, a .pbiviz
file was created in my directory and now I could submit the file to Microsoft for marketplace review.... eventually.
Publishing
Publishing to any app store is understandably a very bureaucratic process, a lot of safety checks and verifications should take place! Signing up for Microsoft's Partner Center as an individual developer however feels a bit hostile.
In order to register I needed:
- A form of government id
- A custom domain email address
- Financial records of my domain
- The ability to update my website's DNS with records to verify I own said domain
- An official registry number from my state's business bureau, even if I was operating as a sole proprietorship
For context, other app stores (apple, android) allow you to register as an individual and sidestep the additional and unnecessary government fees and forms. Needing to register a formal business entity opens the developer up to annual state, county, and city (exemption) filings just for wanting to share free open source software!
To add insult to injury, Microsoft's partner program required me to apply as an organization, a publisher, and a developer all under the same account. This entails submitting all of the forms I listed above in three separate portals, that are all approved and denied at different cadences, regardless of what I was trying to do.
The progress bar of rejection is all I would know for weeks!
Marketplace Listings
After surviving the great filter, I was almost at the finish line. Once a visual passes the packaging audit, Microsoft will likely approve a listing as is.
I just needed to fill out the required fields in the application, wait a few days for approval, and in two weeks the visual would appear in the integrated "Get More Visuals" view of AppSource!