Skip to content

Layouts

Overview

The Layout prefabs provide a quick way to generate 3D layout for a selection of 3D visualizations created by Anu.

Usage

js
//name (required), options (optional), scene (required) 

//returns instance of a layout that arranges a selection of 3D visualizations on a 2D flat grid 
let layout = anu.planeLayout(name: String, options?: {}, scene: BABYLON.Scene);

//returns instance of a layout that arranges a selection of 3D visualizations on a 3D cylindrical grid 
let layout = anu.cylinderLayout(name: String, options?: {}, scene: BABYLON.Scene);

//returns instance of n layout that arranges a selection of 3D visualizations on a 3D spherical grid 
let layout = anu.sphereLayout(name: String, options?: {}, scene: BABYLON.Scene);

//Change the row number and update the layout
layout.attr("row", val: number)

//Change the margin and update the layout
layout.attr("margin", val: BABYLON.Vector2)

//Change the radius and update the layout
layout.attr("radius", val: number)

//Update the layout now
//Can be called right after the Selection or Option changes
layout.update();

Options

PropertyValueDefault
selection(anu.Selection) a list of nodes in the Babylon scene-graph to be added to the layout, one cell per node. The expected usage is to pass a selection of root nodes for each chart.null
rows(number) the number of rows for the layout1
radius(number) the radius of the layout, only applicable to cylindrical/spherical layouts5
margin(Vector2) the margin of the layout in x and y dimensions(0, 0)

Methods and Properties

Property / MethodDescription
root(Mesh) the parent TransformNode of the layout
currentLayout(number) the current layout created (1 = plane; 2 = cylinder; 3 = sphere)
attrquickly change an option parameter and update the layout
updateupdate the layout

Examples

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, StandardMaterial, Vector2, Vector3, Color3 } from '@babylonjs/core';
import * as gui from '@babylonjs/gui'
import cars from './data/cars.json' assert {type: 'json'};


export function layout(engine) {

  const scene = new Scene(engine);
  new HemisphericLight('light1', new Vector3(0, 1, 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(-3, 3, -5);

  //Generate 14 charts to begin with
  //Randomly create 2D and 3D charts at 50/50 chance
  let allcharts = [];
  for (let i = 0; i < 15; i++) {
      let n = Math.random();
      if(n > 0.5){
          let chart = make2Dchart(scene, Math.random() * 100);
          allcharts.push(chart);
      } else {
          let chart = make3Dchart(scene, Math.random() * 100);
          allcharts.push(chart);
      }
  }

  //Make an Anu selection of all meshes that have the name 'cot' (in this case, our charts we just created)
  let charts = anu.selectName('cot', scene)
                  .scalingX((d) => Math.max(Math.random(), .5) * 2) //Add some size variation to these charts
                  .scalingY((d) => Math.max(Math.random(), .5) * 2)
                  .scalingZ((d) => Math.max(Math.random(), .4) * 2)

  //Layout variables
  let rows = 3;
  let curve = 20;
  let margin = new Vector2(20, 5);

  //Create the layout, specify the layout type, parent name, and layout configurations
  let layout = new anu.cylinderLayout('Layout', { selection: charts, rows: rows, margin: new Vector2(20, 5), radius: 20 }, scene)
                      .attr("row", 2);
  //Resize the entire layout to make it more easily visible in a 1x1x1 area
  layout.root.normalizeToUnitCube();

  //Example functions to update the configurations of the layout, such as curvature, row number, margins
  let changeRow = function(rownum) {
      rows = rownum;
      layout.attr("row", rows);
  }

  let changeCurve = function(radius) {
      curve = radius;
      layout.attr("radius", curve);
  }

  let updateMarginX = function(val) {
      margin.x = val;
      layout.attr("margin", margin);
  }

  let updateMarginY = function(val) {
      margin.y = val;
      layout.attr("margin", margin);
  }

  let displayValue = function(value) {
      return Math.floor(value);
  }

  let addChart = function () {
      let chartnew = make3Dchart(scene, 0)
                      .scalingX((d) => Math.max(Math.random(), .5) * 2) //Add some size variation
                      .scalingY((d) => Math.max(Math.random(), .5) * 2)
                      .scalingZ((d) => Math.max(Math.random(), .4) * 2)
      allcharts.push(chartnew);
      charts = anu.selectName('cot', scene);  //Update our Anu selection
      layout.options.selection = charts;
      layout.update();
  }

  let removeChart = function() {
      if(allcharts.length == 0)
          return;

      allcharts[allcharts.length - 1].dispose();
      allcharts.pop();
      charts = anu.selectName('cot', scene);
      layout.options.selection = charts;
      layout.update();
  }

  let setLayout = function(val) {
      switch(val) {
          case 0:
            layout.planeLayout();
            break;
          case 1:
            layout.cylinderLayout();
            break;
          case 2:
            layout.sphereLayout();
            break;
      }
  }

  //Make the Babylon UI that will allow the user to change the layout
  let layoutGroup = new gui.RadioGroup("Layout");
  layoutGroup.addRadio("Plane", setLayout);
  layoutGroup.addRadio("Cylinder", setLayout, true);
  layoutGroup.addRadio("Sphere", setLayout);

  let rotateGroup = new gui.SliderGroup("Config", "S");
  rotateGroup.addSlider("row", changeRow, "rows", 1, 6, 3, displayValue);

  let curvature = new gui.SliderGroup("Curvature", "S");
  curvature.addSlider("curvature", changeCurve, "units", 0, 80, 20, displayValue);

  let marginx = new gui.SliderGroup("MarginX", "S");
  marginx.addSlider("marginx", updateMarginX, "unit", 0, 60, 20, displayValue);

  let marginy = new gui.SliderGroup("MarginY", "S");
  marginy.addSlider("marginy", updateMarginY, "unit", 0, 20, 5, displayValue);

  let advancedTexture = gui.AdvancedDynamicTexture.CreateFullscreenUI("UI");

  let selectBox = new gui.SelectionPanel("sp", [rotateGroup, curvature, marginx, marginy, layoutGroup]);
  selectBox.width = 0.2;
  selectBox.height = 1.5;
  selectBox.scaleX = 0.6;
  selectBox.scaleY = 0.6;
  selectBox.background = "#FFFFFF";
  selectBox.horizontalAlignment = gui.Control.HORIZONTAL_ALIGNMENT_LEFT;
  selectBox.verticalAlignment = gui.Control.VERTICAL_ALIGNMENT_CENTER;
  selectBox.fontFamily = "times new roman";
  selectBox.fontSize = "14pt";

  let rect2 = gui.Button.CreateSimpleButton("button1", "add chart");
  rect2.width = 0.2; // 0.2 = 20%
  rect2.height = "40px";
  rect2.cornerRadius = 20;
  rect2.color = "white";
  rect2.thickness = 4;
  rect2.background = "blue";

  rect2.top = 200; //200 px
  rect2.left = "10%";
  rect2.onPointerClickObservable.add(() => {
      addChart();
  });

  let rect1 = gui.Button.CreateSimpleButton("button2", "remove chart");
  rect1.width = 0.2; // 0.2 = 20%
  rect1.height = "40px";
  rect1.cornerRadius = 20;
  rect1.color = "white";
  rect1.thickness = 4;
  rect1.background = "blue";

  rect1.top = 250; //200 px
  rect1.left = "10%";
  rect1.onPointerClickObservable.add(() => {
      removeChart();
  });
  advancedTexture.addControl(rect1);
  advancedTexture.addControl(rect2);
  advancedTexture.addControl(selectBox);

  return scene;
}

//Similar to 2D bar chart example
function make2Dchart(scene, id){

  const cylinders = [...new Set(cars.map(item => item.Cylinders))].sort();

  //Aggregate our data to the mean MPG and HP for two keys, origin and cylinders
  let carsRollup = d3.flatRollup(cars, (v) => { return {Horsepower: d3.mean(v, d => d.Horsepower),
                                                          Miles_per_Gallon: d3.mean(v, d => d.Miles_per_Gallon)}},
                                                          d => d.Cylinders)

  carsRollup = carsRollup.map(([Cylinders, Data]) => ({Cylinders, ...Data }));

  //Get Min/Max values for our linear scales
  const horsepowerMinMax = d3.extent([...new Set(carsRollup.map(item => item.Horsepower))])
  const MPGMinMax = d3.extent([...new Set(carsRollup.map(item => item.Miles_per_Gallon))])

  //Create our scales for positioning and coloring meshes
  let scaleX = d3.scaleBand().domain(cylinders).range([-2.5,2.5]).paddingInner(1).paddingOuter(0.5);
  let scaleY = d3.scaleLinear().domain(horsepowerMinMax).range([0,5]).nice();
  let scaleC = d3.scaleSequential(d3.interpolatePuBuGn).domain(MPGMinMax);

  //Create and select a transform node to be our parent
  let CoT = anu.create('cot', 'cot' + id)
  let chart = anu.selectName('cot' + id, scene);

  //Bind boxes to our rolled-up data, position, scale, and color with our scales
  let bars = chart.bind('plane', {height: 1, width: 0.8, sideOrientation:2}, carsRollup)
                  .positionX((d) => scaleX(d.Cylinders))
                  .positionZ(-0.01)
                  .scalingY((d) => scaleY(d.Horsepower))
                  .positionY((d) => scaleY(d.Horsepower) / 2)
                  .material((d, i) => new StandardMaterial("myMaterial", scene))
                  .diffuseColor((d) => { let rgb = scaleC(d.Miles_per_Gallon)
                                                    .replace(/[^\d,]/g, '')
                                                    .split(',')
                                                    .map((v) => v / 255)
                                          return new Color3(...rgb)})

  anu.createAxes('test', scene, { parent: chart, scale: { x: scaleX, y: scaleY } });

  chart.name('cot');

  return chart;
}

//Similar to 3D bar chart example
function make3Dchart(scene, id){

  //Get unique values for our categorical and ordinal scales
  const origin = [...new Set(cars.map(item => item.Origin))];
  const cylinders = [...new Set(cars.map(item => item.Cylinders))].sort().reverse();

  //Aggregate our data to the mean MPG and HP for two keys, origin and cylinders
  let  carsRollup = d3.flatRollup(cars, (v) => { return {Horsepower: d3.mean(v, d => d.Horsepower),
                                                          Miles_per_Gallon: d3.mean(v, d => d.Miles_per_Gallon)}},
                                                          d => d.Origin,
                                                          d => d.Cylinders)

  carsRollup = carsRollup.map(([Origin, Cylinders, Data]) => ({ Origin, Cylinders, ...Data }));

  //Get Min/Max values for our linear scales
  const horsepowerMinMax = d3.extent([...new Set(carsRollup.map(item => item.Horsepower))])
  const MPGMinMax = d3.extent([...new Set(carsRollup.map(item => item.Miles_per_Gallon))]).reverse()

  //Create our scales for positioning and coloring meshes
  let scaleX = d3.scaleBand().domain(cylinders).range([-2.5,2.5]).paddingInner(1).paddingOuter(0.5);
  let scaleY = d3.scaleLinear().domain(horsepowerMinMax).range([0,5]).nice();
  let scaleZ = d3.scaleBand().domain(origin).range([-2.5,2.5]).paddingInner(1).paddingOuter(0.5);
  let scaleC = d3.scaleSequential(anu.sequentialChromatic('OrRd').toPBRMaterialRough()).domain(MPGMinMax);

  //Create and select a transform node to be our parent
  let CoT = anu.create('cot', 'cot' + id)
  let chart = anu.selectName('cot' + id, scene);

  //Bind boxes to our rolled-up data, position, scale, and color with our scales
  let bars = chart.bind('box', {height: 1, width: 0.8, depth: 0.8}, carsRollup)
                  .positionX((d) => scaleX(d.Cylinders))
                  .positionZ((d) => scaleZ(d.Origin))
                  .scalingY((d) => scaleY(d.Horsepower))
                  .positionY((d) => scaleY(d.Horsepower) / 2)
                  .material((d, i) => scaleC(d.Miles_per_Gallon))
                  //.diffuseColor((d) => scaleC(d.Miles_per_Gallon))

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

  chart.name('cot');

  return chart;
}