Spine Chart

Spine charts #

A spine chart is a way to show deviation about a particular value, and can be thought of as constructed using back-to-back bar charts.

!

Image taken from https://blog.gramener.com/types-of-data-visualization-for-data-stories/

Spine charts in Plotly #

There is no direct method by which to construct a spine chart within Plotly, however, we can use the subplot functionality to create two bar charts that we can place back to back. This will be our approach.

Prepare the data #

For a spine chart, we must have data that shares similar categories and quantities, but for which it makes sense to represent in bars facing opposite directions. There are many examples of such data, but one that we might consider is the gender representation in the United States Congress. Though the number of members in congress might change, we can always represent this as a fraction of the total members, which the number of men and the number of women ranging from 0 to 1 each.

Thus, we simply need data where we have this percentage (or fraction) for each gender. Collecting data for this, we might have the following…

!

The data continues beyond what is shown, but we have the data for each session of congress (every 2 years) and the number of women, as well as the fraction of women and fraction of men in congress. You can download the data set here.

Plot the data #

We can make a chart by first importing the requisite libraries:

import numpy as np
from plotly.subplots import make_subplots

Then we can import the data we want. We really only need the years, fraction women and fraction men. Recalling that we start counting in Python at 0, this is columns 0, 2, and 3. We also want to skip one rows of headers (the first row) and we want to also unpack the return, so we get the columns. This can be done using:

congress = np.genfromtxt(<path to data>,
                         unpack = True, 
                         delimiter = ",",
                         skip_header =1, 
                         usecols = [0, 2, 3])

In this way the variable congress will hold three arrays of data, the years, fraction of women, and fraction of men, in that order. We are now in a position to plot this data.

A spine chart is basically just back to back bar charts, so the place to start is with two bar charts. As illustrated in the tutorial on small multiples, we can use the rows and columns keyword arguments in the make_subplots() function to create a grid.

congressPlot = make_subplots(rows = 2, cols = 1)

If we want to leverage the visual metaphor for women rising, we can place the fraction of women on top, and then the fraction of men on bottom.

congressPlot.add_bar(x = congress[0], y = congress[1], row = 1, col =1)
congressPlot.add_bar(x = congress[0], y = congress[2], row = 2, col =1)

If we change the template to simple_white and show the plot:

congressPlot.update_layout(template = "simple_white")
congressPlot.show("png")

We will obtain the following plot.

This is clearly not a spine chart. However, we can use the vertical_spacing keyword argument to reduce the separation between rows to 0, and we can also reverse the autorange direction for the top plot (row = 1, col = 1) using the following changes:

congressPlot = make_subplots(rows = 2, cols = 1, vertical_spacing = 0)

and

congressPlot.update_yaxes(autorange = "reversed", row = 2, col = 1)

which will yield the following plot:

This is not yet well-designed, but it is a spine chart. So let us work from this stage to refine it.

Refine the plot #

The first thing to do is to refine the $x$-axes. We don’t need the $x$-axis for the top plot—indeed, it is in the way. For the bottom, we don’t need the vertical line. We can accomplish these two changes with the following:

congressPlot.update_xaxes(showline = False, showticklabels = False, ticks = "", row = 1, col = 1)
congressPlot.update_xaxes(showline = False, row = 2, col = 1)

which provides:

This solves the issues with the $x$-axis, but not the $y$-axes. So we need to address this. Perhaps the largest problem is that the scales are different. The meaningful scale here is between 0 and 1, for both men and women. So, we can enforce this using:

congressPlot.update_yaxes(autorange = "reversed", row = 2, col = 1)
congressPlot.update_yaxes(range = [0, 1])

Which will provide:

We are now very close to done. I think we can remove the legend, which violates proximity and separation, and then add a title that makes a claim about the data. We also might consider a different color for the bars. Though there are reasons to object to such choices, pink and blue are often used to represent women and men in the USA, so might leverage that existing metaphor as well. We can even color code the word “women” in the title, to emphasize it, and remove the need for a legend. For this, we can use the HTML commands for changing the color of a word:

<span style='color: salmon'> words to be changed </span>

Since Plotly is the plotting library for the web, it can interpret such commands well. So, we will use that.

Putting this all together, we arrive at the following final code solution:

import numpy as np
from plotly.subplots import make_subplots

congress = np.genfromtxt(r"C:\Users\benle\Documents\GitHub\DMD-HUGO\hugosite\static\data\Women in Congress.csv",
                         unpack = True, 
                         delimiter = ",",
                         skip_header =1, 
                         usecols = [0, 2, 3])

congressPlot = make_subplots(rows = 2, cols = 1, vertical_spacing = 0)

congressPlot.add_bar(x = congress[0], y = congress[1], row = 1, col =1, marker = dict(color = "salmon"), showlegend = False)
congressPlot.add_bar(x = congress[0], y = congress[2], row = 2, col =1, marker = dict(color = "lightblue"), showlegend = False)

congressPlot.update_xaxes(showline = False, showticklabels = False, ticks = "", row = 1, col = 1)
congressPlot.update_xaxes(showline = False, row = 2, col = 1)

congressPlot.update_yaxes(autorange = "reversed", row = 2, col = 1)
congressPlot.update_yaxes(range = [0, 1])

congressPlot.update_layout(title = "The fraction of <span style='color: salmon'><b>women</b></span> in the US congress is increasing over time" , 
                           title_x = 0.12,
                           template = "simple_white")

congressPlot.show("png")

Which results in: