A few months back I began experimenting with data visualisation in Blender. I was curious about the potential for creating accurate, informative visualisations in Python with the creative power of Blender behind them. A number of tools exist for creating eye-catching data visualisations in 2D, beyond those available in Python. However, once I began scouring the internet for similar tutorials in 3D there was significantly less information available and what tutorials I did find were inaccurate or were not thorough enough to be suitable.
What you will find on this page is a guide of my initial experimentations in putting my Python and Blender knowledge to use to create perhaps the most simple of graphs, a column graph.
Section 1. Setting up your workspace
The Blender Scripting workspace is the main setup that we'll use with the 3D Viewport set to the Orthogonic Top View. We need the orthogonic view to ensure accurate placement of graph elements. I recommend to view everything exclusively in the orthogonal view and only switch to perspective view when you're ready to animate.
In the main section of the workspace, you'll find the Text Editor which is where you will write your script. Hit the 'New' button to start a new file. You can use the 'Save As' button to save your script to whatever folder on your computer.
Section 2. Import the Blender module
Python comes with many preinstalled modules but thousands more external modules are also available. Often you will need to specifically import a module installed in your version of Python. The bpy module is what Python uses to interact with Blender. You don't need to worry about installing it because it comes pre-installed with Blender. However, you will need to import the module into your script otherwise your script will generate an error. I've also added the random function which we will use to generate some materials later on in the tutorial.
Write the following as the first line of your script:
Section 3. Delete the default cube
You could code this in with the following snippet which might be useful for whenever you want to generate a new column graph in a new Blender file. However, seeing as this will likely be the first time you'll use this code you'll likely run the script several times for testing so this code snippet becomes a little redundant. In any case, I leave it up to you as to whether to include it in your version of this script.
If you do include the following, insert it under your import line. Everything from here on out goes under the import line, don't stick anything above it save for a comment. Comments in Python follow the # symbol. Use these to make notes.
Section 4. Defining our data
There are many ways to do this in Python. In this example, we will create a simple list to store our data values but, if you have the knowledge, you could also fetch data from a file such as a .csv or store the data in another data structure such as a dictionary.
Note: the data we use here is dummy data used only to illustrate how to generate a column graph. It has no true meaning whatsoever.
We also need lists and variables to store our x and y axis ticks and labels as well as axis titles.
Section 5. Defining some dimensions
We're also going to take some time to pre-define how large our grids, bars, titles and labels are, as well as some variables that are going to help space these things out evenly across our graph. Doing these things beforehand allows you to tweak and change your graph easily whenever you want to make a change.
Gridx and Gridy are the x and y lengths of the grid that we will generate for our graph. You can tweak these to change the size of the grid
labelSize is the size of the labels on the x axis
ticksize is the size of the labels on the y axis
titleSize is the size of the main title that will display at the top of the graph
axisTitleSize is the size of the titles on the x and y axis
barWidth is the width of each bar that we will generate
nsubs is the number of subdivisions we want in our grid. We will need 11, not 10, in order to accurately display values in tenths. E.g. in our case we're going from 0 to 1. If we only had 10 subdivisions our values would only go from 0 to 0.9.
Section 6. Generating a grid
First things first, we'll want a grid for our bars to be displayed on so we can see what values each bar represents. We have already defined our dimensions and subdivisions so we'll use these with our Python bpy module to tell Blender what kind of grid we want to create.
The first line in this code snippet adds a grid to our scene in much the same way as using the 'Add' menu in the main viewport does. x_subdivisions and y_subdivisions are arguments we can use to tell Blender how many subdivisions we want in both axes. size is self-explanatory as is the location which we've set to the origin for ease. In the second line we rescale the grid to the dimensions we specified in Gridx and Gridy. In the third line we apply a wireframe modifier to change the native grid object in Blender to look like the sort of grid that we would like for our graph. Once you run your script, you can find this modifier in the modifier stack and change the options manually if you don't like the thickness of the lines etc.
At this point you may want to test your script to see if your grid does indeed appear in the viewport. To do this, first save your script and then go to Text>Run Script. A Grid should appear and hopefully without any errors. If the script does fail, you'll need to see the console in order to understand what went wrong. Go Window>Toggle System Console and scroll to the bottom to see the error message.
Note: Before you run your script again you must delete everything in the viewport. The default Camera and Light are the only exceptions.
Section 7. Generating bars
This is probably one of the harder sections to understand. However, I've set it up such that you only need to pre-define your variables at the beginning of the script, so you should be able to generate your bars without a problem.
The first line defines the step which is the spacing between each of your bars. Gridx/nxlabels divides the length of the Grid by the number of bars. Since the origin of the bar objects are at the width centre instead of the edge, the step must also take into account the width of the bars which is why we subtract half the barWidth from the previous value. The final number is simply a buffer to increase or decrease the step depending on how you want your graph to look.
The tickstep generates even spacings for the labels on the y axis. This value shouldn't be changed, for the sake of accuracy.
The barLocation varible is the hardest to understand. -1*(Gridx/2) is the x location of the left edge of the grid in the which is negative because the centre of our grid is at the origin. We take this first point and add step to it every time we add a new bar which is essentially what the rest of the line does.
Note: if you're not familiar with the structure in the third line, it's called a list comprehension. List comprehensions are slightly advanced which is why I've set the script up such that you don't need to understand how one works in order to complete this tutorial. For anything that you might be interested in changing, go to the variables predefined near the start of the script.
However, if you are interested in learning more I would encourage you to look them up. Here I will only say that they are like for loops, just faster and obviously much more condensed.
This is the for loop which generates the actual cubes that will act as bars in our bar graph. The first line ensures that we generate the right number of bars, one at a time. For each of these bars, the second line calculates the location of that bar in the y. The third line then generates the bar at the x co-ordinate in barLocation that corresponds to that bar, and the y location currentBarLocationy which was calculated in the previous line. 0.2 is the z co-ordinate of the bar which you can change depending on how close of far you want the bar to be from the Grid.
The three if statements rescale the bars to the width we specified in barWidth, and height according to the corresponding values that we want them to represent in our barHeight list.
Note: the if statements depend on Blender's default naming convention for objects.
Section 8. Generating x and y labels
The following code looks a little bulky but this is mostly down to the way that Blender handles Text objects, which is 'not easily'. Except for the middle text editing chunk of code, generating labels is actually fairly similar to generating bars in the previous step. We start with the x axis labels.
We start by looping through the amount of labels that we want to generate, one at a time. For each label we calculate the desired y co-ordinate in the second line. The 0.3 value is a buffer which you can change for the labels under your graph. In the third line we add a text object with the default 'Text' word and enter Edit mode.
In the first line of 'Changing the text' we are deleting the default word 'Text'. In the second line we replace it with the corresponding text in our barLabels list. In the third line we get out of Edit mode. The fourth line sets the origin of the text object to the center instead of the lower left hand corner of the first letter. The fifth line is where we define the co-ordinates of the label, barLocation[i] in the x, xLabelHeight in the y and 0.2 in the z. The sixth line defines the rotation of the Text object where 0.785 radians is equal to 45 degrees. You can delete this line if your labels don't overlap.
The rescale section is the same as the if statements in the Generating bars section. The only difference, of course, is the name of the objects and their scaling. In this example we set them all to the same value.
Tip: When coding with rotations you'll generally need radians and not degrees. You can convert these values a few different ways (e.g. calculator). In the Blender viewport you can find the radian conversion of any degree value by opening up the properties tab (shortcut 'n') and hovering your mouse over any one of the object rotation values. A little tooltip will appear with the radian value so, use these rotation fields to find the radian conversion of any value you wish to use when scripting.
The y label loop works more or less the same way as the x label loop with the addition of a y co-ordinate calculation and the variable n which we use to find the right name of Text objects that we want to rescale at the end. Most of the rest of the loop is exactly the same logic just with the values relevant to the y ticks instead of the x labels.
Section 9. Adding titles
Congratulations! If you've made it this far then the bulk of your graph is done and the rest is just adding titles and materials which you could arguably do manually. However, more complete and efficient graph generating script to reuse in the future you may want to continue on.
First, we start with the main title that goes over your graph. The good news is that nothing here is new. These are the same coding concepts and functions that we've already used in the previous label and tick loops.
There are only a few things here to note. t was calculated previously at the start of the script. Use the line with bpy.context.object.location to tweak the location of the title.
Same as the main title except for location, scale factor and t+1.
Same as the x label titles except for rotation, location and t+2.
Section 10. Adding Materials
This part is a fun little exercise in generating some random materials. Right now your graphs are dull grey bars, labels and grids on a grey background. You could manually assign materials to every object in your scene or, you could use your script to do it automatically for you.
Here, we've started with the labels, ticks and titles. Pretty much any object with 'Text' in it's name. Traditionally, these bits of text would just be coloured black so that's what we're going to do here. Of course, you could make your text any colour you like.
In the first line, we create a new material called 'LabelMaterial' which we assign to a variable called labelMat.
In the second line, we set the diffuse colour of the material to be black or (0,0,0,1).
In the third line the material 'LabelMaterial' in labelMat is added to every object with 'Text' in it's name.
In this last bit of code we're assigning a material for each object in our scene with 'Cube' in it's name (which should just be the bars). We're giving each material a colour using the random function in Python to generate 3 random numbers between 0 and 1. In the last line of the loop we're simply adding the materials to the objects as they're generated.
Congratulations! You should now have a fully functioning script for generating column graphs! At this point you can go back into the default layout and tweak your graph however you like. You might want to change the z scale of your bars, they're quite square at the moment. Or manually change the distance between the grid and the bars. I wouldn't recommend changing the x, y locations of your bars. Python has calculated their correct positions so it's best not to tamper with them. You could tweak your grid or assign a material to it as well as tweak the colours of the manually generated materials that we just coded into your script.
A final word from Kit.
And that's it! I have attached my cleaned up version of the code and offline version of the tutorial to download if you would like to reference it. There are also bonus sections on animating your graphs like the one below using the same information as Part 2.1.
Take care everyone and feel free to send me an email. :)
Appendix