Skip to content

Donut Chart

Basic donut chart using the pie() functions from D3. The code is functionally the same as that for the Pie Chart except with an additional step where the center of the pie is subtracted using Constructive Solid Geometry (CSG), which is a wrapper around the Manifold library.

js
// SPDX-License-Identifier: Apache-2.0
// Copyright : J.P. Morgan Chase & Co.

import * as anu from '@jpmorganchase/anu';
import * as BABYLON from '@babylonjs/core';
import * as d3 from 'd3';
import 'manifold-3d';   //Required for Constructive Solid Geometry

//Create and export a function that takes a Babylon engine and returns a Babylon Scene
export async function donutChart(engine){   //Mark this function as async

  //Create an empty Scene
  const scene = new BABYLON.Scene(engine);
  //Add some lighting
  new BABYLON.HemisphericLight('light1', new BABYLON.Vector3(0, 10, -5), scene);
  //Add a camera that rotates around the origin and adjust its properties
  const camera = new BABYLON.ArcRotateCamera('Camera', 0, 0, 0, new BABYLON.Vector3(0, 0, 0), scene);
  camera.position = new BABYLON.Vector3(0, 1.25, -1);
  camera.wheelPrecision = 20;
  camera.minZ = 0;
  camera.attachControl(true);
  
  //Create some fake data
  const data = [2, 3, 5, 7, 11, 13, 17, 19];

  //Create our D3 pie generator which will calculate the required angles for our pie chart
  const pie = d3.pie();
  //Create arcs for our data
  const arcs = pie(data);

  //Create a D3 scale function that will color each pie segment
  let scaleC = d3.scaleOrdinal(anu.ordinalChromatic('d310').toStandardMaterial());

  //Create a Center of Transform TransformNode that serves the parent node for all our meshes that make up our chart
  let CoT = anu.create('cot', 'cot');
  //Select our CoT so that we have it as a Selection object
  let chart = anu.selectName('cot', scene);

  //Create cylinder meshes as children of our CoT to form our pie segments based on the arcs we calculated
  let pieSegments = chart.bind('cylinder',
    {
      arc: (d,n,i) => (d.endAngle - d.startAngle) / (Math.PI * 2),  //Babylon cylinders use percentages, so we have to convert this value
      diameter: 1,
      height: 0.1,
      enclose: true
    },
    arcs)
    .rotationY((d,n,i) => d.startAngle - Math.PI / 2);   //Offset the rotation so that the segments "start" at 12 o'clock and not 3 o'clock


  //Initialize the CSG
  await BABYLON.InitializeCSG2Async();

  //Create an "inner" cylinder that will be used to subtractively create the hole in the center of the pie to form a donut
  const inner = anu.create('cylinder', 'cylinder', { diameter: 0.5 });
  //Create the inner cylinder's CSG representation, which is used for calculations
  const innerCSG = BABYLON.CSG2.FromMesh(inner);

  //Keep a list of all the new Meshes we are about to create
  const newMeshes = [];

  //For each pie segment...
  pieSegments.run((d,n,i) => {
    //Create this segment's CSG representation
    const segmentCSG = BABYLON.CSG2.FromMesh(n);
    //Subtract the inner cylinder from the pie segment
    const donutSegmentCSG = segmentCSG.subtract(innerCSG);
    //Create the mesh from the CSG result
    const newMesh = donutSegmentCSG.toMesh(n.name, scene, { centerMesh: false }); //Disable centerMesh to keep our original pivot point
    //Store the reference so that we can rebuild our Selection
    newMeshes.push(newMesh);

    //Cleanup, destroy the CSGs
    segmentCSG.dispose();
    donutSegmentCSG.dispose();
  })

  //Because we created new meshes, we can create a new Anu Selection in case we want to further manipulate them
  const donutSegments = new anu.Selection(newMeshes);   //Here we use the list of mesh references re created
  donutSegments.prop('parent', CoT)         //Re-set the parent
    .metadata('data', (d,n,i) => arcs[i])   //Re-set the data
    .material((d,n,i) => scaleC(i));        //Set the material to color the segments
    
  //Dispose of the original pie segments
  pieSegments.dispose();
  //Dispose of the inner cylinder and its CSG representation
  inner.dispose();
  innerCSG.dispose();

  return scene;
}