Web Hosting Blog by Nest Nepal | Domain & Hosting Tips

How to Create Custom Visuals in Power BI

So you’ve been working with Power BI for a while, and you’re hitting those moments where the built-in visuals just don’t cut it. Maybe you need a specific chart type for your industry, or your stakeholders want something that matches your brand perfectly. That’s where custom visuals come in, and honestly, they’re not as scary to build as you might think.

custom visuals

What Are Custom Visuals Anyway?

Custom visuals in Power BI are essentially JavaScript-based components that extend the platform’s visualization capabilities. They’re built using the Power BI Visuals SDK and can do pretty much anything a web-based chart can do. Think D3.js visualizations, custom business charts, or even interactive widgets that go way beyond standard bar and line charts.

The cool part? Once you build them, you can package them up and reuse them across different reports or even share them with the community through Microsoft AppSource.

Setting Up Your Development Environment

Before we dive into code, let’s get your machine ready. You’ll need a few things installed:

Required Tools:

power-bi
  • Node.js (version 14 or higher)
  • Power BI Visuals Tools (pbiviz)
  • A code editor (VS Code works great)
  • Power BI Desktop for testing

First, install the Power BI command line tools:

npm install -g powerbi-visuals-tools

Then create a certificate for local development:

pbiviz-install-cert

This certificate lets Power BI Desktop trust your custom visuals during development. You’ll need to restart Power BI Desktop after installing it.

Creating Your First Custom Visual

Let’s build something simple but useful: a custom gauge chart. Start by creating a new visual project:

pbiviz new customGauge

cd customGauge

This creates a project structure that looks like this:

customGauge/

├── .vscode/

├── assets/

├── src/

│   ├── settings.ts

│   └── visual.ts

├── style/

├── capabilities.json

├── package.json

└── pbiviz.json

The main files you’ll work with:

  • visual.ts—Your main visual logic
  • capabilities.json—Defines what data your visual accepts
  • pbiviz.json—Visual metadata and configuration

Understanding the Visual Lifecycle

Every Power BI custom visual follows the same basic lifecycle:

  1. Constructor—Initialize your visual
  2. Update – Handle data changes and re-render
  3. Destroy—Clean up when visual is removed

Here’s the basic structure in visual.ts:

export class Visual implements IVisual {

    private target: HTMLElement;

    private host: IVisualHost;

    constructor(options: VisualConstructorOptions) {

        this.target = options.element;

        this.host = options.host;

        // Initialize your visual here

    }

    public update(options: VisualUpdateOptions) {

        // Handle data updates and re-render

        const dataView = options.dataViews[0];

        if (!dataView) return;

        // Your rendering logic goes here

    }

    public destroy(): void {

        // Clean up resources

    }

}

Configuring Data Binding

Power BI is informed by the JSON file about what type of data your visualization requires. We would like one measured value and possibly a target value for our gauge:

{

    “dataRoles”: [

        {

            “displayName”: “Value”,

            “name”: “measure”,

            “kind”: “Measure”

        },

        {

            “displayName”: “Target”,

            “name”: “target”,

            “kind”: “Measure”

        }

    ],

    “dataViewMappings”: [

        {

            “categorical”: {

                “values”: {

                    “select”: [

                        { “bind”: { “to”: “measure” } },

                        { “bind”: { “to”: “target” } }

                    ]

                }

            }

        }

    ]

}

Building the Gauge Visual

Now for the fun part: actually creating the visual. We’ll use D3.js since it’s powerful and well-supported:

import * as d3 from “d3”;

export class Visual implements IVisual {

    private svg: d3.Selection<SVGElement, any, any, any>;

    private container: d3.Selection<SVGElement, any, any, any>;

    constructor(options: VisualConstructorOptions) {

        this.svg = d3.select(options.element)

            .append(‘svg’)

            .classed(‘gauge’, true);

        this.container = this.svg.append(“g”)

            .classed(‘container’, true);

    }

    public update(options: VisualUpdateOptions) {

        const dataView = options.dataViews[0];

        const width = options.viewport.width;

        const height = options.viewport.height;

        this.svg

            .attr(“width”, width)

            .attr(“height”, height);

        if (dataView?.categorical?.values) {

            const value = dataView.categorical.values[0].values[0] as number;

            const target = dataView.categorical.values[1]?.values[0] as number || 100;

            this.renderGauge(width, height, value, target);

        }

    }

    private renderGauge(width: number, height: number, value: number, target: number) {

        // Clear previous content

        this.container.selectAll(“*”).remove();

        const radius = Math.min(width, height) / 2 – 10;

        const centerX = width / 2;

        const centerY = height / 2;

        this.container.attr(“transform”, `translate(${centerX}, ${centerY})`);

        // Create gauge background arc

        const backgroundArc = d3.arc()

            .innerRadius(radius – 20)

            .outerRadius(radius)

            .startAngle(-Math.PI / 2)

            .endAngle(Math.PI / 2);

        this.container.append(“path”)

            .datum({})

            .attr(“class”, “gauge-background”)

            .attr(“d”, backgroundArc)

            .attr(“fill”, “#e0e0e0”);

        // Create value arc

        const percentage = Math.min(value / target, 1);

        const valueArc = d3.arc()

            .innerRadius(radius – 20)

            .outerRadius(radius)

            .startAngle(-Math.PI / 2)

            .endAngle(-Math.PI / 2 + Math.PI * percentage);

        this.container.append(“path”)

            .datum({})

            .attr(“class”, “gauge-value”)

            .attr(“d”, valueArc)

            .attr(“fill”, this.getColor(percentage));

        // Add text label

        this.container.append(“text”)

            .attr(“text-anchor”, “middle”)

            .attr(“dy”, “0.35em”)

            .attr(“font-size”, “24px”)

            .text(`${Math.round(percentage * 100)}%`);

    }

    private getColor(percentage: number): string {

        if (percentage < 0.5) return “#ff4444”;

        if (percentage < 0.8) return “#ffaa00”;

        return “#44ff44”;

    }

}

Testing Your Visual

To test your visual locally:

pbiviz start

This starts a development server. In Power BI Desktop, you’ll see a “Developer Visual” option in the visualizations pane. Drag it onto your canvas, and it’ll load your custom visual in real-time.

Pro tip: Keep the browser developer tools open (F12) while testing. Any console errors from your visual will show up there, which is super helpful for debugging.

Adding Custom Properties

You’ll probably want users to customize your visual. Add formatting options in the capabilities.json:

“objects”: {

    “gauge”: {

        “displayName”: “Gauge Settings”,

        “properties”: {

            “minValue”: {

                “displayName”: “Minimum Value”,

                “type”: { “numeric”: true }

            },

            “maxValue”: {

                “displayName”: “Maximum Value”, 

                “type”: { “numeric”: true }

            },

            “fillColor”: {

                “displayName”: “Fill Color”,

                “type”: { “fill”: { “solid”: { “color”: true } } }

            }

        }

    }

}

Then read these properties in your visual:

private getSettings(dataView: DataView): any {

    return {

        minValue: dataView.metadata?.objects?.gauge?.minValue || 0,

        maxValue: dataView.metadata?.objects?.gauge?.maxValue || 100,

        fillColor: dataView.metadata?.objects?.gauge?.fillColor || “#1f77b4”

    };

}

Packaging and Distribution

When you’re happy with your visual, package it up:

pbiviz package

This creates a .pbiviz file in the dist folder. You can:

  • Import it directly into Power BI Desktop
  • Upload it to your Power BI tenant
  • Submit it to Microsoft AppSource for public distribution

Common Gotchas and Tips

Performance matters: Power BI can handle large datasets. Always consider performance, especially with D3.js animations. Debounce updates and avoid creating DOM elements unnecessarily.

Responsive design: Your visual needs to work at different sizes. Test it on small and large users; it will resize.

Handle missing data gracefully: Always check if your data exists before trying to render. Users will drag fields around, and your visual shouldn’t crash.

Use TypeScript properly: The SDK is built for TypeScript. Use proper typing; it’ll save you debugging time later.

Test with real data: Lorem ipsum is fine for initial development, but test with actual messy business data as soon as possible.

Next Steps

This gauge is just the beginning. Custom visuals can integrate with external APIs, include complex interactions, or even embed other web technologies. The Power BI Visuals SDK documentation has tons of examples, and the community gallery is full of inspiration.

The key is starting simple and iterating. Build something basic that works, then add complexity. Your stakeholders will be impressed that you built something specifically for their needs, and you’ll have learned a skill that makes you way more valuable as a Power BI developer.

Want to take it further? Look into adding drill-down capabilities, cross-filtering with other visuals, or even integrating with external charting libraries like Chart.js or Plotly. The possibilities are pretty much endless once you get the hang of it.

Share this article
Shareable URL
Prev Post

Predictive Analysis Using Power BI

Next Post

Time Series Forecasting Using Power BI

Leave a Reply

Your email address will not be published. Required fields are marked *

Read next