Published
Edited
Apr 29, 2022
12 forks
121 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
camera = {
let aspect = viz_width / height;
return new THREE.PerspectiveCamera(fov, aspect, near, far + 1);
}
Insert cell
Insert cell
Insert cell
scene = {
let scene = new THREE.Scene();
scene.background = new THREE.Color(0xefefef);
scene.add(points);
return scene;
}
Insert cell
Insert cell
Insert cell
function getScaleFromZ (camera_z_position) {
let half_fov = fov/2;
let half_fov_radians = toRadians(half_fov);
let half_fov_height = Math.tan(half_fov_radians) * camera_z_position;
let fov_height = half_fov_height * 2;
let scale = height / fov_height; // Divide visualization height by height derived from field of view
return scale;
}
Insert cell
Insert cell
function getZFromScale(scale) {
let half_fov = fov/2;
let half_fov_radians = toRadians(half_fov);
let scale_height = height / scale;
let camera_z_position = scale_height / (2 * Math.tan(half_fov_radians));
return camera_z_position;
}
Insert cell
Insert cell
function zoomHandler(d3_transform) {
let scale = d3_transform.k;
let x = -(d3_transform.x - viz_width/2) / scale;
let y = (d3_transform.y - height/2) / scale;
let z = getZFromScale(scale);
camera.position.set(x, y, z);
}
Insert cell
Insert cell
zoom = {
let d3_zoom = d3.zoom()
.scaleExtent([getScaleFromZ(far), getScaleFromZ(near)])
.on('zoom', () => {
let d3_transform = d3.event.transform;
zoomHandler(d3_transform);
});
return d3_zoom;
}
Insert cell
Insert cell
view = d3.select(renderer.domElement)
Insert cell
{
function setUpZoom() {
view.call(zoom);
let initial_scale = getScaleFromZ(far);
var initial_transform = d3.zoomIdentity.translate(viz_width/2, height/2).scale(initial_scale);
zoom.transform(view, initial_transform);
camera.position.set(0, 0, far);
}
setUpZoom();
return setUpZoom;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
circle_sprite= new THREE.TextureLoader().load(
"https://blog.fastforwardlabs.com/images/2018/02/circle-1518727951930.png"
)
Insert cell
Insert cell
Insert cell
circle_sprite_aa= new THREE.TextureLoader().load(
"https://blog.fastforwardlabs.com/images/2018/02/circle_aa-1518730700478.png"
)
Insert cell
sprite_settings = {
// Non anti-aliased settings
let circle = {
map: circle_sprite,
transparent: true
}
// Anti-aliased settings
let circle_aa = {
map: circle_sprite_aa,
transparent: true,
alphaTest: 0.5
}
// Switch out which settings object is returned to change settings
return circle;
}
Insert cell
Insert cell
Insert cell
points = {
let pointsGeometry = new THREE.Geometry();
let colors = [];
for (let datum of generated_points) {
// Set vector coordinates from data
let vertex = new THREE.Vector3(datum.position[0], datum.position[1], 0);
pointsGeometry.vertices.push(vertex);
let color = new THREE.Color(color_array[datum.group]);
colors.push(color);
}
pointsGeometry.colors = colors;
let pointsMaterial = new THREE.PointsMaterial({
size: 8,
sizeAttenuation: false,
vertexColors: THREE.VertexColors,
});
// Add settings from sprite_settings
for (let setting in sprite_settings) {
pointsMaterial[setting] = sprite_settings[setting];
}
return new THREE.Points(pointsGeometry, pointsMaterial);
}
Insert cell
Insert cell
Insert cell
Insert cell
raycaster = new THREE.Raycaster()
Insert cell
{
view.on("mousemove", () => {
let [mouseX, mouseY] = d3.mouse(view.node());
let mouse_position = [mouseX, mouseY];
checkIntersects(mouse_position);
});
return view;
}
Insert cell
Insert cell
function mouseToThree(mouseX, mouseY) {
return new THREE.Vector3(
mouseX / viz_width * 2 - 1,
-(mouseY / height) * 2 + 1,
1
);
}
Insert cell
Insert cell
function checkIntersects(mouse_position) {
let mouse_vector = mouseToThree(...mouse_position);
raycaster.setFromCamera(mouse_vector, camera);
let intersects = raycaster.intersectObject(points);
if (intersects[0]) {
let sorted_intersects = sortIntersectsByDistanceToRay(intersects);
let intersect = sorted_intersects[0];
let index = intersect.index;
let datum = generated_points[index];
highlightPoint(datum);
showTooltip(mouse_position, datum);
} else {
removeHighlights();
hideTooltip();
}
}
Insert cell
Insert cell
{
raycaster.params.Points.threshold = 1;
return raycaster;
}
Insert cell
Insert cell
function sortIntersectsByDistanceToRay(intersects) {
return _.sortBy(intersects, "distanceToRay");
}
Insert cell
Insert cell
hoverContainer = new THREE.Object3D()
Insert cell
{
scene.add(hoverContainer);
return scene;
}
Insert cell
Insert cell
function highlightPoint(datum) {
removeHighlights();
let geometry = new THREE.Geometry();
geometry.vertices.push(
new THREE.Vector3(
datum.position[0],
datum.position[1],
0
)
);
geometry.colors = [ new THREE.Color(color_array[datum.group]) ];

let material = new THREE.PointsMaterial({
size: 26,
sizeAttenuation: false,
vertexColors: THREE.VertexColors
});
// Add settings from sprite settings
for (let setting in sprite_settings) {
material[setting] = sprite_settings[setting];
}
let point = new THREE.Points(geometry, material);
hoverContainer.add(point);
}
Insert cell
Insert cell
function removeHighlights() {
hoverContainer.remove(...hoverContainer.children);
}
Insert cell
Insert cell
{
view.on("mouseleave", () => {
removeHighlights()
})
return view;
}
Insert cell
Insert cell
tooltip_state = { return { display: "none" } }
Insert cell
function showTooltip(mouse_position, datum) {
let tooltip_width = 120;
let x_offset = -tooltip_width/2;
let y_offset = 30;
tooltip_state.display = "block";
tooltip_state.left = mouse_position[0] + x_offset;
tooltip_state.top = mouse_position[1] + y_offset;
tooltip_state.name = datum.name;
tooltip_state.group = datum.group;
}
Insert cell
function hideTooltip() {
tooltip_state.display = "none";
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function toRadians (angle) {
return angle * (Math.PI / 180);
}
Insert cell
mutable viz_width = Math.min(width, 700)
Insert cell
height = 350
Insert cell
generated_points = {
let radius = 25;
// Random point in circle code from https://stackoverflow.com/questions/32642399/simplest-way-to-plot-points-randomly-inside-a-circle
function randomPosition(radius) {
var pt_angle = Math.random() * 2 * Math.PI;
var pt_radius_sq = Math.random() * radius * radius;
var pt_x = Math.sqrt(pt_radius_sq) * Math.cos(pt_angle);
var pt_y = Math.sqrt(pt_radius_sq) * Math.sin(pt_angle);
return [pt_x, pt_y];
}
let data_points = [];
for (let i = 0; i < point_num; i++) {
let position = randomPosition(radius);
let name = 'Point ' + i;
let group = Math.floor(Math.random() * 6);
let point = { position, name, group };
data_points.push(point);
}
return data_points;
}
Insert cell
point_num = 1000
Insert cell
color_array = [
"#1f78b4",
"#b2df8a",
"#33a02c",
"#fb9a99",
"#e31a1c",
"#fdbf6f",
"#ff7f00",
"#6a3d9a",
"#cab2d6",
"#ffff99"
]

Insert cell
renderer = new THREE.WebGLRenderer({antialias: true})
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more