This chart has been discussed in a recent video on the Minute Physics YouTube Channel. It is actually quite simple, consisting of a floating stacked bar chart. The idea of such plot is that we can use it to visualize range values, while assigning a color to represent such range.
Despite its simplicity, one might be surprised to find out that this chart is not readily available in many of the spreadsheet softwares. It is possible to draw it, yet, one needs to resort to some ad-hoc methods. This and more are discussed in the Minute Physics video.
The Challenge
We want to implement this visualization using the Julia programming language; but we want to be able to do this without resorting to a bunch of ad-hoc methods, and without having to write a humongous amount of code.
One possible solution would be to search for a charting library that covers such example. Yet, as in the case with spreadsheets, this search might be futile. Another possible approach would be to use a more generic visualization library that enables us to “directly” specify such graphic.
The Graphic Description
Before we can implement a solution, we must first get a clear picture of how this graphic is constructed. Consider the following dataset:
In order to reproduce the desired chart, we start by rearranging the dataset. We transform the locations (“Ontario”, “England” and “Kentucky”) into row values, and we pair together the temperature labels, for example, “winter mean low | annual mean low”.
If the dataset organized as such, we can more easily describe how to construct our graphic. Here is the construction logic. For each row, draw a “bar” with the horizontal position according to the location column, the lower end of the bar according to the low_temp column, the higher end of the bar according to the high_temp column, and the bar color is based on the label_temp column.
That’s it! With the dataset structured in such a way, the procedure to construct our graphic is fairly intuitive. We are now ready to implement our solution.
The Implementation
In order to draw our graphic, we need a visualization library that allows us to implement the construction logic we just described. This can be done using the package called Vizagrams.jl.
Vizagrams is a visualization grammar with a syntax very similar to VegaLite. If you have used packages such as Altair or ggplot, you probably know what I am talking about. The difference compared to these grammars is that Vizagrams implements what is called “graphic expression”.
A graphic expression is a constructive description of a graphic. In other words, we can use graphic expressions to encode our constructive logic without having to resort to ad-hoc methods, or writing a bunch of code.
This tutorial does not aim to thoroughly explain Vizagrams or graphic expressions. So let us cut short our explanation here, and move on to the solution.
In the code below, we start by importing Vizagrams. We assume that df holds the dataframe already properly transformed, and we use my-colors to specify the colors to be used (one could also just pick an existing colorscheme). We then specify the plot, which is stored into the plt variable.
The plot specification contains the data, the encoding variables (x, y, color, low_y) and the graphic, which is where we write the graphic expression. Our graphic expression simply iterates over each row in the dataset, and draws a trail having width 20 and with the respective color, lower point and higher point. The trail mark is simpler to use (in this example) and delivers the same result as using the bar mark.
To get a better understanding of what is going on, you should hop on a notebook and try the code yourself. Try changing the graphic expression using different graphical marks, or changing how the encoding variables are used.
A new approach to Data Visualization using Vizagrams.jl
Data visualization and diagramming are usually treated as distinct subjects. Yet, for the keen observer, they are quite similar. In fact, one could say that data visualization is a subset of diagramming, where data is used in a structured manner to generate a diagram drawing. This idea is explored in the Julia package Vizagrams.jl, which implements a diagramming Domain Specific Language (DSL) with a data visualization grammar built on top of it.
In this article, we show how to use Vizagrams.jl both for diagramming and data visualization. I highly recommend using a notebook such as Jupyter or Pluto in order to follow along.
Installation
The package is registered in the Julia general repository, hence, it can be installed by simply doing:
julia>] pkg> add Vizagrams
The Basics of Diagramming
Vizagrams implements a diagramming DSL inspired in the great Haskell library Diagrams. We can think of a diagram as simply a collection of primitive graphical objects (circles, rectangles, lines…), where the final drawing simply renders each object one after the other, much like an SVG.
We start with the most basic example, a single circle:
using Vizagrams # My first Diagram d = Circle() draw(d)
By default, our circle is drawn as black. We can modify its color by applying a style transformation.
d = S(:fill=>:red)*Circle() draw(d)
We can make things more interesting by adding another object to our drawing. How can we do this? Well, just add it:
d = S(:fill=>:red)*Circle() + Square() draw(d)
Note that the order of the summation matters. By adding a square after the circle, Vizagrams renders the square above the circle.
We have used the S(:fill=>:red) to apply the fill color red to the circle. Besides stylistic transformations, we can also apply geometric transformations, such as translation T , rotation R and scaling U .
d = S(:fill=>:red)*Circle() + S(:stroke=>:blue,:fill=>:white)T(2,0)*Square() + S(:fill=>:white)R(3.14/4)*Square()
draw(d)
Finally, we can combine also combine existing diagrams to form new ones.
d_2 = d + T(4,0) * d draw(d_2)
Constructing Data Visualizations
Let us now go to plotting. Again, we start with the most basic example:
As expected, the result is a simple scatter plot. Yet, there is something interesting going on. The variable plt is actually holding a diagram. Which means that we can manipulate it just like we did previously with other diagrams, and we can also combine it with other diagrams.
d = R(π/4)plt + T(250,0)*plt + Line([[180,250],[350,350],[350,200]]) + T(350,350)Circle(r=10)
draw(d)
Although the example above was silly, it does illustrates the possibilities of what can be done. In fact, it is easy to see how one can combine diagramming operations to construct more useful visualizations, such as:
Scatter Plot with a PCA fit over the data. The histogram represents the error distribution in the PCA fit. This plot was draw using Vizagrams.jl.
Visualization Grammars with Diagrams
In Vizagrams, the diagramming DSL was used to build a data visualization grammar, i.e. a specification that can produce a variety of visualizations. The syntax for the grammar is based on Vega-Lite, which is a very popular grammar in the Data Visualization community (the Python package Altair is based on Vega-Lite, and there is also a Julia package called VegaLite.jl).
Explaining visualization grammars would take an article on its own. Yet, the specification style if fairly simple, so hopefully even those not familiar with Vega-Lite will understand what is going on.
# Importing DataFrames to store the data using DataFrames
# VegaDatasets is used to load the dataset `cars` using VegaDatasets df = DataFrame(dataset("cars")); df = dropmissing(df);
# Here is where Vizagrams plot specification actually starts plt = Plot( data=df, encodings=( x=(field=:Horsepower,), y=(field=:Miles_per_Gallon,), color=(field=:Origin,), size=(field=:Acceleration,), ), graphic=Circle() ) draw(plt)
Note that we specified our plot by first passing on the dataset. Then, we defined the “encodings”, which were x , y , color and size. Each encoding variable has a parameter field which says which column in the dataset is mapped to it. Thus, in our example, we are mapping “Horsepower” to the x-axis, “Miles_per_Gallon” to the y-axis, the color is varying according to the “Origin” and the size according to “Acceleration”. At last, the “graphic” is specifying what is to be drawn in this plot. Since we passed Circle() , this means that we are drawing circles.
Here is where things get interesting. We can pass diagrams to this “graphic” parameter in the plot specification. First, let us create a diagram:
d = S(:fill=>:white,:stroke=>:black)*Circle(r=2) + Circle() + T(2,0)*Square() draw(d)
Now, we place this diagram inside the plot specification, and…
Again, this example is not very useful, yet, it illustrates the sort of things that can be achieved. Here is perhaps a more “useful” example:
Scatter plot using Penguin mark for the famous Palmer Penguis dataset. The complete example can be found in Vizagrams documentation.
Some Final Words
This article was a mere introduction to Vizagrams. There is much more to be explored, such as graphical marks creation, graphic expressions, stacking operations, and so on.
If you are interested in learning more about Vizagrams, check out the documentation. Again, I recommend using a notebook (Jupyter or Pluto), as one can quickly experiment with different designs.
We are all very used to plotting packages, yet, from time to time, we want to create visualizations that require more freedom. When that happens, it might be better to go straight to vector graphics. This is an area where people tend to be less experienced, and it’s not always clear what package to use.
Vector Graphics Packages
When it comes to vector graphics, you’ve probably heard the name Cairo, since it is perhaps on of the most famous 2D graphics libraries, and it’s commonly used by plotting packages as a backend. Thus, a first option for drawing your vector graphics is using Cairo.jl, which is Julia’s api for the Cairo library. Yet, it might not be a very pleasant experience, as Cairo can be fairly complicated for newcomers.
Luxor is a Julia package for drawing simple static vector graphics. It provides basic drawing functions and utilities for working with shapes, polygons, clipping masks, PNG and SVG images, turtle graphics, and simple animations.
The focus of Luxor is on simplicity and ease of use: it should be easier to use than plain Cairo.jl, with shorter names, fewer underscores, default contexts, and simplified functions. — Luxor’s documentation.
I could actually stop here and tell you to go read the docs, cause this package is insanely well documented. But since you are already here, I might as well take some time to quickly spill out some of the basics.
Quick Introduction to Luxor.jl
Perhaps the first thing to note about Luxor is that it’s procedural and static, which means that you code a series of drawing commands until you say it’s finished, which will in turn produce either a PNG, SVG, PDF or EPS based on what you defined at the start.
If you are used to dynamic programming (which you probably are, since you are using Julia), then this static way of drawing with Luxor will be a bit strange at first. Yet, there is a flip side. Luxor is fast! What do I mean? Just try it and you’ll see that not only the precompilation time is minimal, but every redrawing is also quite fast (compared specially to plotting packages such as VegaLite.jl and Plotly.jl).
The code above encapsulates very well the workflow in Luxor. The very first line is just importing the package. In the second line, we create a drawing with size of 500 by 500 pixels, and once we finish the drawing, Luxor will create a file called “my-drawing.svg” in the folder we are working at. All good, let’s proceed to the third line of the code.
You might think that the orgin() line is “starting” the drawing, but what it’s actually doing in altering the origin of the coordinate system to the center of the image… What do I mean? Well, in Luxor the default origin is in the top left of the figure, and the y-axis actually points down. Thus, the origin() function is responsible to altering the location of origin coordinate. And if we call this function without arguments, it places the origin at the very center of the drawing (you might be a bit confused, unless you’ve already used other vector drawing packages, in which case, this convention might actually make sense to you).
Moving on, the next line is setcolor("red") and it sets the current color to red. The important thing to note here is that this is similar to choosing the current color of the drawing pen. Hence, every drawing that we do after calling setcolor("red") will be red, until we change the color of the pen.
Let’s finally draw something in our canvas. And the command circle(Point(0,0),100,:fill) does exactly that. This command is drawing a circle with center at point (0,0) , which is the center of our figure, since we used the command origin() . The next argument sets the radius, which in our case is 100 pixels. Finally, the :fill argument indicates that we must paint the inside of the circle. Again, this is all very common if you have worked with vector drawings before.
We conclude our drawing with the finish() command, which then saves the drawing file. Now, if you want to see the drawing right after finishing it, you can just use the function preview() , which will show the most current finished drawing.
This is very similar to our initial drawing, but we’ve added some new functions and removed the origin() . Note that now our circle is actually centered in the top left corner of our figure, which can now be seen because of the background() function. These numbers inside the background function is just the color specification in hsla.
Another difference is that we used :stroke instead of :fill in the circle function. Thus, instead of filling the circle, it just draws the border. The setline() function alters the thickness of the stroke, and the setdash() changes the line style. There are many more variations in line styles, which you can check in the function docstring.
Luxor for the Mathematically Inclined
All I showed up until now is very basic, and perhaps not very useful, so perhaps this next example will grab your interest, that is, if you have a more mathematical inclination.
Let’s use Luxor to draw some diagrams, which occur in a discipline called Category Theory 🥸.
using MathTeXEngine
Drawing(200, 200, "my-drawing.png")
# Drawing the borders of our drawing. Note that the `O` is a # shortcut to Point(0,0). rect(O,200,200,:stroke)
# Let's change the origin to the center of the drawing origin()
# We now draw the our diagram setline(10) arrow(Point(-40,0),Point(40,0))
# Let's define the font size and write some text fontsize(15) text(L"f",Point(0,20), halign = :center)
Pretty neat, no? You might be a bit overwhelmed by the code above, but just read it through and you will see it’s quite straightforward. Here are some comments on things that might be obscure.
First, you might have noticed that we can write LaTeX in Luxor (I know, it’s awesome)! Just writeL”\sum^n_{i=10}”, i.e. place “L” before the string. The MathTeX is a package that is required in order to render the LaTeX string without actually needing to use a LaTeX version installed in your computer.
The final part is the code is more complicated, but the only thing going on there is that one can pass control points in order to draw curves. Hence, instead of passing only the start and finish of the arrow, I’m actually passing four points in order to make the arrow loop, and I’m also altering the start and finish position so that they do not touch the circle. The loopx and the other defined variables are just some auxiliary variables that I used in order to figure out the control points to give me a nice looking curve.
Final Words and a Hot Tip
There are many other things I could say about Luxor, since the package is quite extensive. Fortunately, as I’ve already said, the docs are very good. Not only it has many examples, but it also has many explanations about the many functionalities.
Yet, the fact that the docs are so comprehensive also makes a bit harder to find some specific things one might be looking for. Hence, here are some hot tips that might come in handy.
If you are working in a notebook environment such as Jupyter, you might wish to store the drawings in variables, instead of saving it to files right away. Also, you might wish to do small drawings, and then place them inside another drawing… Is this possible? YES! Here is how to do both.
d = Drawing(200,200,:svg) placeimage(d1) placeimage(d2) finish()
I recommend always working with svg if your drawing is not too heavy, since you’ll always have crystal clear quality. With the code above, you can just run d in a cell in order to visualize the drawing stored in the variable.
Finally, you might wish to see the actual svg specification. Here is a possible solution.
dsvg = String(copy(d.bufferdata))
This will give you the svg string, which you can just save into a mydrawing.svg file.