Published
Edited
May 20, 2021
4 forks
18 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
camera = {
const camera = new THREE.PerspectiveCamera(75, size.width / size.height, 0.2, 1500);
camera.updateProjectionMatrix();
camera.position.set(0, max * 1.5, 0);
return camera;
}
Insert cell
scene = {
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
return scene;
}
Insert cell
Insert cell
hemisphereLight = {
const light = new THREE.HemisphereLight(0xfbfbfb, 0x0, 0.35);
light.position.set(0, 1000, 0);
scene.add(light);
render();
invalidation.then(() => scene.remove(light));
return light;
}
Insert cell
pointLight = {
const light = new THREE.PointLight(0xfbfaf5, 2.25, 1500);
light.decay = 2;
light.position.set(0, 750, 0);
scene.add(light);
render();
invalidation.then(() => scene.remove(light));
return light;
}
Insert cell
Insert cell
controls = {
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.screenSpacePanning = false;
//controls.enablePan = false;
controls.maxPolarAngle = Math.PI / 2.5;
controls.minDistance = 150;
controls.maxDistance = max * 1.25;
controls.addEventListener("change", () => {
tooltip.clear();
renderer.clear();
renderer.render(scene, camera);
});
controls.update();
invalidation.then(() => controls.dispose());
return controls;
}
Insert cell
Insert cell
renderer = {
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(size.width, size.height);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.autoClear = false;
invalidation.then(() => renderer.dispose());
return renderer;
}
Insert cell
render = () => {
renderer.clear();
renderer.render(scene, camera);
}
Insert cell
Insert cell
raycaster = new THREE.Raycaster()
Insert cell
mouse = ({
screen: new THREE.Vector2(),
scene: new THREE.Vector2(),
animating: false,
focus: null
})
Insert cell
trackMouse = e => {
e.preventDefault();
if (e.target instanceof HTMLCanvasElement) {
const x = e.offsetX + e.target.offsetLeft;
const y = e.offsetY + e.target.offsetTop;
mouse.screen.x = x;
mouse.screen.y = y;
mouse.scene.x = (e.offsetX / size.width) * 2 - 1;
mouse.scene.y = -(e.offsetY / size.height) * 2 + 1;
if (!mouse.animating) intersect();
}
}
Insert cell
intersect = () => {
camera.updateMatrixWorld();
raycaster.setFromCamera(mouse.scene, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
let target;
for (let current of intersects) {
if (current.object.info) {
target = current.object;
break;
}
}

if (target) {
if (mouse.focus !== target) {
cancelHighlight();
mouse.focus = target;
tooltip.update(mouse.focus);
render();
}
}
else cancelHighlight();
}
else cancelHighlight();
}
Insert cell
cancelHighlight = () => {
const f = mouse.focus;
if (f) {
mouse.focus = null;
tooltip.clear();
render();
}
}
Insert cell
Insert cell
tooltip = {
const tooltip = initDivTooltip();
invalidation.then(() => tooltip.dispose());
return tooltip;
}
Insert cell
initDivTooltip = () => {
const div = document.createElement("div");
div.style.position = "absolute";
div.style.display = "none";
div.style.font = "10pt Tahoma";
div.style.backgroundColor = "white";
div.style.opacity = 0.85;
div.style.borderRadius = "3px";
div.style.boxShadow = "1px 1px 1px #666666";
div.style.border = "solid 1px #666666";
div.style.padding = "3px";
div.style.pointerEvents = "none";
const tooltip = {
attach: function(container) {
if (container) container.appendChild(div);
},
update: function(target) {
div.innerText = target.info.name + "\n" + format(target.info.value);
div.style.display = "block";
div.style.left = `${mouse.screen.x + 5}px`;
div.style.top = `${mouse.screen.y + 5}px`;
},
clear: function() {
div.innerText = "";
div.style.display = "none";
},
dispose: function() {
if (div.parentElement) div.parentElement.removeChild(div);
}
}
return tooltip;
}
Insert cell
Insert cell
drawCloud = (words, font) => {
mouse.animating = true;
// Disable the OrbitControls to avoid interfering with the animation
controls.enabled = false;

let requestId;
const time = 1.5,
tweens = [];
//const gif = new GIF({ workers: 4, quality: 5, debug: true });
animate();
camera.position.set(-cloudDims.width * 0.4, max * 0.7, cloudDims.height);
camera.rotation.set(-0.6, -0.35, -0.2);

drawFloor(time, tweens);
const firstWord = drawWords(words, font, time, tweens);
moveLight(firstWord);
moveCamera(time, tweens);

Promise.all(tweens).then(() => {
cancelAnimationFrame(requestId);
//gif.on("finished", blob => mutable animGif = blob);
//gif.render();
mouse.animating = false;
controls.enabled = true;
});
function animate() {
requestId = requestAnimationFrame(animate);
render();
//gif.addFrame(renderer.domElement, {copy: true, delay: 0.01});
}
}
Insert cell
drawFloor = (time, tweens) => {
const pw = cloudDims.width * 1.4,
ph = cloudDims.height * 1.4;

for(let i = -pw; i < pw; i += brickDims.width){
for(let j = -ph; j < ph; j += brickDims.depth) {
const cuboid = addCuboid(
brickDims.width, brickDims.height, brickDims.depth,
i, -500, j);
tweens.push(gsap.to(
cuboid.position,
{duration: Math.random() * time, y: -brickDims.height / 2 - Math.random() * 10}));
}
}
}
Insert cell
drawWords = (words, font, time, tweens) => {
const rx = Math.PI * 1.5,
rad = Math.PI / 180;

let first;
words
.sort((a, b) => b.value - a.value)
.forEach(word => {
const text = addText(
word.text, word.size, 5 + wordHeight(word.value),
word.x, 500, word.y,
rx,0, -word.rotate * rad, font);
text.info = {name: word.text, value: word.value};
tweens.push(gsap.to(
text.position,
{duration: Math.random() * time, y: 0}));
// text-anchor = middle in SVG
if (!text.geometry.boundingBox) text.geometry.computeBoundingBox();
const tw = text.geometry.boundingBox.max.x;
if (!first) first = text;
if (word.rotate === 0)
text.position.x -= tw / 2;
else
text.position.z -= tw / 2;
});
return first;
}
Insert cell
moveLight = focus => {
if (focus.rotation.z !== 0) {
pointLight.position.x = focus.position.x + focus.geometry.boundingBox.max.z / 2;
pointLight.position.z = focus.position.z + focus.geometry.boundingBox.max.x / 2;
}
else {
pointLight.position.x = focus.position.x + focus.geometry.boundingBox.max.x / 2;
pointLight.position.z = focus.position.z - focus.geometry.boundingBox.max.z / 2;
}
}
Insert cell
moveCamera = (time, tweens) => {
const y = cloudDims.width === cloudDims.height ? max * 1.2 : max;
tweens.push(gsap.to(camera.position, {duration: time * 1.5, x: 0, y: y, z: 0}));
tweens.push(gsap.to(camera.rotation, {duration: time * 1.5, x: -1.57, y: 0, z: 0}));
}
Insert cell
Insert cell
addCuboid = (w, h, d, x, y, z) => {
const cuboid = new THREE.Mesh(pool.floorGeometry, pool.floorMaterial);
cuboid.position.set(x + w / 2, y, z + d / 2);
cuboid.scale.set(w, h, d);
scene.add(cuboid);
return cuboid;
}
Insert cell
addText = (text, size, h, x, y, z, rx, ry, rz, font) => {
const deviation = 1.75;
const geometry = new THREE.TextBufferGeometry(
text,
{ font: font, size, height: h, curveSegments: 4, bevelThickness: 2, bevelSize: 2, bevelEnabled: true });
geometry.computeBoundingSphere();
geometry.computeVertexNormals();
const mesh = new THREE.Mesh(geometry, pool.textMaterial);
mesh.position.set(x * deviation, y, z * deviation);
mesh.rotation.set(rx, ry, rz);
scene.add(mesh);
return mesh;
}
Insert cell
floorColor = 0x98c1d9
Insert cell
textColor = 0xffffff
Insert cell
pool = ({
floorGeometry: new THREE.BoxBufferGeometry(1, 1, 1),
floorMaterial: new THREE.MeshPhongMaterial({color: floorColor, flatShading: true}),
textMaterial: new THREE.MeshPhongMaterial({color: textColor, flatShading: true})
})
Insert cell
dispose = () => {
cleanup(scene);
function cleanup(obj) {
for(let i = obj.children.length - 1; i >= 0; i--) {
const child = obj.children[i];
if (!(child instanceof THREE.Light)) {
obj.remove(child);
if (child.geometry) child.geometry.dispose();
if (child.material) child.material.dispose();
if (child.children && child.children.length > 0) cleanup(child);
}
}
}
}
Insert cell
Insert cell
cloudData = {
if (dataset === "Sample Dataset") return sales;
else if (dataset === "2020 Full Coverage Auto Insurance Cost") return insurance;
else return oscar;
}
Insert cell
Insert cell
insurance = d3
.csvParse(await FileAttachment("auto2020.csv").text(), d3.autoType)
.map(d => ({text: d.State, value: d.Full}))
Insert cell
Insert cell
oscar = {
const src = d3.csvParse(await FileAttachment("2020oscar.csv").text(), d3.autoType);
return Array.from(d3.rollup(src, v => v.length, d => d.Film)).map(d => ({text: d[0], value: d[1]}));
}
Insert cell
sales = d3
.csvParse(await FileAttachment("orders.csv").text(), d3.autoType)
.map(d => ({text: d.Subcategory, value: d.Quantity}));
Insert cell
wordAngle = () => ~~(Math.random() * 2) * 90;
Insert cell
ext = d3.extent(cloudData.map(d => d.value))
Insert cell
wordSize = d3.scaleLinear().domain(ext).range([24, 48])
Insert cell
wordHeight = d3.scaleLinear().domain(ext).range([5, 30])
Insert cell
format = d3.format(",d")
Insert cell
Insert cell
Insert cell
/*GIF = {
const [gif, workerScript] = await Promise.all([
require("gif.js@0.2"),
require.resolve("gif.js@0.2/dist/gif.worker.js")
.then(fetch)
.then(response => response.blob())
.then(blob => URL.createObjectURL(blob, {type: "text/javascript"}))
]);
return class extends gif {
constructor(options) {
super({workerScript, ...options});
}
};
}*/
Insert cell
//mutable animGif = null;
Insert cell
Insert cell
/*recorder = {
const script = await require.resolve("gif.js@0.2/dist/gif.worker.js")
.then(fetch)
.then(response => response.blob())
.then(blob => URL.createObjectURL(blob, {type: "text/javascript"}));
return new CCapture({
format: "gif",
framerate: 30,
quality: 99,
workersPath: script
});
}*/
Insert cell
//CCapture = require("ccapture.js")
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more