Implementing a rotating pie chart in Highcharts

Motivation

In my workplace, we use Highcharts as a charting library. Recently my product owner came up with this chart design which inspired me to write this post.

So, the purpose of the designed chart is to present interactively two levels of hierarchical data. The first level is supposed to be a simple pie chart. The only addition is the possibility of selecting one of the data points to get the second level. On such selection, a stacked column with details should be displayed right next to the pie chart. So it is something which can be called the bar of pie chart on steroids ("Bar of Pie Chart" is Excel name for the static version of this design).

The design sparked up some discussion in our team. Long story short, we dismissed it as overcomplicated, especially taking into account that:

But despite that it was abandoned, I still wanted to check how much it would take to implement this design. Turns out not that much :)

Project setup - angular-highcharts

For this project I am using Stackblitz Angular template. Why Angular? Considering that I am working on an Angular app in my job, it was natural for me to go that route (who knows, maybe I will need some parts of it someday). So the example is Angular with a little RxJS sprinkled here and there, but it should not be a problem to recreate this in vanilla JS/TS or any other frontend framework.

The only dependency which I am using for this part is highcharts-angular - a simple component provided by Highcharts team, which wraps common Angular-Highcharts interaction logic. It is not necessary but it helps to keep things DRY.

Implementation

With no further ado, here is working example:

Hello

The full code can be found in this GitHub repository or corresponding Stackblitz environment. I won't go in details through basic Highcharts and Angular usage but there are few key points worth discussing.

General structure

Our solution is composed of two Highcharts charts (pie and column) side by side in a flex container:

<div class="container">
  <highcharts-chart
    [Highcharts]="Highcharts"
    [options]="options$ | async"
  ></highcharts-chart>
  <highcharts-chart
    class="drilldown-chart"
    [Highcharts]="Highcharts"
    [options]="drilldownOptions$ | async"
    [oneToOne]="true"
  ></highcharts-chart>
</div>

Both charts are provided with options via input. We use Observable and async pipe to be able to recompute options asynchronously on user interactions (selection of data point or legend item toggle).

Rotating pie chart to correct position

It turns out that Highcharts already provides a startAngle option for pie charts. From the documentation:

(property) PlotPieOptions.startAngle?: number

(Highcharts) The start angle of the pie slices in degrees where 0 is top and 90 right.

The question is, what angle do we need to rotate chart for the desired effect when selection is made?

First we need to take into account how Highcharts is positioning data points on pie charts. Obviously, each data point is allocated with pie segment proportionally to its value. The first data point segment starts at angle 0 (which is top) and goes in clockwise direction until it reaches allocated angle. It is followed by next data point, and so on untill all data points are rendered.

Next lets establish target state. We want the middle of selected data point segment to be exactly at 90 (which is right).

Knowing start and target layout we can describe transition as a series of rotations:

  • rotate countercloclwise by the angle allocated for each of the data points preceeding selected one - now selected data point start is at 0
  • rotate counterclockwise by half of the angle corresponding to selected data point - now middle point of selected data point is at 0
  • rotate clockwise by 90 - middle point of selected data point reaches 90 which is our desired position

Fortunately we don't need to execute all of the rotations separately. They can be superposed together which means we can just sum rotation angles from all above steps (counterclockwise will have negative values) and we will get single rotation equivalent.

Angle calculation code:

recalculateAngle(point: Highcharts.Point): void {
  const targetAngle = 90;
  const preceedingAngle = point.series.points
    .filter(p => p.visible)
    .filter(p => p.index < point.index) // preceeding points
    .map(p => p.percentage)
    .map(AppComponent.percentageToDegree)
    .reduce((a, b) => a + b, 0);
  const pointMiddleAngle =
    AppComponent.percentageToDegree(point.percentage) / 2;
  const startAngle = targetAngle - pointMiddleAngle - preceedingAngle;
  this.startAngle$.next(startAngle);
}

private static percentageToDegree(percentage: number): number {
  return (percentage / 100) * 360;
}

Reacting to user interactions

There are two possible ways of user interacting with the chart:

  • selecting data point on a pie chart
  • toggling visibility of data point by clicking it in the chart legend

On former we need to rotate pie chart and build proper options to be passed as an input to drilldown column chart. On latter we should recalculate rotation angle (as segments change their allocated size). For that we will define and attach callbacks on point events. Highchart chart options (only relevant part):

{
  chart: {
    type: 'pie'
  },
  [...]
  plotOptions: {
    pie: {
      allowPointSelect: true,
      point: {
        events: {
          select() {
            _this.recalculateAngle(this);
            _this.selectedPoint$.next(this);
          },
          legendItemClick() {
            const selectedPoint = this.series.points.find(p => p.selected);
            if (selectedPoint) {
              setTimeout(() => _this.recalculateAngle(selectedPoint));
            }
          }
        }
      }
    }
  },
  [...]
};

Pay attention to the use of setTimeout in second callback. Due to nature of Angular change detection mechanism it is necessary to schedule macro task here, otherwise changes will not be detected and we won't see changes to the view. Scheduling macro task makes it

Summary

I realize that this implementation is far from perfect but it is just a proof of concept. It shows that such design is achievable in Highcharts with very little custom coding. I've tried to extract and describe here only those crucial bits, rest is pretty standard Highcharts.

I do hope that this article at some point will be helpful for someone when confronted with a task for visualizing hierarchical data.