Multiple Selection for Brushing and Linking
js
//Import everything we need to create our babylon scene and write our visualization code.
import * as anu from '@jpmorganchase/anu';
import * as d3 from "d3";
import cars from './data/cars.json' assert {type: 'json'}; //Our data
import { HemisphericLight, Vector3, Scene, ArcRotateCamera, PointerEventTypes, Color3, StandardMaterial} from '@babylonjs/core';
export const brushingLinkingMultiple = function (engine) {
const scene = new Scene(engine);
new HemisphericLight('light1', new Vector3(0, 10, 0), scene);
const camera = new ArcRotateCamera("Camera", (Math.PI / 2) * 3, Math.PI / 2, 6, new Vector3(0, 0, 0), scene);
camera.wheelPrecision = 12;
camera.attachControl(true);
//Add a unique index to each car so that we can easily retrieve this later
cars.forEach((element, index) => element.index = index);
//Left 2D scatterplot
let scaleX1 = d3.scaleLinear().domain(d3.extent(d3.map(cars, (d) => {return d.Miles_per_Gallon}))).range([-1,1]).nice();
let scaleY1 = d3.scaleLinear().domain(d3.extent(d3.map(cars, (d) => {return 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.05 }, cars)
.positionX((d) => scaleX1(d.Miles_per_Gallon))
.positionY((d) => scaleY1(d.Acceleration))
.material((d) => scaleC(d.Origin))
.prop('outlineWidth', 0.0075);
anu.createAxes('chart1', scene, { parent: chart1, scale: { x: scaleX1, y: scaleY1 } });
CoT1.position = new Vector3(-1.3, 0, 0);
//Right 3D scatterplot
let scaleX2 = d3.scaleLinear().domain(d3.extent(d3.map(cars, (d) => {return d.Horsepower}))).range([-1,1]).nice();
let scaleY2 = d3.scaleLinear().domain(d3.extent(d3.map(cars, (d) => {return d.Weight_in_lbs}))).range([-1, 1]).nice();
let scaleZ2 = d3.scaleLinear().domain(d3.extent(d3.map(cars, (d) => {return d.Displacement}))).range([-1,1]).nice();
let CoT2 = anu.create("cot", "cot2");
let chart2 = anu.selectName("cot2", scene);
let spheres2 = chart2.bind('sphere', { diameter: 0.075 }, cars)
.positionX((d) => scaleX2(d.Horsepower))
.positionY((d) => scaleY2(d.Weight_in_lbs))
.positionZ((d) => scaleZ2(d.Displacement))
.material((d) => scaleC(d.Origin))
.prop('outlineWidth', 0.01);
anu.createAxes('chart2', scene, { parent: chart2, scale: { x: scaleX2, y: scaleY2, z: scaleZ2 } });
CoT2.position = new Vector3(1.3, 0, 0);
//Create brush for the 2D scatterplot
let brush1 = anu.createBrush('brush1', scene,
{
parent: chart1, //The chart that the brush is bound to, must be set
scales: { x: scaleX1, y: scaleY1 }, //The scales of this chart which are used to determine ranges that the brush can move in, at least one must be set
padding: { x: 0.1, y: 0.1 }, //Adds padding to the specified ranges that the brush can move in, defaults to 0
rotateAxes: { x: false, y: false, z: false } //Allows or disallows the brush to be rotated along the specified axes, has sensible defaults depending on the scales set
}
);
//Create brush for the 3D scatterplot
//We can also create a material to pass into the brush
let mat = new StandardMaterial('myBrushMaterial');
mat.diffuseColor = Color3.Yellow();
mat.alpha = 0.4;
let brush2 = anu.createBrush('brush2', scene,
{
parent: chart2,
scales: { x: scaleX2, y: scaleY2, z: scaleZ2 },
minSize: { x: 0.5, y: 0.5, z: 0.5 }, //The minimum size of the brush along each axis, has sensible defaults depending on the scales set
material: mat //Assign our material
}
);
//Create observables to respond to brush events
//Get a list of all of our spheres which we will then use to find linked marks
let allSpheres = new anu.Selection([...spheres1.selected, ...spheres2.selected]);
brush1.onBrushChangedObservable.add((evt) => {
let addedIndices = evt.added.map(n => n.metadata.data.index); //Retrieve indices of newly highlighted points via evt.added
allSpheres.filter((d,n,i) => addedIndices.includes(d.index)) //Filter our selection down to these indices
.prop('renderOutline', true); //Change appearance
let removedIndices = evt.removed.map(n => n.metadata.data.index); //Retrieve indices of newly unhighlighted points via evt.removed
allSpheres.filter((d,n,i) => removedIndices.includes(d.index)) //Filter our selection down to these indices
.prop('renderOutline', false) //Change appearance
});
brush2.onBrushChangedObservable.add((evt) => {
let addedIndices = evt.added.map(n => n.metadata.data.index);
allSpheres.filter((d,n,i) => addedIndices.includes(d.index)) //Brushed marks can also be accessed via evt.brushed,
.transition((d,n,i) => ({ duration: 200 })) //but evt.added and removed makes performing transitions easier
.scaling(new Vector3(1.5, 1.5, 1.5));
let removedIndices = evt.removed.map(n => n.metadata.data.index);
allSpheres.filter((d,n,i) => removedIndices.includes(d.index))
.transition((d,n,i) => ({ duration: 200 }))
.scaling(Vector3.One());
});
return scene;
};