Multiple Selection for Brushing and Linking
Demonstration of the Brush Prefab used to brush and link regions on two charts. Brushes can be dragged and scaled. The left brush highlights using an outline, the right brush highlights using a solid color.
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 Babylon Scene
export const brushingLinkingMultiple = function (engine) {
//Create an empty Scene
const scene = new BABYLON.Scene(engine);
//Add some lighting
new BABYLON.HemisphericLight('light1', new BABYLON.Vector3(0, 10, 0), scene);
//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);
//Add a unique index to each car so that we can easily retrieve this later
data.forEach((element, index) => element.index = index);
//Create a 2D 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.Acceleration))).range([-1,1]).nice();
let scaleC = d3.scaleOrdinal(anu.ordinalChromatic('d310').toColor3());
let CoT1 = anu.create('cot', 'cot1');
let chart1 = anu.selectName('cot1', scene);
let spheres1 = chart1.bind('sphere', { diameter: 0.05 }, data)
.position((d) => new BABYLON.Vector3(scaleX1(d.Miles_per_Gallon), scaleY1(d.Acceleration), 0))
.material((d) => new BABYLON.StandardMaterial())
.diffuseColor((d) => scaleC(d.Origin))
.prop('outlineWidth', 0.0075);
anu.createAxes('myAxes1', { scale: { x: scaleX1, y: scaleY1 }, parent: chart1 });
chart1.position(new BABYLON.Vector3(-1.3, 0, 0));
//Create a 3D 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) => new BABYLON.StandardMaterial())
.diffuseColor((d) => scaleC(d.Origin))
.prop('outlineWidth', 0.0075);
anu.createAxes('myAxes2', { scale: { x: scaleX2, y: scaleY2, z: scaleZ2 }, parent: chart2 });
chart2.position(new BABYLON.Vector3(1.3, 0, 0));
//Use the Brush prefab and add it to our 2D scatter plot
let brush1 = anu.createBrush('brush1',
{
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
}
);
//Use the Brush prefab and add it to our 3D scatter plot, here we also pass in a material to color the brush
let mat = new BABYLON.StandardMaterial('myBrushMaterial');
mat.diffuseColor = BABYLON.Color3.Yellow();
mat.alpha = 0.2;
let brush2 = anu.createBrush('brush2',
{
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
}
);
//Make a new selection with all the spheres
let allSpheres = anu.selectName('sphere', scene);
//Subscribe to our 2D scatter plot brush's observable which updates whenever the brushed meshes change
brush1.onBrushChangedObservable.add((evt) => {
//Retrieve our manually bound indices of newly highlighted meshes via evt.added
let addedIndices = evt.added.map(n => n.metadata.data.index);
//Filter all our spheres to just the ones with the above indices
allSpheres.filter((d,n,i) => addedIndices.includes(d.index))
.prop('renderOutline', true);
//Do the same thing but instead with the newly unhighlighted meshes via evt.removed
let removedIndices = evt.removed.map(n => n.metadata.data.index);
allSpheres.filter((d,n,i) => removedIndices.includes(d.index))
.prop('renderOutline', false);
});
//Subscribe to our 2D scatter plot brush's observable which updates whenever the brushed meshes change
brush2.onBrushChangedObservable.add((evt) => {
//Retrieve our manually bound indices of newly highlighted meshes via evt.added
let addedIndices = evt.added.map(n => n.metadata.data.index);
//Filter all our spheres to just the ones with the above indices
allSpheres.filter((d,n,i) => addedIndices.includes(d.index))
.diffuseColor(BABYLON.Color3.Yellow())
.emissiveColor(BABYLON.Color3.Yellow());
//Do the same thing but instead with the newly unhighlighted meshes via evt.removed
let removedIndices = evt.removed.map(n => n.metadata.data.index);
allSpheres.filter((d,n,i) => removedIndices.includes(d.index))
.diffuseColor((d) => scaleC(d.Origin))
.emissiveColor(BABYLON.Color3.Black());
});
return scene;
};