Power BI Advanced Series · Custom Visuals Development · by Raushan Ranjan, MCT

Welcome to this exciting, real-world tutorial! Today, we're going to build a custom bar chart visual for Power BI using TypeScript and the D3.js library. This end-to-end walkthrough, guided by Raushan, will take you from setting up your development environment to seeing your custom visual come to life in Power BI Desktop.

🎯 Goal:

Build a custom bar chart visual in Power BI using the Power BI Custom Visual SDK with TypeScript and D3.js.

✅ Step-by-Step Walkthrough

🧰 Prerequisites:

Before we start, ensure you have Node.js and npm installed. Then, install the Power BI Custom Visuals Tools globally:

npm install -g powerbi-visuals-tools

Next, create a new custom visual project. This command generates a boilerplate structure with TypeScript, D3, and Power BI APIs already set up:

pbiviz new BarChartVisual
cd BarChartVisual

🗂️ Folder Structure Overview

After running pbiviz new, you'll see a project structure similar to this. Here's a quick overview of the most important files:

BarChartVisual/
│
├── src/
│   ├── visual.ts        <- Your main visual logic (TypeScript + D3.js)
│   ├── settings.ts      <- Defines properties for the visual's formatting pane
│
├── capabilities.json    <- Declares what data roles and properties this visual needs
├── pbiviz.json          <- Visual metadata, ID, name, and dependencies
└── ...                  <- Other configuration files and folders

✏️ Step 1: Define Data Fields in capabilities.json

This file tells Power BI what kind of data your visual expects (e.g., categories, values). Open capabilities.json and modify the dataRoles and dataViewMappings sections as follows:

"dataRoles": [
  {
    "name": "category",
    "kind": "Grouping",
    "displayName": "Category"
  },
  {
    "name": "value",
    "kind": "Measure",
    "displayName": "Value"
  }
],
"dataViewMappings": [
  {
    "categorical": {
      "categories": {
        "for": { "in": "category" }
      },
      "values": {
        "select": [{ "bind": { "to": "value" } }]
      }
    }
  }
]
  • dataRoles: Defines the fields that users can drag into your visual's data wells (e.g., "Category" and "Value").
  • dataViewMappings: Specifies how these data roles map to the visual's internal data structure (categorical data in this case).

✏️ Step 2: Write Custom Visual Logic in visual.ts

This is the core of your custom visual, where you'll use TypeScript and D3.js to render the bar chart. Open src/visual.ts and replace its content with the following code:

import * as d3 from "d3"; // Import D3.js library
import powerbi from "powerbi-visuals-api"; // Power BI Visuals API
import DataView = powerbi.DataView; // Type definition for DataView
import VisualConstructorOptions = powerbi.VisualConstructorOptions;
import VisualUpdateOptions = powerbi.VisualUpdateOptions;
import IVisual = powerbi.extensibility.IVisual;

// Define an interface for our data structure
interface BarData {
    category: string;
    value: number;
}

export class Visual implements IVisual {
  private svg: d3.Selection<SVGElement, any, any, any>;
  private barsGroup: d3.Selection<SVGGElement, any, any, any>;

  // Constructor: Called once when the visual is initialized
  constructor(options: VisualConstructorOptions) {
    // Select the host element and append an SVG container
    this.svg = d3.select(options.element)
      .append('svg')
      .classed('barChart', true); // Add a CSS class for styling

    // Append a group element to hold our bars (makes transformations easier)
    this.barsGroup = this.svg.append('g')
      .classed('bars', true);
  }

  // Update method: Called whenever data or visual properties change
  public update(options: VisualUpdateOptions) {
    // Get the current width and height of the visual container
    const width = options.viewport.width;
    const height = options.viewport.height;

    // Set the SVG dimensions to match the viewport
    this.svg.attr("width", width).attr("height", height);

    // Get the dataView from Power BI options
    const dataView: DataView = options.dataViews[0];

    // Extract categories and values from the dataView
    const categories = dataView.categorical.categories[0].values;
    const values = dataView.categorical.values[0].values;

    // Map the raw Power BI data into a more D3-friendly structure
    const data: BarData[] = categories.map((cat, i) => ({
      category: cat.toString(),
      value: <number>values[i]
    }));

    // Define X-scale (Band scale for categories)
    const xScale = d3.scaleBand()
      .domain(data.map(d => d.category)) // Categories as domain
      .range([0, width]) // Map to full width of the visual
      .padding(0.2); // Add padding between bars

    // Define Y-scale (Linear scale for values)
    const yScale = d3.scaleLinear()
      .domain([0, d3.max(data, d => d.value)!]) // Values from 0 to max value
      .range([height, 0]); // Map to full height (inverted for SVG y-axis)

    // D3 Data Join: Select existing bars, bind new data
    const bars = this.barsGroup.selectAll("rect").data(data);

    // ENTER + UPDATE: Add new bars and update existing ones
    bars.enter()
      .append("rect") // Append a new rect for each new data point
      .merge(bars as any) // Merge enter and update selections
      .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", "steelblue"); // Bar color

    // EXIT: Remove bars that no longer have corresponding data
    bars.exit().remove();
  }
}
  • Imports: We import d3 for drawing and Power BI API types for visual development.
  • Constructor: This method is called once when the visual is initialized. We select the host HTML element provided by Power BI and append an SVG container to it. We also create a `` (group) element to hold our bars, which helps in organizing the SVG elements.
  • Update Method: This is the most important method, called whenever the data, size, or properties of the visual change.
    • It retrieves the current width and height of the visual and sets the SVG dimensions.
    • It extracts the category and value data from Power BI's dataView and maps it into a D3-friendly array of objects.
    • Scales: It defines two D3 scales:
      • xScale (a d3.scaleBand()): Maps categorical data (e.g., product names) to positions along the x-axis, providing bandwidth for each bar.
      • yScale (a d3.scaleLinear()): Maps numerical data (e.g., sales values) to pixel heights along the y-axis. Note that SVG's y-axis typically increases downwards, so we map from height to 0.
    • D3 Data Join (enter(), merge(), exit()): This is the core D3 pattern for updating visuals.
      • bars.enter().append("rect"): Creates new `` elements for any new data points.
      • .merge(bars as any): Combines the newly created elements with existing elements, allowing a single chain of attributes to apply to both.
      • Attributes like x, y, width, height, and fill are set dynamically based on the data and scales.
      • bars.exit().remove(): Removes any `` elements that no longer have corresponding data points (e.g., if a category is filtered out).

⚙️ Step 3: Test the Visual

Now, let's see your custom bar chart in action!

  • Open your terminal in the BarChartVisual folder and run:
    pbiviz start
    This command starts a local web server and opens Power BI Developer Tools in your browser (usually at https://localhost:8080/visuals/).
  • Open Power BI Desktop.
  • Go to the Developer tab (if not visible, enable it in File > Options and settings > Options > Preview features > Developer mode visual).
  • Click the Developer Visual icon in the Visualizations pane.
  • Drag some sample data (e.g., "Product Category" to Category, "Total Sales" to Value) onto your visual. You should now see your custom D3.js bar chart!

🎨 Add Event Handling (Optional)

Let's make our bars interactive! You can add event listeners to your D3 elements. Inside the update() method, after the .attr("fill", "steelblue"); line, add the following:

// Add a click event listener to the bars
    bars.enter()
      .append("rect")
      .merge(bars as any)
      // ... (existing attributes) ...
      .on("click", (event, d) => {
        // In a real visual, you'd use powerbi.extensibility.utils.interactivity.selectionManager
        // to handle selections. For this demo, we'll use a simple alert.
        alert(`You clicked on ${d.category} with value ${d.value}`);
      });

Now, refresh your visual in Power BI Desktop (using the refresh button on the Developer Visual) and click on a bar. You should see a browser alert!

🚀 Build for Production

Once you're happy with your visual, you can package it into a .pbiviz file for distribution.

  • In your terminal (still in the BarChartVisual folder), run:
    pbiviz package
    This command compiles your TypeScript code, bundles all assets, and generates a .pbiviz file inside the /dist folder. This file can then be imported into any Power BI Desktop report or uploaded to Power BI Service.

🧠 Recap – What You Used

Technology Purpose
TypeScript Used to write clean, type-safe, and maintainable logic for rendering the visual, catching errors early.
D3.js The powerful JavaScript library used to draw and manipulate SVG elements dynamically based on data, creating the bars and handling their positioning and sizing.
Power BI Custom Visual SDK Provided the framework and APIs (like IVisual, DataView, VisualUpdateOptions) to integrate our D3.js code with Power BI, handle visual packaging, data binding, and formatting pane properties.

💡 Real-World Scenarios:

Building custom visuals is not just an academic exercise; it solves real business problems:

  • Advanced animations: When you need more sophisticated visual transitions or animations than built-in visuals provide (e.g., changing bar color on click, animating growth).
  • Unique charts: To create highly specialized or non-standard chart types (e.g., Sunburst charts, Radial Bar charts, Bullet charts, Sankey diagrams) that are not available natively.
  • Visual identity/Branding: For organizations that require specific branded or company-themed visuals that strictly adhere to design guidelines.
  • Interactive components: To implement custom drilldowns, tooltips, dynamic highlights, or other interactive behaviors that go beyond standard Power BI interactions.
  • Integrating external libraries: When you need to leverage other JavaScript libraries for specific functionalities within your visual.

Quick Knowledge Check

Q1. In a D3.js bar chart, why is the bar's y attribute calculated as yScale(value) while the bar's height is height - yScale(value)?

Show Answer

Because SVG coordinates start from the top-left: y=0 is at the top, increasing downward. yScale(value) maps a data value to a pixel position from the top of the chart area. A larger value maps to a SMALLER y (closer to the top). The bar must start at yScale(value) (its top edge) and extend DOWN to the bottom of the chart area. Height = chartHeight - yScale(value) gives the correct vertical span. A common mistake is using just yScale(value) as the height, which produces inverted bars that grow from the top.

Q2. When new data is passed to a D3 bar chart on each Power BI update() call, which D3 pattern ensures old bars are removed, existing bars are resized, and new bars are created — all efficiently?

  • A) Clearing the SVG and re-appending all elements from scratch
  • B) The enter/update/exit pattern using .data(), .enter().append(), and .exit().remove()
  • C) Using document.querySelectorAll to find and update each bar manually
  • D) Calling d3.selectAll().remove() before each render
Show Answer

B. D3's enter/update/exit pattern is the idiomatic approach: .data(newData) joins data to existing elements; .enter().append() creates elements for new data items; the update selection adjusts existing elements; .exit().remove() deletes elements for removed data items. Option A works but is inefficient — it destroys and recreates all DOM elements on every update, causing flicker and poor performance with large datasets.

Q3. How do you make a Power BI custom bar chart respond to window/viewport resizes — e.g., when the user resizes the report page?

Show Answer

Use options.viewport.width and options.viewport.height from the update() call to recalculate scales, redraw axes, and resize the SVG. Power BI calls update() with type === VisualUpdateType.Resize when the viewport changes. Inside update(), always read the current viewport dimensions, recalculate scaleBand and scaleLinear ranges, and re-render. Never hardcode pixel dimensions — custom visuals must be fully responsive.

5 Things to Remember
  • SVG y-axis is inverted — y=0 is top; bars start at yScale(value) and have height chartHeight - yScale(value). Getting this backwards produces upside-down bars.
  • enter/update/exit is more efficient than clear+redraw — D3 only changes what's necessary, avoiding full DOM destruction and flicker on every update call.
  • Always read viewport from options.viewport — never hardcode dimensions. Power BI calls update on resize; your scales must recalculate to fill the new space.
  • scaleBand.bandwidth() gives bar width automatically — after setting domain and range on a band scale, bandwidth() returns the computed bar width including padding.
  • Add axes in the constructor, update them in update() — create axis group elements once; call axisBottom(xScale) and call(xAxis) in every update to reposition labels when data changes.