Skip to content

Data Dimension Change

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 * as gui from '@babylonjs/gui';
import { Scene, HemisphericLight, ArcRotateCamera, Vector3, SineEase } from '@babylonjs/core';
import iris from './data/iris.json' assert {type: 'json'};  //Our data

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

  //Babylon boilerplate
  const scene = new Scene(engine);
  new HemisphericLight('light1', new Vector3(0, 10, 0), scene);
  const camera = new ArcRotateCamera("Camera", -(Math.PI / 4) * 3, Math.PI / 4, 10, new Vector3(0, 0, 0), scene);
  camera.wheelPrecision = 20;
  camera.minZ = 0;
  camera.attachControl(true);
  camera.position = new Vector3(2, 2.5, -3);

  //Scaling functions
  let scaleC = d3.scaleOrdinal(anu.ordinalChromatic('d310').toStandardMaterial());

  //Create Center of Transform
  let CoT = anu.create("cot", "cot");
  let chart = anu.selectName("cot", scene);

  //Create spheres
  let spheres = chart.bind('sphere', { diameter: 0.05 }, iris)
                      .position((d) => Vector3.Zero())        //Set the position of the spheres before any transition has begun
                      .material((d) => scaleC(d.species));    //We need to use the arrow function here despite setting a uniform value, otherwise the animation will not work properly

  //Create a variable for the scatterplot axes so that we can access it easily
  let axes;

  //Create our functions which will change the scatterplot to two hardcoded states
  //Create our easing function, see https://doc.babylonjs.com/features/featuresDeepDive/animation/advanced_animations#easing-functions
  let sineEase = new SineEase();
  sineEase.setEasingMode(2);

  function changeState1() {
    //Create scales for this state
    let scaleX = d3.scaleLinear().domain(d3.extent(d3.map(iris, (d) => {return d.sepalLength}))).range([-1,1]).nice();
    let scaleY = d3.scaleLinear().domain(d3.extent(d3.map(iris, (d) => {return d.sepalWidth}))).range([-1,1]).nice();
    let scaleZ = d3.scaleLinear().domain(d3.extent(d3.map(iris, (d) => {return d.petalLength}))).range([-1,1]).nice();

    spheres.transition((d,n,i) => ({        //Start the transition, arrow function allows us to customize transition properties per each Mesh in our selection
            duration: 2000,                 //Duration of the animation in milliseconds
            loopMode: 0,                    //0: no loop, 1: loop
            delay: 0,                       //Delay to apply to this Mesh's animation in milliseconds
            easingFunction: sineEase,       //Set our EasingFunction
            onAnimationEnd: () => {}        //Callback function when the animation on this Mesh ends
            }))
            .positionX((d,n,i) => scaleX(d.sepalLength))
            .positionY((d,n,i) => scaleY(d.sepalWidth))
            .positionZ((d,n,i) => scaleZ(d.petalLength));
    

    //Destroy the previous axes if it exists
    axes?.CoT.dispose();
    //Create axes
    axes = anu.createAxes('axes', scene, { parent: chart, scale: { x: scaleX, y: scaleY, z: scaleZ } });
  }

  //Same as above but with different data dimensions
  function changeState2() {
    let scaleX = d3.scaleLinear().domain(d3.extent(d3.map(iris, (d) => {return d.sepalWidth}))).range([-1,1]).nice();
    let scaleY = d3.scaleLinear().domain(d3.extent(d3.map(iris, (d) => {return d.sepalLength}))).range([-1,1]).nice();
    let scaleZ = d3.scaleLinear().domain(d3.extent(d3.map(iris, (d) => {return d.petalWidth}))).range([-1,1]).nice();

    spheres.transition((d,n,i) => ({
            duration: 2000,
            loopMode: 0,
            delay: 0,
            easingFunction: sineEase,
            onAnimationEnd: () => {}
            }))
            .positionX((d,n,i) => scaleX(d.sepalWidth))
            .positionY((d,n,i) => scaleY(d.sepalLength))
            .positionZ((d,n,i) => scaleZ(d.petalWidth));
            
    //Destroy the previous axes if it exists
    axes?.CoT.dispose();

    //Create axes
    axes = anu.createAxes('axes', scene, { parent: chart, scale: { x: scaleX, y: scaleY, z: scaleZ } });
  }

  //Call our function to initially change the data dimensions of the scatterplot
  changeState1();


  //Create a 2D GUI with buttons that, when clicked, will change our scatterplot between the two states
  let advancedTexture = gui.AdvancedDynamicTexture.CreateFullscreenUI("UI");

  let rect1 = gui.Button.CreateSimpleButton("button1", "State 1");
  rect1.width = 0.1;
  rect1.height = "40px";
  rect1.cornerRadius = 2;
  rect1.color = "white";
  rect1.thickness = 4;
  rect1.background = "blue";
  rect1.top = "30%";
  rect1.left = "-25%";
  rect1.onPointerClickObservable.add(() => {
      changeState1();
  });

  let rect2 = gui.Button.CreateSimpleButton("button2", "State 2");
  rect2.width = 0.1;
  rect2.height = "40px";
  rect2.cornerRadius = 2;
  rect2.color = "white";
  rect2.thickness = 4;
  rect2.background = "blue";
  rect2.top = "30%";
  rect2.left = "25%";
  rect2.onPointerClickObservable.add(() => {
      changeState2();
  });

  advancedTexture.addControl(rect1);
  advancedTexture.addControl(rect2);

  return scene;
}