You can find the working code for the chart.js extension on GitHub.
What is a Spie Chart?
A spie chart is an overloaded version of a pie chart, where you can vary the height of each segment along with its width. This enhancement enables various things, such as the comparison of a dataset in two different states, or the addition of extra dimensions to a single dataset.
The next two sections use examples from the original paper on spie charts to illustrate these uses (though my data is totally made up).
Type A: Comparison of States
To compare a dataset in two states, the original dataset can be segmented in a regular pie chart, with the difference between this and the next state illustrated by the varying height of each segment.
Imagine government expenditure for two distinct years charted in separate pie charts. The change between the two charts is so marginal that it is difficult to determine what changed.
With a spie chart, expenditure in one year can be represented by the width (angle) of each segment in the chart, and the change in spending for the next year can represented by the height (radius) of each segment. This allows you to visualize both the relative weight of spending in each sector, and how that spending has changed over time:
In this example, some segments are broken down into multiple slices, where the outer slice illustrates how much spending has been reduced between the two reports.
Type B: Additional Dimensions
Alternatively, the height can be used to show a size relative to one value, while the width shows the same size relative to another.
In the following example (again based on one in the Feitelson paper), the spie chart conveys road casualties by age and sex. The height of each segment is used to indicate the number of casualties relative to the population size of that segment, while the width is used to indicate the proportion of casualties between age ranges (segments).
This example also shows how slices can be used to convey even more information, such as a breakdown of casualties in a segment between pedestrians, car riders, and (motor)cyclists.
The key characteristics of the spie chart in these examples are the ability to vary both the height and width of each segment, along with the ability to slice each segment into distinct sections.
Creating a Spie Chart
I decided to use chart.js as the basis for the chart, which proved to be a good decision, as it’s nicely written and extremely extendible.
Chart.js consists of a core file — named Chart.Core.js — and several others, with one for each type of chart supported. I decided to copy and extend the polar area chart, since it allows for pie-like segments that have different heights (though each segment has the same width).
Each of the chart type files has the same general structure, but i’ll discuss the Chart.PolarArea.js file here.
At the top of the file, a defaultConfig object provides defaults to be used for the chart’s display, including whether a scale is shown, whether animations are used, and the specifics of these features.
The majority of the code is contained within an extension of the Chart.Type object, which is the base class for all of the chart types. For a polar area chart, this contains an array of segments (the SegmentArc type), and set-up code for displaying a scale and tooltips.
The key functions in the PolarArea chart type are:
- addData(), through which incoming data is passed from the calling function, and parsed into individual SegmentArc objects.
- draw(), which is (somewhat obviously) called to draw the chart. When an animation is used for the chart, this function is called repeatedly, with the size of each segment gradually increasing as a result of a call to an easing function.
Each call to draw() adjusts the size of each segment by changing the start and end angle of the segment, and then the draw() function for each segment is called. This draw function is implemented in the Chart.Arc type in the core file and is common to pie charts and polar area charts.
Writing the Spie Chart
The basis of my spie chart is the existing polar area chart, modified to support variable width segments and slices. The Chart.Core.js file remains un-altered.
To begin, I updated the expected input format of the chart to allow for each of these changes.
The input format looked like this for a given segment:
But now looks like this:
label: "Segment Label",
label: "Slice Label 1"
label: "Slice Label 2"
I created a new Chart.Arc type, called SliceArc, and updated the addData() function to parse the incoming slices into this object (meaning the SegmentArc object contains many SliceArc objects).
The draw() function is significantly updated, with the main change being that each slice needs to be updated as the chart is drawn, rather than each segment (essentially, more looping).
The SegmentArc.draw() function is overridden, and now draws individual slices in a segment rather than the whole segment in one call. (I think this logic should be moved to the SliceArc call itself, which may make it possible to call the original Arc.draw() function without modifying it)
Tooltips were particuarly challenging to fix, because the existing inRange() and tooltipPosition() functions are very much designed around the location and position of the segment, and not slices. To avoid modifying the showTooltip() function (which is relatively large and in Chart.Core.js [called here]), I pass a slice object and make it look like a segment object (this is more than a little hacky).
Both share the same base class, so this in some ways reflects the extensible design of chart.js, but it involved adding some state to the slices that I don’t think would have otherwise been duplicated from the segment object.
Finally, I updated the tooltips template, which now iterates through slices as well as segments, and highlights slices on hover rather than segments (an implementation of which is shown in the spie-enhanced.html example).
Thoughts on Solution
Now that I have a better understanding of the structure of the codebase, I think I could do a better job modularizing certain functions and re-using existing core components. That said, i’m impressed how easy it was to create the new chart, and how little new code had to be written.
I thought about trying to allow the chart to behave like a multi-level pie chart, but there are already solutions for this, and adding an extra dimension would make things even messier.