Adding Axes To Your Power BI Custom Visual
Learn how to add axes (as in axis of course) to your Power BI Custom Visual. This is video #14 of the Power BI Custom Visual Development Fundamentals series, please check the playlist below for the set.
Resources
Transcript
Hello again and welcome back to this series on developing custom visuals for Power BI.
Today, you will focus on drawing axis on a a custom visual.
Using our growing little bar chart as an example, you will see how to add a categorical axis for the bars and a continuous axis for the values.
So let’s get to it right now.
private settings = {
axis: {
x: {
padding: 50
}
}
}
The first thing to do is actually make some room in the visual to put the x-axis on.
This means padding the visual contents on the bottom side so they go up a bit and allow enough space for the axis to live in.
As I don’t like to leave magic numbers around the place, I’m going to add a new property to the visual class called settings.
This will be a simple json object that will hold a collection of general settings for the visual, starting with a padding of 50 pixels.
I may refactor this into something far more beautiful on a future video, but for now, this will do just fine.
So now that we some padding defined, we just need to push the bars up by this amount.
The way you do this in D3 is by telling the yScale object that it should start drawing the lower value data points at a higher place in the SVG canvas.
.range([height - this.settings.axis.x.padding, 0]);
Since we know by how many pixels we want to push the bars up, we just to include those pixels into the calculation of the minimum y range value.
Now if you’re thinking like, wait what is he talking about, then I recommend you go and review video 9 of this series, where I explain the principle behind D3 scales and the relationship between domain values and range values.
Otherwise, let’s take a look of what this did.
So, as you may see here, if you’re really paying attention, the bars have grown a lit bit upwards.
Okay, so it’s a bit hard to see, but we can compare and see that the bar sizes are a bit bigger than the vanilla chart, even taking the stretching here into account.
That’s because the D3 scale is now drawing the data points of these bars slightly above what they were before.
However, this does not mean that the scale is now drawing each bar 20 pixels above the previous value.
Instead, the scale is now considering that the minimum data point of zero should be drawn at exactly 20 pixels from the bottom and then scaling all the data points accordingly.
Which is great, however, the bar is still filling down the whole thing, and that’s really not the scale’s fault.
To fix this, we need to go down to the rendering code and find the bit where we are defining the height of the SVG rectangle object for each bar.
height: d => height - yScale(d.value) - this.settings.axis.x.padding,
Then, we just subtract the padding that we defined for the x-axis.
And that’s it, let’s see what this it.
There you have it, as you can see the bars are now shorter and consistent with the vanilla visual as well.
Time to go make our axis happen.
private xAxisGroup: d3.Selection;
We will start by creating a placeholder SVG group object.
This will hold all the rendering bits of the x-axis and will also allow us to easily position the axis on the visual.
this.xAxisGroup = this.svg.append("g")
.classed("x-axis", true);
Of course, we need to instantiate this, so let’s go down to the constructor and add a bit more code.
Again, same as with the bar group, this is just an empty SVG group element, it does not render anything on its own, however we are setting a CSS class of x-axis, just in case we want to style it later down the process.
With this done, let’s go down to the rendering code, right after the bit where we are defining the x scale.
let xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.tickSize(1);
This is the best spot to define our x-axis, and we can do this with this bit of code.
This creates a D3 axis function that is able to render an axis on-screen.
It does this all on its own and using SVG elements in the html.
That’s why this is part of the SVG namespace in D3 version 3.
We then associate the axis with a scale so it is able to infer what type of axis we want to render.
Right now, we are associating this axis with a categorical scale, so the axis object will know that it needs to render a categorical axis as opposed to a continuous axis or a time axis or any other type of axis.
It will also know the range of values it needs to show on-screen.
We also set the orientation of the axis so it renders neatly as a bottom axis.
Having said that, a bottom style axis is the default in D3 so you don’t need to define this if you don’t want to but it’s always good to make things explicit.
Finally, we set the ticksize to 1.
This affects the size of main line in the axis and any ticks that it has.
Although I’m not a fan of magic numbers, I’m happy leaving this one here for now as we rarely need to change this at all.
Now this code, by itself, does not render the x-axis anywhere on-screen.
It only sets up the D3 object that is able to render the axis for us whenever we need to.
xAxis(this.xAxisGroup);
To make the actual rendering happen, we need to run the D3 axis function over the SVG placeholder we have prepared for it.
So let’s see what this did.
Okay, well, that did something, we have an x-axis.
It’s just not on a very good spot yet.
Let’s go back to the code and fix that.
this.xAxisGroup.call(xAxis);
First, I’m going to replace the D3 call code with something that is functionally equivalent.
Now this bit of code does exactly what the previous bit did, it’s just another way of writing the same thing.
this.xAxisGroup
.call(xAxis)
.attr({
transform: "translate(0, " + (height - this.settings.axis.x.padding) + ")"
});
The bonus is, we can now keep calling other things on the axis group immediately after we render the axis on it.
And here we’re making use of the D3 attribute function to move the axis placeholder itself down to the bottom by applying a transform attribute.
Nothing to it, let’s see the result.
Look at that, we now have a categorical x-axis.
Thing is, it’s using default settings, so it’s a bit all over the place.
Let’s go and fix that.
.style({
fill: "#777777"
})
First, that black there is somewhat strong on the eyes, so let’s make it a nice shade of grey instead.
.selectAll("text")
Now those labels were all over the place so we must do something about them.
The D3 axis function renders those labels as SVG text elements inside whatever container you give to it, so we can grab the lot by using the selectAll function.
This returns a D3 collection that lets you continue to apply attributes and style to all those text elements at the same time, just like we do when rendering the bars, for example.
So with that in mind, we just need to do a couple of things with the labels.
.attr({
transform: "rotate(-35)",
})
First, we need to rotate them just a bit so that they don’t overlap and are easier to read.
.style({
"text-anchor": "end",
"font-size": "x-small"
});
And second, we just need to style the labels so that the text justifies to the right and the font isn’t as big as is it.
Now I’m going cheat here and use a relative size for the font, so I can get away with not creating settings for this yet, as that is in fact the theme of the next video.
So let’s see what this looks like now.
And there we go.
Now it looks like a proper axis.
Not only that, look at how it adjusts to the bars dynamically when you maximize it.
Cool, isn’t it?
So now that you understand how to add an x-axis, adding the y-axis should be a pice of cake.
Let’s go through the motions again.
y: {
padding: 50
}
First, let’s get back to our settings variable and add a setting for some y-axis padding.
I’m going to use the same padding value for now, we can change it if we find we need more than this.
private yAxisGroup: d3.Selection;
Now let’s go the visual variables and add a new placeholder SVG group for the y-axis as well…
this.yAxisGroup = this.svg.append("g")
.classed("y-axis", true);
Let’s also instantiate this svg group in the constructor…
let yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.tickSize(1);
And let’s define the D3 axis function for this y-axis.
We do this almost the exact same way as we did for the x-axis.
The only difference is that we’re now telling D3 to orient this axis left instead of bottom.
this.yAxisGroup
.call(yAxis)
.attr({
transform: "translate(" + this.settings.axis.y.padding + ",0)"
})
.style({
fill: "#777777"
})
.selectAll("text")
.style({
"text-anchor": "end",
"font-size": "x-small"
});
The last thing to do is to actually render the axis on-screen.
Now for the y-axis, we will position it horizontally using the padding value we defined.
A left side axis starts rendering from the right, so this means that this will fill up all the area that we defined as padding.
So let’s see what this did.
Hmm, interesting.
So there is still an issue here that we need to address.
We still need to push and scale both the bars and the x-axis to the right so we have room for the y-axis.
We can do this is one go by affecting the xScale object in the rendering code.
.rangeRoundBands([this.settings.axis.y.padding, width], this.xPadding);
We just need to say that the data points need to start being rendered from the y-axis padding position and not from zero anymore.
Since the other D3 functions are re-using these, they will just pick this up and well, scale accordingly.
Let’s see what this did.
Almost, almost there.
Now if you notice, the top value on the y-axis is really risking getting a hair-cut there as it’s getting drawn right on the edge of the visual.
Fixing this one is straightforward, so let’s take care of this and finish the job.
border: {
top: 10
}
First, let’s add a border setting so we can say how many pixels of space we want to free up at the top.
Ten is fine for our purposes.
.range([height - this.settings.axis.x.padding, 0 + this.settings.border.top]);
Now let’s go to where we set the yScale target range and say that we want the maximum domain data value to render just below the top of the visual, which in screen coordinates is at zero, meaning we need to add our border value to it.
So let’s see what this did.
And there you have it.
Two nice axes on our visual.
That’s it for today.
If you have any questions, let me know and I’ll get back to you.
Next, you will learn how to add simple settings to the properties pane to let the user control whether these axis will show up or not.
Until then, take care and see you next time.