Creating a custom visual in Power BI
I'm making a visual to display a single KPI with some data, similar to Card with States - no graphs or anything fancy. To get started, follow Microsoft's instructions, and then go to the online version of Power BI (it won't work on desktop).
I haven't looked too much into allowing users to adjust settings yet, but will update this when I do.
Data binding and capabilities
Here is Microsoft's capabilities documentation, but don't expect much in terms of explanation.
If you feel like playing around as we go, all of the data is in the options
arg passed in the update()
method. Look at options.dataViews[0]
.
It's a little confusing that there are two different options
arguments - one for the constructor and one for the update method. You'll find that almost everything goes in the update
method, rather than the constructor. The constructor doesn't have any data because none has been dragged into it yet, so it's really not very useful.
The capabilities.json
lets you define which data you're going to be able to drag and drop into the visual, among other things. They let you specify whether you want a "Grouping" or a "Measure", or there's a combo-pack "GroupingOrMeasure". You can also just give the enum int value. I'm not entirely sure what groupings or measures are supposed to be. I'm guessing a measure is a measurement, and a grouping is some kind of category, and that's worked well so far.
Here is a sample snippet of JSON from my capabilities.json
.
{
"dataRoles": [
{
"displayName": "KPI Name",
"name": "kpiName",
"kind": "Grouping"
},
{
"displayName": "YTD Actual",
"name": "ytdActual",
"kind": "Measure"
},
{
"displayName": "YTD Target",
"name": "ytdTarget",
"kind": "Measure"
}
]
...
}
You can go more into detail and define how many of each are allowed to be in the category, along with their type.
Looking at the json: displayName
is what will appear to the user in the GUI when they're dragging and dropping fields. name
is how it's going to show up in the json, so that's what you'll be messing with in the code.
Cool, so you should now be able to see those things in the GUI. But now how do you connect to them in the code?
Making that data available to your code
First we'll have to add some more to the capabilities.json file.
{
"dataRoles" [...],
"dataViewMappings": [
{
"categorical": {
"categories": {
"for": {"in": "kpiName"}
},
"values": {
"select": [
{"bind": {"to": "ytdActual" }},
{"bind": {"to": "ytdTarget"}}
]
}
}
}
]
}
Honestly, I don't have a very deep understanding of what this stuff is. I just know that it works enough right now. I also saw that there are also different kinds of bindings you can do, so I'm sure this gets more powerful.
Anyways, looking at the json, it looks like we're defining the kpiName
as the category, and it looks like some sort of for
loop. In that loop, I think we're binding the values in values
to the data view. That part is pretty simple (for our purposes). In order to see the data, you're going to have to specify the fields that you want to see. So just add a list item for every field.
You can double-check how the data looks by either console.logging options
in your visual and then checking it out in the console. Or there's also a data view button next to the update button in the visual's context menu.
In the code
So now everything should be available to us.
Assume I have a variable called const dataView = options.dataViews[0];
To get a Grouping, you'll be looking at the categorical
property. I'm assuming this code will be a little different with multiple groupings.
let kpiName = dataView.categorical.categories[0].values[0].toString();
And then I wrote a helper function to get the measurements.
private render(options?: VisualUpdateOptions) {
...
const dataView = options.dataViews[0];
let kpiName = dataView.categorical.categories[0].values[0].toString();
let ytdTarget = this.valueForSource(dataView, "ytdTarget").toString();
}
private valueForSource(dataView, sourceName: string) {
let values = [];
dataView.categorical.values.forEach((value: any, key: number) => {
if (value.source.roles.hasOwnProperty(sourceName)) {
values.push(value.values);
}
});
if (values.length <= 0) {
console.log("Couldn't find any data for: " + sourceName);
return null;
}
return values[0];
}
And there you are. Your data.
Styling
CSS styling all happens in /style/visual.less
. Brush up on your less if it's been a while. You'll probably find the @import
statements helpful if you're bringing in CSS from other libraries, for instance, the power bi fonts. To do that, you'll need to add the font-face to the top of your file. It's got a really long base64 string, otherwise I'd just add it here. Which reminds me - in order to use fonts or images, you'll need to convert them to base64.
Tooltips
- Followed the instructions here to install: https://github.com/Microsoft/powerbi-visuals-utils-tooltiputils/blob/master/docs/usage/installation-guide.md
- Requires d3 - see below. Not sure why installing the tooltip helper library doesn't include d3, but you know. Can't have everything.
- Add the .ts file to your tsconfig.json file.
{
...
"files": [
"node_modules/jquery/dist/jquery.js",
".api/v1.10.0/PowerBI-visuals.d.ts",
"node_modules/powerbi-visuals-utils-dataviewutils/lib/index.d.ts",
"node_modules/powerbi-visuals-utils-tooltiputils/lib/index.d.ts", // <--- this one!
"src/settings.ts",
"src/visual.ts"
]
}
- You'll want to call this tooltip initialization code from your constructor:
private initTooltip(options: VisualConstructorOptions) {
this.tooltipServiceWrapper = tooltip.createTooltipServiceWrapper(options.host.tooltipService, options.element
);
}
- And then I have these methods, which I call in my render method, which is called in
update
private renderTooltip() {
this.tooltipServiceWrapper.addTooltip(
d3.select(this.target),
(tooltipEvent: tooltip.TooltipEventArgs<number>) => this.getTooltipData(tooltipEvent.data),
(tooltipEvent: tooltip.TooltipEventArgs<number>) => null);
this.tooltipServiceWrapper.hide();
}
private getTooltipData(value: any): VisualTooltipDataItem[] {
// Each item in the list adds another section to the tooltip.
// By default, the section title (the display name) appears on the left, with the value on the right.
return [
{
displayName: "Section 1",
value: "This is section 1 content",
header: this.kpi.name // This is the header for the entire toolbar
},
{
displayName: "Section 2",
value: "This is section 2",
},
];
}
Adding an external library
- A lot of newer versions aren't supported, so you'll often run into issues if you just
npm install
something. For instance, jquery and d3 are a full major version behind. npm install d3@3.5.5 --save
- After npm installing the package, you need to install the types files:
npm install @types/d3@3 --save
- Apparently the
typings
command is being phased out. Using npm is kind of nicer anyways, since you only have to deal with the packages file. reference
- Apparently the
- Add the js file to
externalJS
in thepbiviz.json
. In this case, it'll look like
"externalJS": [
"node_modules/powerbi-visuals-utils-dataviewutils/lib/index.js",
"node_modules/d3/d3.min.js",
"node_modules/powerbi-visuals-utils-tooltiputils/lib/index.js"
],
- You might also have to add a path to your tsconfig.json. This will be the case if it looks like everything has installed correctly, but your IDE can't find the library, or your console is giving you an error like
Cannot find namespace 'tooltip'.
Cannot find name 'tooltip'.
In this case, your tsconfig.json should look like:
{
...
"files": [
"node_modules/jquery/dist/jquery.js",
"node_modules/powerbi-visuals-utils-tooltiputils/lib/index.d.ts",
...
]
}
Misc tips
- If you want to change your class name, you'll have to edit
visualClassName
inpbiviz.json
- Can't play with visuals in development on PBI Desktop - you have to use the online version
- Next to the refresh button in Power BI there is an option for looking at the data behind the visual. This is really helpful, as it's essentially what's being passed through
options.dataView
Resources
- Good examples
- MAQ Software visuals (github)
- OKViz