Circular visualization of daily and monthly data with KPI ring chart and custom colormap

Code examples and walkthrough of visualising daily data by month on a circular plot, and applying a custom color map. A single plot and monthly sub-plots are shown.

Context

Today's walkthrough uses circular plots to visualise daily data by month against a target value, and create a "KPI" gauge showing a summary for the month.

The specific example here is number of steps per day, such as from a fitness tracker. (It turns out the cats of this site also use a wearable to track their steps!) The concepts can be applied to many types of data, though.

I also cover creating a custom colormap for matplotlib, as I found the inbuilt color palettes didn't provide what I needed for this one.

After putting together this notebook I was curious how many steps cats do actually take during a day, and there has actually been a study on it (if you can think of it, there's almost certainly been a research project on it!) which found that the mean number of steps a free-roaming cat takes was almost 20,000! {So the target of 5,000 I've used in my examples should be easily meetable, although Bella who was the source of this particular fictitious data must be exceptionally lazy!}

Zhang, Z.; Li, Y.; Ullah, S.; Chen, L.; Ning, S.; Lu, L.; Lin, W.; Li, Z. Home Range and Activity Patterns of Free-Ranging Cats: A Case Study from a Chinese University Campus. Animals 2022, 12, 1141. https:// doi.org/10.3390/ani12091141

Plotting data for a single month

The data values are "number of steps" (or more broadly, "some metric") with a target value. For this case I've defined the target amount as 5,000 steps per day, and generated some test data with a random value between 1 and 10,000. The values go into a list, one per day of an individual month (I've assumed a 31-day month in this case).


import random
random.seed(256) # for reproducibility
single_month_data = [random.randint(1,2000) * random.randint(1,5) for r in range(31)]

The resulting chart will have the following elements:

  • An individual bar, labelled for each day of the month, as a circular bar plot
  • A shaded background behind the individual bars, showing the point where the 'target' value should be
  • A "ring" chart (progress circle) on the outside, showing the average value against the target, colour coded according to how much of the target has been achieved
  • Text at the center of the plot showing the total and average as actual numbers

Here's the plot I produced using the data and elements above.

The bars for individual days are color-coded according to how close they are to the target. I made the decision that any data point that meets or exceeds the target is the "100%" point (dark green in this case) which is why several bars in the chart have the same color but different numeric values - all have exceeded the target.

The yellow ring chart on the outside is showing the proportion of the target (5,000) that the actual value (2,921) has achieved.

Below is the Python script used to generate this (I've also included it in a complete notebook at the end). Points to note:

  • Space at the center of the chart for the numbers and on the outside for the ring chart is allocated by finding the maximum data value and then "scaling up" the chart accordingly
  • I used a custom colormap (detailed further down) but there is also commented out code included to use a standard colormap instead
  • The plot is rotated and reversed so that the values begin at the top and go clockwise
  • I used Open Sans for the text which I find to be highly readable and aesthetic. If this isn't available on the machine running the notebook then something else should be used instead (or I recommend installing it!)
  • The outside ring is created using 2 bars - one for the colored (yellow in this case) part, and one for the grey part representing the 'unmet' amount of the target. If the target is fully met, there will only be one bar which will take up the entire outer circle.

Plotting data for multiple months

From the plot for a single month, it is easy to extend this into multiple months. I decided to show this as a grid of 4 columns by 3 rows (January-April, May-August and September-December). It could be amended easily, such as to show data for a financial year that runs July-June.

The sample data, instead of being just a list of numbers, is now a list of lists, where each list contains the daily values for a single month. This is just one method of many that could be used -- in the case of using actual data it will need to be reshaped into something similar to this.


import random
random.seed(41) # for reproducibility. 300 is also a good test case
import math
days_in_the_months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] # JFMAMJJASOND
month_names = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
months_data = [[int(random.randint(1,3000) * math.sqrt(ind+1)) for r in range(n)] for ind, n in enumerate(days_in_the_months)]

The resulting plot contains 12 subplots (one per month) of the same type that was created above.

I constructed the sample data in such a way that there's a clear progression from January through December to demonstrate how the outside ring plot will appear for different levels of achieving against the target. It also makes clear that the bars are sized and colored according to the dataset as a whole, rather than the individual month (e.g. all the bars for January are red or at best a weak orange - finishing strong in December!)

(This is a fairly large image - click through to it.)

The code is similar to the first plot, with the main difference being enumerating over the list of lists, adding a subplot at the appropriate place in the grid for each. The source of the data for each subplot is of course the list at the given place in the overall list.


for ind, d in enumerate(months_data):
    # other code here
    ax = plt.subplot(3, 4, ind+1, polar=True);

The rest of the code follows through in the same way as for the single plot, with each subplot being labelled with its month according to month_names.

Creating a custom colormap

I found that matplotlib's inbuilt colormaps didn't have what I needed, which was data with a clear red - orange - yellow - light green - dark green progression. There is a RdYlGn diverging colormap which is close to what I want, but the yellow in the middle was too light and I wanted to place more emphasis on the red values than there is in the inbuilt one. The one I've created uses colors from Material Design.

The code and output below shows the method of creating it and the difference (first row in the output is the inbuilt and the second row is the custom one). The plots above were created using the custom one.

Jupyter notebook

The complete Jupyter notebook for the above can be found here (Github Gist) including the custom colormap, single plot and multiple subplots.