Detailed Guide: Creating Power BI Custom Visuals with TypeScript and D3.js
Complete Guide: Developing & Deploying Power BI Custom Visuals with TypeScript & D3.js
This comprehensive guide covers the entire process of creating, developing, packaging, and importing a Power BI custom visual using TypeScript and D3.js. We'll also incorporate solutions to common issues encountered during development, ensuring a smoother learning experience.
Prerequisites
Before you begin, ensure you have the following tools installed on your system:
- Node.js (LTS version): This includes npm (Node Package Manager), which is essential for managing project dependencies and the Power BI tools. Download from nodejs.org.
- Visual Studio Code (VS Code): A highly recommended code editor with excellent TypeScript support. Download from code.visualstudio.com.
- Power BI Desktop: To test and use your custom visual. Download from powerbi.microsoft.com/desktop.
Step 1: Install Power BI Custom Visuals Tools
The Power BI visuals tools provide the command-line interface (pbiviz
) necessary to create, develop, and package your custom visuals.
- Open your terminal or command prompt.
- Run the following command to install the tools globally:
npm install -g powerbi-visuals-tools
pbiviz
command, which you'll use throughout the development process.
Step 2: Create a New Custom Visual Project
Create the basic project structure for your custom visual.
- Navigate to the directory where you want to create your project in your terminal.
- Run the
pbiviz new
command, followed by your desired project name:pbiviz new MyD3VisualProject
MyD3VisualProject
with a name of your choice. This command will create a new folder with that name and populate it with the boilerplate files for a custom visual.
Step 3: Explore the Project Structure and Configure pbiviz.json
Navigate into your newly created project folder (cd MyD3VisualProject
). Here's a brief overview of the key files and folders:
pbiviz.json
: Visual metadata (name, version, author). You must configure this file.src/visual.ts
: Main TypeScript file for core visual logic (rendering, updating).src/settings.ts
: Defines properties for the Power BI format pane.capabilities.json
: Describes data roles and properties your visual supports.node_modules/
: Project dependencies.dist/
: Compiled JavaScript and assets after building.
Configure pbiviz.json
The pbiviz.json
file contains essential metadata about your visual. When packaging, the CLI checks for required fields. You need to fill in the author details, description, supportUrl, and crucially, the apiVersion
.
Open pbiviz.json
and update it to include the following details. Pay close attention to the apiVersion
field:
{ "visual": { "name": "MyD3VisualProject", "displayName": "My D3 Visual", "guid": "MyD3VisualProjectXXXXXXX", // REPLACE with your actual GUID "visualVersion": "1.0.0.0", "description": "A custom Power BI visual built with D3.js for interactive data visualization.", "supportUrl": "https://www.example.com/support", // REPLACE with your support URL "gitHubUrl": "https://github.com/yourusername/MyD3VisualProject", // REPLACE with your GitHub URL "images": { "icon": "assets/icon.png", "thumbnail": "assets/thumbnail.png", "screenshots": [] }, "version": "1.0.0.0" }, "author": { "name": "Your Name", // REPLACE with your name "email": "your.email@example.com" // REPLACE with your email }, "assets": { "icon": "assets/icon.png", "css": "style/visual.less", "template": "src/visual.ts" }, "externalJS": [], "style": "style/visual.less", "capabilities": "capabilities.json", "dependencies": null, "stringResources": [], "apiVersion": "5.3.0", // IMPORTANT: Add or ensure this line exists and matches your installed API version "version": "1.0.0.0" }
Important:
- Replace
"MyD3VisualProjectXXXXXXX"
with the actual GUID generated for your project (it's usually already there when you runpbiviz new
). - Update
"Your Name"
,"your.email@example.com"
,"https://www.example.com/support"
, and"https://github.com/yourusername/MyD3VisualProject"
with your actual information. - The
description
field should briefly explain what your visual does. - The
supportUrl
should be a valid URL where users can find support for your visual. apiVersion
: Make sure this matches thepowerbi-visuals-api
version installed in yournode_modules
(e.g.,"5.3.0"
as seen in yourpackage.json
or installation logs). This is critical for compatibility.
Step 4: Integrate D3.js
The pbiviz new
command typically installs D3.js and its type definitions by default. If not, or if you need to update, follow these steps:
- In your project directory, install D3.js and its type definitions:
npm install d3 @types/d3
- In your
src/visual.ts
file, ensure you have the D3 import at the top:import * as d3 from 'd3';
visual.ts
file.
Step 5: Update src/visual.ts
(Crucial Imports & Logic)
The boilerplate src/visual.ts
from pbiviz new
uses a specific import pattern for Power BI API types. Ensure your src/visual.ts
matches this structure, especially if you've copied code from other examples. This example provides a basic bar chart structure with D3.js.
Correct src/visual.ts
Structure:
import * as d3 from 'd3'; // D3.js import import powerbi from "powerbi-visuals-api"; // Import the powerbi namespace import { FormattingSettingsService } from "powerbi-visuals-utils-formattingmodel"; // Import FormattingSettingsService // Use type aliases from the powerbi namespace for Power BI API types import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions; import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions; import IVisual = powerbi.extensibility.IVisual; import IVisualHost = powerbi.extensibility.IVisualHost; // Import IVisualHost for tooltips, etc. import DataView = powerbi.DataView; // Import DataView for data access import { VisualFormattingSettingsModel } from "./settings"; // Import your settings model import "./../style/visual.less"; // Import visual styling (if using LESS) // Define a data point interface for clarity and type safety interface BarChartDataPoint { category: string; value: number; color: string; } export class Visual implements IVisual { // Correctly type host as an HTMLElement selection (the root container provided by Power BI) private host: d3.Selection<HTMLElement, any, any, any>; private svg: d3.Selection<SVGElement, any, any, any>; // SVG element for drawing private xAxisGroup: d3.Selection<SVGGElement, any, any, any>; private yAxisGroup: d3.Selection<SVGGElement, any, any, any>; private barsGroup: d3.Selection<SVGGElement, any, any, any>; private tooltip: d3.Selection<HTMLDivElement, any, any, any>; // For the tooltip element private formattingSettings: VisualFormattingSettingsModel; private formattingSettingsService: FormattingSettingsService; private visualHost: IVisualHost; // To access Power BI host services like tooltips, selection, etc. private margin = { top: 20, right: 20, bottom: 80, left: 60 }; // Increased bottom margin for category labels // Constructor: Called once when the visual is initialized constructor(options: VisualConstructorOptions) { this.visualHost = options.host; // Initialize visualHost this.formattingSettingsService = new FormattingSettingsService(); // Select the host element (which is an HTMLElement) provided by Power BI this.host = d3.select(options.element); // Append the main SVG element to the host container as the drawing canvas this.svg = this.host.append('svg') .attr('width', '100%') .attr('height', '100%') .classed('bar-chart-visual', true); // Add a CSS class for styling // Append groups for axes and bars to organize SVG elements and apply transformations this.barsGroup = this.svg.append('g').classed('bars', true); this.xAxisGroup = this.svg.append('g').classed('x-axis', true); this.yAxisGroup = this.svg.append('g').classed('y-axis', true); // Initialize a hidden tooltip div in the body (will be styled via CSS) this.tooltip = d3.select("body").append("div") .attr("class", "tooltip") .style("position", "absolute") .style("background-color", "white") .style("border", "1px solid #ccc") .style("padding", "8px") .style("border-radius", "4px") .style("pointer-events", "none") // Ensures tooltip doesn't block mouse events on elements below it .style("opacity", 0); // Start hidden } // Update method: Called whenever data or visual properties change public update(options: VisualUpdateOptions) { // Populate formatting settings model from dataView (for properties defined in settings.ts) this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(VisualFormattingSettingsModel, options.dataViews[0]); // Get current viewport dimensions const viewportWidth = options.viewport.width; const viewportHeight = options.viewport.height; // Calculate inner dimensions for the chart area, respecting margins const width = viewportWidth - this.margin.left - this.margin.right; const height = viewportHeight - this.margin.top - this.margin.bottom; // Update SVG dimensions to match the viewport this.svg.attr('width', viewportWidth).attr('height', viewportHeight); // Transform bar and axis groups to account for margins, positioning them correctly within the SVG this.barsGroup.attr('transform', `translate(${this.margin.left}, ${this.margin.top})`); this.xAxisGroup.attr('transform', `translate(${this.margin.left}, ${height + this.margin.top})`); this.yAxisGroup.attr('transform', `translate(${this.margin.left}, ${this.margin.top})`); // Data validation: Check if data is present and in the expected categorical format if (!options.dataViews || !options.dataViews[0] || !options.dataViews[0].categorical) { // If no valid data, clear all existing elements from the chart groups this.barsGroup.selectAll('*').remove(); this.xAxisGroup.selectAll('*').remove(); this.yAxisGroup.selectAll('*').remove(); return; // Exit the update method } // Extract categorical data from Power BI's dataView const dataView: DataView = options.dataViews[0]; const categoricalData = dataView.categorical; // Cast values to specific types for better type safety const categories = categoricalData.categories[0].values as string[]; const values = categoricalData.values[0].values as number[]; // Map the raw Power BI data into a more D3-friendly array of objects const data: BarChartDataPoint[] = categories.map((category, i) => ({ category: category, value: values[i] || 0, // Ensure value is a number, default to 0 if undefined/null color: this.formattingSettings.general.fill.value.value // Use the color from settings.ts })); // Filter out any data points where category or value might be invalid (e.g., null, undefined, NaN) const validData = data.filter(d => d.category !== null && d.category !== undefined && d.value !== null && d.value !== undefined && !isNaN(d.value)); if (validData.length === 0) { // If no valid data after filtering, clear all elements this.barsGroup.selectAll('*').remove(); this.xAxisGroup.selectAll('*').remove(); this.yAxisGroup.selectAll('*').remove(); return; } // Define X-scale (Band scale for categories) const xScale = d3.scaleBand() .range([0, width]) .padding(0.1) .domain(validData.map(d => d.category)); // Define Y-scale (Linear scale for values) const maxVal = d3.max(validData, d => d.value); const yScale = d3.scaleLinear() .range([height, 0]) // SVG y-axis is inverted (0 at top, height at bottom) .domain([0, maxVal ? maxVal * 1.1 : 100]); // Add a little padding to the top of the Y-axis // D3 Data Join: Select existing bars, bind new data const bars = this.barsGroup.selectAll('rect') .data(validData); // EXIT selection: Remove elements that no longer have corresponding data bars.exit().remove(); // ENTER selection: Create new elements for new data points // UPDATE selection: Apply attributes to both new and existing elements bars.enter() .append('rect') .merge(bars as any) // Merge enter and update selections for common attributes .attr('x', d => xScale(d.category)!) // X position based on category .attr('y', d => yScale(d.value)) // Y position based on value (top of bar) .attr('width', xScale.bandwidth()) // Width of bar from scaleBand .attr('height', d => height - yScale(d.value)) // Height of bar .attr('fill', d => d.color) // Bar color from data point (which gets it from settings) .on("mouseover", (event, d) => { // Tooltip show on mouseover this.tooltip.style("opacity", 1); // Clear previous content and append text nodes for security and new lines this.tooltip.html(""); // Clear existing HTML content this.tooltip.append("div").text(`Category: ${d.category}`); // Use div for new line this.tooltip.append("div").text(`Value: ${d.value.toLocaleString()}`); // Use div for new line // Position the tooltip relative to the mouse pointer this.tooltip.style("left", (event.pageX + 10) + "px") .style("top", (event.pageY - 28) + "px"); }) .on("mouseout", () => { // Tooltip hide on mouseout this.tooltip.style("opacity", 0); }); // Add transition for smooth updates of existing bars bars.transition().duration(500) .attr('x', d => xScale(d.category)!) .attr('y', d => yScale(d.value)) .attr('width', xScale.bandwidth()) .attr('height', d => height - yScale(d.value)) .attr('fill', d => d.color); // Draw X-Axis this.xAxisGroup.call(d3.axisBottom(xScale)) .selectAll("text") .attr("transform", "rotate(-45)") // Rotate labels for better readability if they overlap .style("text-anchor", "end") .style("fill", this.formattingSettings.general.axisTextColor.value.value); // Apply axis text color from settings // Draw Y-Axis this.yAxisGroup.call(d3.axisLeft(yScale)) .selectAll("text") .style("fill", this.formattingSettings.general.axisTextColor.value.value); // Apply axis text color from settings } // This method is required by IVisual and returns the formatting model for the visual public getFormattingModel(): powerbi.visuals.FormattingModel { return this.formattingSettingsService.buildFormattingModel(this.formattingSettings); } // Destroy method: Called when the visual is removed from the report public destroy() { // Clean up the dynamically created tooltip div to prevent memory leaks this.tooltip.remove(); } }
Step 6: Update src/settings.ts
Ensure your settings file uses formattingSettings.SimpleCard
as the base for standard formatting cards. This file defines the properties that will appear in the Power BI format pane.
Correct src/settings.ts
Structure:
import { formattingSettings } from "powerbi-visuals-utils-formattingmodel"; // Corrected: Use formattingSettings.SimpleCard as the base for simple formatting cards import FormattingSettingsCard = formattingSettings.SimpleCard; import FormattingSettingsSlice = formattingSettings.Slice; import FormattingSettingsModel = formattingSettings.Model; /** * General Settings Card for your visual. * This card will appear in the format pane in Power BI. */ export class GeneralSettings extends FormattingSettingsCard { // Example: A color picker property for the bars fill = new formattingSettings.ColorPicker({ name: "fill", // Internal name for the property (must match capabilities.json) displayName: "Bar Color", // Label displayed in Power BI Desktop's format pane value: { value: "#1F77B4" } // Default color (a common Power BI blue) }); // Example: A color picker for axis text color axisTextColor = new formattingSettings.ColorPicker({ name: "axisTextColor", displayName: "Axis Text Color", value: { value: "#333333" } // Default to dark gray }); // Mandatory properties for a FormattingSettingsCard name: string = "general"; // Internal name for this card (must match object name in capabilities.json) displayName: string = "General"; // Label for this card in Power BI Desktop slices: Array<FormattingSettingsSlice> = [this.fill, this.axisTextColor]; // Array of properties (slices) on this card } /** * Defines the overall formatting settings model for the visual. * This class is the root for all formatting cards and properties that appear in the format pane. */ export class VisualFormattingSettingsModel extends FormattingSettingsModel { // Instantiate your formatting cards here. // The 'general' card will contain the 'fill' color picker defined above. general = new GeneralSettings(); // Add all your formatting cards to this array. cards: Array<FormattingSettingsCard> = [this.general]; }
Step 7: Update capabilities.json
This file defines data roles, data mappings, objects (which link to your settings.ts
), and crucial privileges for your visual. The privileges
array is essential and must contain specific, case-sensitive names if your visual requires certain permissions (e.g., web access for external data, local storage).
Correct capabilities.json
Structure (Updated with Minimal privileges):
{ "dataRoles": [ { "name": "category", "displayName": "Category", "kind": "Grouping" }, { "name": "measure", "displayName": "Measure", "kind": "Measure" } ], "dataViewMappings": [ { "categorical": { "categories": { "for": { "in": "category" } }, "values": { "select": [ { "bind": { "to": "measure" } } ] } } } ], "objects": { "general": { "displayName": "General", "properties": { "fill": { // This 'fill' property maps to the 'fill' in settings.ts "displayName": "Bar Color", "type": { "fill": { "solid": { "color": true } } } }, "axisTextColor": { // This 'axisTextColor' property maps to the 'axisTextColor' in settings.ts "displayName": "Axis Text Color", "type": { "fill": { "solid": { "color": true } } } } } } }, "privileges": [ { "name": "WebAccess", // Example: If your visual needs to access external web resources "essential": true } // Add other privileges like 'FileAccess', 'Clipboard', 'Download' if needed ] }
Step 8: Start the Development Server
The development server allows you to preview your visual in Power BI Desktop as you make changes.
- In your project directory, run:
pbiviz start
info Server listening on port 8080
. Ignore any "Cannot GET /" messages in your browser if you try to openhttps://localhost:8080/
directly; this is normal as it's not a web page.
Step 9: Enable Developer Mode in Power BI Desktop
If you don't see the "Developer Visual" in Power BI Desktop, you need to enable developer mode. The option's location can vary slightly by Power BI Desktop version.
- Open Power BI Desktop.
- Go to File > Options and settings > Options.
- In the Options window, navigate to GLOBAL > Security.
- Under the "Data Extensions" section, select the radio button for: "(Not Recommended) Allow extension to load without validation or warning".
- Click OK.
- Restart Power BI Desktop for the changes to take effect.
Step 10: Test Your Visual in Power BI Desktop
Now, let's see your custom visual in action!
- Ensure your
pbiviz start
server is still running in your terminal. - Open Power BI Desktop.
- Go to the "Insert" tab on the ribbon.
- In the "Visualizations" pane, click the "Developer Visual" icon. It usually looks like a prompt or a circle with a developer-related icon, often found at the bottom of the Visualizations pane.
- Add the Developer Visual to your report canvas.
- Your custom visual should now load and display. Any changes you save in your TypeScript files will automatically refresh in Power BI Desktop (you might need to click the refresh button on the visual or interact with it). You can also drag data fields into the visual's data roles and test the formatting options in the format pane.
Step 11: Package Your Visual for Distribution
Once your visual is complete and tested, you can package it into a .pbiviz
file for distribution or for use in the Power BI service.
- Stop the development server (by pressing
Ctrl+C
in the terminal wherepbiviz start
is running). - Run the package command in your project directory:
pbiviz package
.pbiviz
file in thedist
folder of your project. This is the single file you can share or import into Power BI.
Note on Linter Errors: You might see "Linter found X errors and Y warnings." This indicates coding style or potential issues identified by ESLint (a code linting tool). While these don't prevent packaging, it's good practice to fix them for cleaner, more maintainable code. You can run pbiviz package --verbose
to see detailed linter messages.
Step 12: Import Your Packaged Visual into Power BI Desktop
This .pbiviz
file can be shared and imported into any Power BI Desktop instance, even without developer mode enabled.
- Open Power BI Desktop.
- Go to the "Insert" tab on the ribbon.
- In the "Visualizations" pane, click the three dots (
...
) and select "Import a visual from a file". - In the "Open" dialog, browse to your project's
dist
folder and select the.pbiviz
file (e.g.,MyD3VisualProject.pbiviz
). - Confirm the import. Your custom visual will now appear as a new icon in the Visualizations pane, ready for use!
By following these detailed steps, you should be fully equipped to create, test, and deploy your custom Power BI visuals. Remember to iterate: start simple, get it working, then add complexity and features like axes, tooltips, and custom formatting options.
Comments
Post a Comment