Brushing and Linking Filtering
Basic example of one approach to enabling filtering with brushing and linking. Drag and release the red box to filter along the right scatter plot's y-axis.
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 data from './data/cars.json' assert {type: 'json'};
//create and export a function that takes a babylon engine and returns a scene
export const brushingLinkingFilter = function(engine){
//Create an empty Scene
const scene = new BABYLON.Scene(engine);
//Add some lighting
const fillLight = new BABYLON.HemisphericLight('fillLight', new BABYLON.Vector3(0, 1, 0), scene);
fillLight.intensity = 1.25;
fillLight.groundColor = new BABYLON.Color3(0.5, 0.5, 0.5);
//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, 0.5, -5);
camera.wheelPrecision = 20;
camera.minZ = 0;
camera.attachControl(true);
//Create a scatter plot and put it on the left
let scaleX1 = d3.scaleLinear().domain(d3.extent(d3.map(data, (d) => d.Miles_per_Gallon))).range([-1,1]).nice();
let scaleY1 = d3.scaleLinear().domain(d3.extent(d3.map(data, (d) => d.Weight_in_lbs))).range([-1,1]).nice();
let scaleZ1 = d3.scaleLinear().domain(d3.extent(d3.map(data, (d) => d.Acceleration))).range([-1,1]).nice();
let scaleC = d3.scaleOrdinal(anu.ordinalChromatic('d310').toStandardMaterial());
let CoT1 = anu.create('cot', 'cot1');
let chart1 = anu.selectName('cot1', scene);
let spheres1 = chart1.bind('sphere', { diameter: 0.075 }, data)
.position((d) => new BABYLON.Vector3(scaleX1(d.Miles_per_Gallon), scaleY1(d.Weight_in_lbs), scaleZ1(d.Acceleration)))
.material((d) => scaleC(d.Origin));
let axes1 = anu.createAxes('myAxes1', { scale: { x: scaleX1, y: scaleY1, z: scaleZ1 }, parent: chart1 });
chart1.position(new BABYLON.Vector3(-1.25, 0, 0));
//Create a scatter plot and put it on the right
let scaleX2 = d3.scaleLinear().domain(d3.extent(d3.map(data, (d) => d.Cylinders))).range([-1,1]).nice();
let scaleY2 = d3.scaleLinear().domain(d3.extent(d3.map(data, (d) => d.Displacement))).range([-1,1]).nice();
let scaleZ2 = d3.scaleLinear().domain(d3.extent(d3.map(data, (d) => d.Horsepower))).range([-1,1]).nice();
let CoT2 = anu.create('cot', 'cot2');
let chart2 = anu.selectName('cot2', scene);
let spheres2 = chart2.bind('sphere', { diameter: 0.075 }, data)
.position((d) => new BABYLON.Vector3(scaleX2(d.Cylinders), scaleY2(d.Displacement), scaleZ2(d.Horsepower)))
.material((d) => scaleC(d.Origin));
let axes2 = anu.createAxes('myAxes2', { scale: { x: scaleX2, y: scaleY2, z: scaleZ2 }, parent: chart2 });
chart2.position(new BABYLON.Vector3(1.5, 0, 0));
//Lets create simple UI to filter the data across both charts based on our Y scale on Chart 2
let allSpheres = anu.selectName('sphere', scene) //First lets select all our data points, in this case all meshed named 'sphere'
axes2.background.y.bind("box", {size: 0.15}) //Lets create a box to be our filter UI
.material(new BABYLON.StandardMaterial()) //Give it a material and color
.diffuseColor(BABYLON.Color3.Red)
.positionY(() => scaleY2.range()[1]) //And set its default position to the top of our y axes
.positionX(() => scaleZ2.range()[1])
.behavior((d,n,i) => { //Now lets create a behavior to handle our filter interaction
// Create a pointer drag behavior along the y axis
let behavior = new BABYLON.PointerDragBehavior({ dragAxis: new BABYLON.Vector3(0, 1, 0) })
behavior.moveAttached = false; // We will disable the default movement to hand it ourselves
// add a on drag observable to handle movement but clamp it to the range of our Y scale
behavior.onDragObservable.add((event) => {
const newY = n.position.y + event.delta.y; //Calculate how far to move the mesh
n.position.y = BABYLON.Scalar.Clamp(newY, scaleY2.range()[0], scaleY2.range()[1]) //Clamp that value to our range
});
// now add a on drag end observable to hand filtering, we could do this on move but it could fire this behavior too much and slow down our scene
behavior.onDragEndObservable.add((event) => {
allSpheres.prop('isVisible', true) //first set all nodes to visible again to reset meshes that should be visible
//then filter the selection of meshes we want to hide
let filtered = allSpheres.filter((d) => d.Displacement >= scaleY2.invert(n.position.y)) //using invert on our scale lets us go from range to domain
filtered.prop('isVisible', false) //set that filtered selection is invisible
});
return behavior //set the behavior on our cube and done!
})
return scene;
};