Tutorial: Build a bar chart
This tutorial shows you how to develop a Power BI visual that displays data in the form of a simple bar chart. This visual supports a minimal amount of customization. Other pages of this documentation explain how to add further customization like context menus, tool-tips, and more.
In this tutorial, you learn how to:
- Define the capabilities of your visual
- Understand the source code used to build a visual
- Render the visual
- Add objects to the properties pane
- Package the visual
Set up your environment
Before you start developing your Power BI visual, verify that you have everything listed in this section.
A Power BI Pro or Premium Per User (PPU) account. If you don't have one, sign up for a free trial.
Visual Studio Code (VS Code). VS Code is an ideal Integrated Development Environment (IDE) for developing JavaScript and TypeScript applications.
Windows PowerShell version 4 or later (for Windows). Or Terminal (for Mac).
An environment ready for developing a Power BI visual. Set up your environment for developing a Power BI visual.
This tutorial uses the US Sales Analysis report. You can download this report and upload it to Power BI service, or use your own report. If you need more information about Power BI service, and uploading files, refer to the Get started creating in the Power BI service tutorial.
Note
If the D3 JavaScript library wasn't installed as part of your setup, install it now. From PowerShell, run npm i d3@latest --save
Creating a bar chart visual involves the following steps:
- Create a new project
- Define the capabilities file -
capabilities.json
- Create the visual API
- Package your visual -pbiviz.json
Create a new project
The purpose of this tutorial is to help you understand how a visual is structured and written. You can follow these instructions to create a bar code visual from scratch, or you can clone the source code repository and use it to follow along without creating your own visual.
Open PowerShell and navigate to the folder you want to create your project in.
Enter the following command:
pbiviz new BarChart
You should now have a folder called BarChart containing the visual's files.
In VS Code, open the [tsconfig.json] (visual-project-structure.md#tsconfigjson) file and change the name of "files" to "src/barChart.ts".
"files": [ "src/barChart.ts" ]
The tsconfig.json "files" object points to the file where the main class of the visual is located.
Your final tsconfig.json file should look like this.
The package.json file contains a list of project dependencies. Replace your package.json file with this one.
You should now have a new folder for your visual with the following files and folders:
For a detailed explanation of the function of each of these files, see Power BI visual project structure.
The two files we focus on in this tutorial are the capabilities.json file, which describes the visual to the host, and the src/barchart.ts file, which contains the visual's API.
Define capabilities
The capabilities.json file is where we bind data to the host. We describe the kind of data fields it accepts and what features the visual should have.
Define data roles
Variables are defined and bound in the dataRoles
section of the capabilities file. We want our bar chart to accept two types of variables:
- Categorical data represented by the different bars on the chart
- Numerical, or measured data, which is represented by the height of each bar
In Visual Studio Code, in the capabilities.json file, confirm that the following JSON fragment appears in the object labeled "dataRoles".
"dataRoles": [
{
"displayName": "Category Data",
"name": "category",
"kind": "Grouping"
},
{
"displayName": "Measure Data",
"name": "measure",
"kind": "Measure"
}
],
Map the data
Next, add data mapping to tell the host what to do with these variables:
Replace the content of the "dataViewMappings" object with the following code:
"dataViewMappings": [
{
"conditions": [
{
"category": {
"max": 1
},
"measure": {
"max": 1
}
}
],
"categorical": {
"categories": {
"for": {
"in": "category"
}
},
"values": {
"select": [
{
"bind": {
"to": "measure"
}
}
]
}
}
}
],
The above code creates the "conditions" that each data-role object can hold only one field at a time. Notice that we use the data-role's internal name
to refer to each field.
It also sets the categorical data mapping so that each field is mapped to the correct variable.
Define objects for the properties pane
The "objects" section of the capabilities file is where we define the customizable features that should appear on the format pane. These features don't affect the content of the chart but they can change its look and feel.
For more information on objects and how they work, see Objects.
The following objects are optional. Add them if you want to go through the optional sections of this tutorial to add colors and render the X-axis.
Replace the content of the "objects" section with the following code:
"objects": {
"enableAxis": {
"properties": {
"show": {
"type": {
"bool": true
}
},
"fill": {
"type": {
"fill": {
"solid": {
"color": true
}
}
}
}
}
},
"colorSelector": {
"properties": {
"fill": {
"type": {
"fill": {
"solid": {
"color": true
}
}
}
}
}
}
},
Save the capabilities.json file.
Your final capabilities file should look like the one in this example.
Visual API
All visuals start with a class that implements the IVisual
interface. The src/visual.ts file is the default file that contains this class.
In this tutorial, we call our IVisual
file barChart.ts. Download the file and save it to the /src folder, if you didn't do so already. In this section, we go through this file in detail and describe the various sections.
Imports
The first section of the file imports the modules that are needed for this visual. Notice that in addition to the Power BI visual modules, we also import the d3 library.
The following modules are imported to your barChart.ts file:
import {
BaseType,
select as d3Select,
Selection as d3Selection
} from "d3-selection";
import {
ScaleBand,
ScaleLinear,
scaleBand,
scaleLinear
} from "d3-scale";
import "./../style/visual.less";
import { Axis, axisBottom } from "d3-axis";
import powerbi from "powerbi-visuals-api";
type Selection<T extends BaseType> = d3Selection<T, any, any, any>;
// powerbi.visuals
import DataViewCategoryColumn = powerbi.DataViewCategoryColumn;
import Fill = powerbi.Fill;
import ISandboxExtendedColorPalette = powerbi.extensibility.ISandboxExtendedColorPalette;
import ISelectionId = powerbi.visuals.ISelectionId;
import IVisual = powerbi.extensibility.IVisual;
import IVisualHost = powerbi.extensibility.visual.IVisualHost;
import PrimitiveValue = powerbi.PrimitiveValue;
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
import DataViewObjectPropertyIdentifier = powerbi.DataViewObjectPropertyIdentifier;
import { textMeasurementService } from "powerbi-visuals-utils-formattingutils";
import { FormattingSettingsService } from "powerbi-visuals-utils-formattingmodel";
import { BarChartSettingsModel } from "./barChartSettingsModel";
import { dataViewObjects} from "powerbi-visuals-utils-dataviewutils";
Interfaces
Next, we define the visual interfaces. The following interface is used to describe our bar chart visual:
- BarChartDataPoint
This interface is defined as follows:
/**
* Interface for BarChart data points.
*
* @interface
* @property {PrimitiveValue} value - Data value for point.
* @property {string} category - Corresponding category of data value.
* @property {string} color - Color corresponding to data point.
* @property {string} strokeColor - Stroke color for data point column.
* @property {number} strokeWidth - Stroke width for data point column.
* @property {ISelectionId} selectionId - Id assigned to data point for cross filtering
* and visual interaction.
*/
interface BarChartDataPoint {
value: PrimitiveValue;
category: string;
color: string;
strokeColor: string;
strokeWidth: number;
selectionId: ISelectionId;
}
Visual transform
Now that the data structure is defined, we need to map data onto it using the createSelectorDataPoints
function. This function receives data from the data view and transforms it to a format the visual can use. In this case, it returns the BarChartDataPoint[]
interface described in the previous section.
The DataView
contains the data to be visualized. This data can be in different forms, such as categorical or tabular. To build a categorical visual like a bar chart, use the categorical property on the DataView
.
This function is called whenever the visual is updated.
/**
* Function that converts queried data into a viewmodel that will be used by the visual.
*
* @function
* @param {VisualUpdateOptions} options - Contains references to the size of the container
* and the dataView which contains all the data
* the visual had queried.
* @param {IVisualHost} host - Contains references to the host which contains services
*/
function createSelectorDataPoints(options: VisualUpdateOptions, host: IVisualHost): BarChartDataPoint[] {
const barChartDataPoints: BarChartDataPoint[] = []
const dataViews = options.dataViews;
if (!dataViews
|| !dataViews[0]
|| !dataViews[0].categorical
|| !dataViews[0].categorical.categories
|| !dataViews[0].categorical.categories[0].source
|| !dataViews[0].categorical.values
) {
return barChartDataPoints;
}
const categorical = dataViews[0].categorical;
const category = categorical.categories[0];
const dataValue = categorical.values[0];
const colorPalette: ISandboxExtendedColorPalette = host.colorPalette;
const strokeColor: string = getColumnStrokeColor(colorPalette);
const strokeWidth: number = getColumnStrokeWidth(colorPalette.isHighContrast);
for (let i = 0, len = Math.max(category.values.length, dataValue.values.length); i < len; i++) {
const color: string = getColumnColorByIndex(category, i, colorPalette);
const selectionId: ISelectionId = host.createSelectionIdBuilder()
.withCategory(category, i)
.createSelectionId();
barChartDataPoints.push({
color,
strokeColor,
strokeWidth,
selectionId,
value: dataValue.values[i],
category: `${category.values[i]}`,
});
}
return barChartDataPoints;
}
Note
The next few functions in the barChart.ts file deal with color and creating the X axis. Those are optional and are discussed further down in this tutorial. This tutorial will continue from the IVisual
function.
Render the visual
Once the data is defined, we render the visual using the BarChart class that implements theIVisual
interface. The IVisual
interface is described on the Visual API page. It contains a constructor
method that creates the visual and an update
method that is called each time the visual reloads.
Before rendering the visual, we have to declare the members of the class:
export class BarChart implements IVisual {
private svg: Selection<SVGSVGElement>;
private host: IVisualHost;
private barContainer: Selection<SVGElement>;
private xAxis: Selection<SVGGElement>;
private barDataPoints: BarChartDataPoint[];
private formattingSettings: BarChartSettingsModel;
private formattingSettingsService: FormattingSettingsService;
private barSelection: Selection<BaseType>;
static Config = {
xScalePadding: 0.1,
solidOpacity: 1,
transparentOpacity: 1,
margins: {
top: 0,
right: 0,
bottom: 25,
left: 30,
},
xAxisFontMultiplier: 0.04,
};
}
Construct the visual
The constructor function is called only once, when the visual is rendered for the first time. It creates empty SVG containers for the bar chart and the X-axis. Notice that it uses the d3 library to render the SVG.
/**
* Creates instance of BarChart. This method is only called once.
*
* @constructor
* @param {VisualConstructorOptions} options - Contains references to the element that will
* contain the visual and a reference to the host
* which contains services.
*/
constructor(options: VisualConstructorOptions) {
this.host = options.host;
//Creating the formatting settings service.
const localizationManager = this.host.createLocalizationManager();
this.formattingSettingsService = new FormattingSettingsService(localizationManager);
this.svg = d3Select(options.element)
.append('svg')
.classed('barChart', true);
this.barContainer = this.svg
.append('g')
.classed('barContainer', true);
this.xAxis = this.svg
.append('g')
.classed('xAxis', true);
}
Update the visual
The update method is called every time the size of the visual or one of its values changes.
Scaling
We need to scale the visual so that the number of bars and current values fit into the defined width and height limits of the visual. This is similar to the update method in the Circle card tutorial.
To calculate the scale, we use the scaleLinear
and scaleBand
methods that were imported earlier from the d3-scale
library.
The options.dataViews[0].categorical.values[0].maxLocal
value holds the largest value of all current data points. This value is used to determine the height of the y axis. The scaling for the width of the x axis is determined by the number of categories bound to the visual in the barchartdatapoint
interface.
For cases where the X axis is rendered, this visual also handles word breaks in case there isn't enough room to write out the entire name on the X axis.
Other update features
In addition to scaling, the update method also handles selections and colors. These features are optional and are discussed later:
/**
* Updates the state of the visual. Every sequential databinding and resize will call update.
*
* @function
* @param {VisualUpdateOptions} options - Contains references to the size of the container
* and the dataView which contains all the data
* the visual had queried.
*/
public update(options: VisualUpdateOptions) {
this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(BarChartSettingsModel, options.dataViews?.[0]);
this.barDataPoints = createSelectorDataPoints(options, this.host);
this.formattingSettings.populateColorSelector(this.barDataPoints);
const width = options.viewport.width;
let height = options.viewport.height;
this.svg
.attr("width", width)
.attr("height", height);
if (this.formattingSettings.enableAxis.show.value) {
const margins = BarChart.Config.margins;
height -= margins.bottom;
}
this.xAxis
.style("font-size", Math.min(height, width) * BarChart.Config.xAxisFontMultiplier)
.style("fill", this.formattingSettings.enableAxis.fill.value.value);
const yScale: ScaleLinear<number, number> = scaleLinear()
.domain([0, <number>options.dataViews[0].categorical.values[0].maxLocal])
.range([height, 0]);
const xScale: ScaleBand<string> = scaleBand()
.domain(this.barDataPoints.map(d => d.category))
.rangeRound([0, width])
.padding(0.2);
const xAxis: Axis<string> = axisBottom(xScale);
this.xAxis.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis)
.attr("color", this.formattingSettings.enableAxis.fill.value.value);
const textNodes: Selection<SVGElement> = this.xAxis.selectAll("text");
BarChart.wordBreak(textNodes, xScale.bandwidth(), height);
this.barSelection = this.barContainer
.selectAll('.bar')
.data(this.barDataPoints);
const barSelectionMerged = this.barSelection
.enter()
.append('rect')
.merge(<any>this.barSelection);
barSelectionMerged.classed('bar', true);
barSelectionMerged
.attr("width", xScale.bandwidth())
.attr("height", (dataPoint: BarChartDataPoint) => height - yScale(<number>dataPoint.value))
.attr("y", (dataPoint: BarChartDataPoint) => yScale(<number>dataPoint.value))
.attr("x", (dataPoint: BarChartDataPoint) => xScale(dataPoint.category))
.style("fill", (dataPoint: BarChartDataPoint) => dataPoint.color)
.style("stroke", (dataPoint: BarChartDataPoint) => dataPoint.strokeColor)
.style("stroke-width", (dataPoint: BarChartDataPoint) => `${dataPoint.strokeWidth}px`);
this.barSelection
.exit()
.remove();
}
private static wordBreak(
textNodes: Selection<SVGElement>,
allowedWidth: number,
maxHeight: number
) {
textNodes.each(function () {
textMeasurementService.wordBreak(
this,
allowedWidth,
maxHeight);
});
}
Populate the properties pane using the formatting model Utils
The final method in the IVisual
function is getFormattingModel
. This method builds and returns a modern format pane formatting model object containing all the format pane components and properties. It then places the object inside the Format pane. In our case, we create format cards for enableAxis
and colorSelector
, including formatting properties for show
and fill
, according to the "objects" in the capabilities.json file. To add a color picker for each category on the Property pane, add a for loop on barDataPoints
and for each one add a new color picker format property to the formatting model.
To build a formatting model, the developer should be familiar with all its components. Check out the components of the format pane in Format Pane
. Check out getFormattingModel
API of the FormattingModel utils
in the formatting model utils repository.
Download the file and save it to the /src folder. Declare formatting properties and their values in a formatting settings class:
import { formattingSettings } from "powerbi-visuals-utils-formattingmodel";
import { BarChartDataPoint } from "./barChart";
import Card = formattingSettings.SimpleCard;
import Model = formattingSettings.Model;
import Slice = formattingSettings.Slice;
import ColorPicker = formattingSettings.ColorPicker;
import ToggleSwitch = formattingSettings.ToggleSwitch;
/**
* Enable Axis Formatting Card
*/
class EnableAxisCardSettings extends Card {
show = new ToggleSwitch({
name: "show",
displayName: undefined,
value: false,
});
fill = new ColorPicker({
name: "fill",
displayName: "Color",
value: { value: "#000000" }
});
topLevelSlice: ToggleSwitch = this.show;
name: string = "enableAxis";
displayName: string = "Enable Axis";
slices: Slice[] = [this.fill];
}
/**
* Color Selector Formatting Card
*/
class ColorSelectorCardSettings extends Card {
name: string = "colorSelector";
displayName: string = "Data Colors";
// slices will be populated in barChart settings model `populateColorSelector` method
slices: Slice[] = [];
}
/**
* BarChart formatting settings model class
*/
export class BarChartSettingsModel extends Model {
// Create formatting settings model formatting cards
enableAxis = new EnableAxisCardSettings();
colorSelector = new ColorSelectorCardSettings();
cards: Card[] = [this.enableAxis, this.colorSelector];
/**
* populate colorSelector object categories formatting properties
* @param dataPoints
*/
populateColorSelector(dataPoints: BarChartDataPoint[]) {
const slices: Slice[] = this.colorSelector.slices;
if (dataPoints) {
dataPoints.forEach(dataPoint => {
slices.push(new ColorPicker({
name: "fill",
displayName: dataPoint.category,
value: { value: dataPoint.color },
selector: dataPoint.selectionId.getSelector(),
}));
});
}
}
}
Build and create the formatting settings service model in the visual's constructor method. The formatting settings service receives the barChart format settings and converts them into a FormattingModel object that's returned in the getFormattingModel
API.
To use the localization feature, add the localization manager to the formatting settings service.
import { FormattingSettingsService } from "powerbi-visuals-utils-formattingmodel";
// ...
// declare utils formatting settings service
private formattingSettingsService: FormattingSettingsService;
//...
constructor(options: VisualConstructorOptions) {
this.host = options.host;
const localizationManager = this.host.createLocalizationManager();
this.formattingSettingsService = new FormattingSettingsService(localizationManager);
// Add here rest of your custom visual constructor code
}
Update the formatting settings model using update API. Call the Update API each time a formatting property in the properties pane is changed. Create bar chart selectors data points and populate them in formatting settings model:
// declare formatting settings model for bar chart
private formattingSettings: BarChartSettingsModel;
// ...
public update(options: VisualUpdateOptions) {
this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(BarChartSettingsModel, options.dataViews[0]);
this.barDataPoints = createSelectorDataPoints(options, this.host);
this.formattingSettings.populateColorSelector(this.barDataPoints);
// Add the rest of your custom visual update API code here
}
Finally, the new API getFormattingModel
is a simple line of code using the formatting settings service and current formatting settings model that was created in the update API above.
public getFormattingModel(): powerbi.visuals.FormattingModel {
return this.formattingSettingsService.buildFormattingModel(this.formattingSettings);
}
(Optional) Render the X axis (static objects)
You can add objects to the Property pane to further customize the visual. These customizations can be user interface changes, or changes related to the data that was queried.
You can toggle these objects on or off in the Property pane.
This example renders an X-axis on the bar chart as a static object.
We already added the enableAxis
property to the capabilities file and the barChartSettings interface.
(Optional) Add color (data-bound objects)
Data-bound objects are similar to static objects, but typically deal with data selection. For example, you can use data-bound objects to interactively select the color associated with each data point.
We already defined the colorSelector
object in the capabilities file.
Each data point is represented by a different color. We include color in the BarChartDataPoint interface, and assign a default color to each data point when it's defined in IVisualHost
.
function getColumnColorByIndex(
category: DataViewCategoryColumn,
index: number,
colorPalette: ISandboxExtendedColorPalette,
): string {
if (colorPalette.isHighContrast) {
return colorPalette.background.value;
}
const defaultColor: Fill = {
solid: {
color: colorPalette.getColor(`${category.values[index]}`).value,
}
};
const prop: DataViewObjectPropertyIdentifier = {
objectName: "colorSelector",
propertyName: "fill"
};
let colorFromObjects: Fill;
if(category.objects?.[index]){
colorFromObjects = dataViewObjects.getValue(category?.objects[index], prop);
}
return colorFromObjects?.solid.color ?? defaultColor.solid.color;
}
function getColumnStrokeColor(colorPalette: ISandboxExtendedColorPalette): string {
return colorPalette.isHighContrast
? colorPalette.foreground.value
: null;
}
function getColumnStrokeWidth(isHighContrast: boolean): number {
return isHighContrast
? 2
: 0;
}
The colorPalette
service, in the createSelectorDataPoints
function, manages these colors. Since createSelectorDataPoints
iterates through each of the data points, it's an ideal place to assign categorical objects like color.
For more detailed instructions on how to add color to your bar chart go to Add colors to your Power BI visual.
Note
Verify that your final barChart.ts file looks like this barChart.ts source code, or download the barChart.ts source code and use it to replace your file.
Test the visual
Run the visual in the Power BI server to see how it looks:
In PowerShell, navigate to the project's folder and start the development app.
pbiviz start
Your visual is now running while being hosted on your computer.
Important
Do not close the PowerShell window until the end of the tutorial. To stop the visual from running, enter Ctrl+C, and if prompted to terminate the batch job, enter Y, and then Enter.
View the visual in Power BI service by selecting the Developer visual from the Visualization pane.
Add data to the visual
Drag the edges of the visual to change the size and notice how the scale adjusts.
Toggle the X-axis on and off.
Change the colors of the different categories.
Add other features
You can further customize your visual by adding more features. You can add features that increase the visual's functionality, enhance its look and feel, or give the user more control over its appearance. For example, you can:
- Add Selection and Interactions with Other Visuals
- Add a property pane slider that controls opacity
- Add support for tooltips
- Add a landing page
- Add local language support
Package the visual
Before you can load your visual into Power BI Desktop or share it with the community in the Power BI Visual Gallery, you have to package it.
To prepare the visual for sharing, follow the instructions in Package a Power BI visual.
Note
For the full source code of a bar chart with more features, including tool-tips and a context menu, see Power BI visuals sample bar chart.