Skip to content

3D Line Chart

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

import * as anu from '@jpmorganchase/anu';
import * as d3 from "d3";
import { Scene, HemisphericLight, ArcRotateCamera, Vector3, Color3, Mesh, VertexBuffer } from '@babylonjs/core';
import data from './data/yield-curve.csv';  //Our data

//Create and export a function that takes a Babylon engine and returns a Babylon Scene
export function linechart3D(engine) {

  //Create an empty Scene
  const scene = new Scene(engine);

  //Add some lighting
  new HemisphericLight('light1', new Vector3(0, 10, 0), scene);

  //Add a camera that rotates around the origin and adjust its properties
  const camera = new ArcRotateCamera("Camera", -(Math.PI / 4) * 3, Math.PI / 4, 10, new Vector3(0, 0, 0), scene);
  camera.wheelPrecision = 20; // Adjust the sensitivity of the mouse wheel's zooming
  camera.minZ = 0;            // Adjust the distance of the camera's near plane
  camera.attachControl(true); // Allow the camera to respond to user controls
  camera.position = new Vector3(5, 0, -6);

  //Specify the columns in our dataset that should each be its own line
  let years = ["1 Yr", "2 Yr", "3 Yr", "5 Yr", "7 Yr", "10 Yr"];

  //Create D3 functions to parse the time and date
  let parseTime = d3.timeParse("%m/%d/%Y");
  let dateFormat = d3.timeFormat("'%y");

  //Get all of the dates in our dataset into an array so that we can easily get its extents later
  let dates = data.map((d) => parseTime(d.Date))

  //Create the D3 functions that we will use to scale our data dimensions to desired output ranges for our visualization
  //In this case, we create scale functions that correspond to the x, y, and z positions and color
  let scaleX = d3.scaleTime().domain(d3.extent(dates)).range([-3, 3]);
  let scaleY = d3.scaleLinear().domain([0, 9]).range([-1, 1]).nice();
  let scaleZ = d3.scalePoint().domain(years).range([-2, 2]);
  let scaleC = d3.scaleSequential(d3.interpolateBlues).domain([1, -1]);

  //For each column/year/line, map it to its x, y, and z position along each timestep using the D3 scale functions
  let paths = years.map((col) => {
    return data.map((row)=> new Vector3(scaleX(parseTime(row.Date)),
                                        scaleY(row[col]),
                                        scaleZ(col)));
  });

  //Bind a new CoT
  let CoT = anu.bind("cot");

  //Bind a new ribbon mesh to the CoT, which we will need to update manually to create our 3D line chart
  let ribbon = CoT.bind("ribbon", { pathArray: paths, updatable: true, sideOrientation: Mesh.DOUBLESIDE })
                   .selected[0];  // Get the Babylon Mesh that Anu had created for us, which had been stored in this selected property as an element of an array

  //The ribbon already has our position values, but we now need to set its color values for each vertex in its mesh (data row)
  //Retrieve the VertexBuffer of position values of the ribbon, these are stored as Numbers in a flat array [x0, y0, z0, x1, y1, z1, ...]
  let positions = ribbon.getVerticesData(VertexBuffer.PositionKind);
  let colors = [];

  //Loop through our position buffer
  for (let p = 0; p < positions.length; p += 3) {
    //Get the color that this vertex should have based on its y-axis value
    let colorString = scaleC(positions[p + 1]);
    //Our scaleC function, which is from D3, returns a string in the format 'rgb(r, g, b)', so we need to parse this
    let color = colorString.substring(4, colorString.length - 1)
                           .replace(/ /g, "")
                           .split(",");
    //Store our new color
    colors.push(color[0] / 255, color[1] / 255, color[2] / 255, 1);
  }

  //Set our new color values to the ribbon
  ribbon.setVerticesData(VertexBuffer.ColorKind, colors);
  //Turn off picking to improve performance of our complex mesh geometry
  ribbon.isPickable = false;

  //Use the createAxes() Anu helper function to create the axes for us based on our D3 scale functions
  //Also adjust its visual properties to properly format the axes labels
  anu.createAxes("test", scene, { parent: CoT,
                                  scale: { x: scaleX, y: scaleY, z: scaleZ },
                                  domainMaterialOptions: { "color": Color3.Black(), width: 5 },
                                  gridTicks: { x: scaleX.ticks(d3.timeYear.every(2)) },
                                  labelTicks: { x: scaleX.ticks(d3.timeYear.every(2)) },
                                  labelFormat: { x: dateFormat, y: (text) => (text === undefined) ? "0%" : text + "%" }
  });

  //Add some additional white lines for each line (column)
  let whiteLines = CoT.bind("lineSystem", { lines: paths })
                      .attr("color", Color3.White())
                      .prop("alpha", 0.5);

  //Add an additional black line to the front-most line
  let blackOutline = CoT.bind("lines", { points: paths[0] })
                        .attr("color", Color3.Black());

  return scene;
}