Building a Custom Bar Chart in Power BI with TypeScript and D3.js

Building a Custom Bar Chart in Power BI with TypeScript and D3.js

Building a Custom Bar Chart in Power BI with TypeScript and D3.js

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.

Raushan Ranjan

Microsoft Certified Trainer

.NET | Azure | Power Platform | WPF | Qt/QML Developer

Power BI Developer | Data Analyst

📞 +91 82858 62455
🌐 raushanranjan.azurewebsites.net
🔗 linkedin.com/in/raushanranjan

Comments

Popular posts from this blog

Module 1 - Lesson 1: Getting Started with Power BI

Power BI Advanced learning

Module 1 - Lesson 2: Getting Data from Multiple Sources

Module 1 - Lesson 3: Resolve Data Import Errors in Power BI

Module 2 - Lesson 1: Introduction to Power Query Editor