Public
Edited
Oct 19, 2023
Insert cell
Insert cell
Insert cell
defaultColor = "gray"; // Set a single color (gray) for all nodes
Insert cell
colorsDVL = ["#2251FF", "#00A9F4", "#AAE6F0", "#F2F2F2"]
Insert cell
colorsRainbow = ["#FFA500", "#00CC00", "#000088", "#D81B60", "#9B59B6", ]
Insert cell
colorsMDS = ["#FCB833", "#F45887", "#9F55B7", "#2251FF", "#73E4D6", ]
Insert cell
singleBoundaryColor = "red";
Insert cell
Insert cell
// Constants - can be adjusted:
viewof color = Inputs.radio(["single", "DVL", "rainbow!", "MDS"], {label: `Colors!!`, value: 'rainbow!'})
Insert cell
Insert cell
Insert cell
// FIXED flickering effect when node remains within boundary of cursor
// TODO: make circles fill up canvas? Will have to add a bounding force to make sure nodes don't go out of view
// TODO: move force of cursor node out of this function; have forceDistance be dependent on cursor node force
// QUESTION: why does the force seem more volatile at first in the original version of this notebook, but takes a while to build up volatility here...? Look into caching: compare both notebooks on incognito vs regular browser to see if we get same behavior over time


chart5 = {
const height = width; // CHANGE THIS HERE FOR AUTO HEIGHT FIT...

// Set initial positions outside the viewport
// data.forEach(node => {
// node.x = Math.random() < 0.5 ? -100 : width + 100; // Initial positions outside the canvas
// node.y = Math.random() * height; // Random initial y position
// });
const context = DOM.context2d(width, height);
const nodes = data.map(Object.create);
const forceDistance = -20;
const maxBoundaryDistance = 0;

context.fillStyle = "#000";
context.fillRect(0, 0, width, height);

const simulation = d3.forceSimulation(nodes)
.alphaTarget(0.3)
.velocityDecay(0.1)
.force("x", d3.forceX().strength(0.01))
.force("y", d3.forceY().strength(0.01)) // Initial y force
.force("collide", d3.forceCollide().radius(d => d.r + 1).iterations(3))
.force("charge", d3.forceManyBody().strength((d, i) => i ? 0 : -width * 2 / 50))
.on("tick", ticked);

const duration = 1000; // 5 seconds duration
const startTime = performance.now();
function updateForceYStrength() {
const currentTime = performance.now();
const elapsed = currentTime - startTime;
if (elapsed < duration) {
const progress = elapsed / duration;
const newStrength = 0.5 - (0.49 * progress); // Adjust the 0.49 factor as needed
// Update the forceY strength
simulation.force("y", d3.forceY().strength(newStrength));
// Continue updating the animation
requestAnimationFrame(updateForceYStrength);
} else {
// Ensure that the final strength is exactly 0.01
simulation.force("y", d3.forceY().strength(0.01));
}
}

// Start the animation to gradually decrease forceY strength
// requestAnimationFrame(updateForceYStrength);

d3.select(context.canvas)
.on("touchmove", event => event.preventDefault())
.on("pointermove", pointermoved);

invalidation.then(() => simulation.stop());

const fadeDuration = 6000; // Adjust the duration as needed - originally 3000
const fadeOutRate = 0.003; // Adjust the rate as needed - originally 0.005
const fadingNodes = [];

function getRandomColor() {
const letters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}

function getRandomColorFromArray() {
const colorArray = color === 'DVL' ? colorsDVL : color === 'MDS' ? colorsMDS : colorsRainbow
// Pick a random index from the array
const randomIndex = Math.floor(Math.random() * colorArray.length);
// Return the color at the random index
return colorArray[randomIndex];
}

function pointermoved(event) {
const [x, y] = d3.pointer(event);
nodes[0].fx = x - width / 2;
nodes[0].fy = y - height / 2;

// Calculate distances from node centers to cursor node boundary
for (let i = 1; i < nodes.length; ++i) {
const d = nodes[i];
const dx = d.x - x + width / 2;
const dy = d.y - y + height / 2;
const distanceToBoundary = Math.sqrt(dx ** 2 + dy ** 2) - d.r - nodes[0].r + forceDistance;

// Set isTouchingBoundary based on distance
d.isTouchingBoundary = distanceToBoundary <= maxBoundaryDistance;

// If it's touching the boundary and hasn't been set to a random color yet, set it
if (d.isTouchingBoundary) {
// d.randomColor = getRandomColorFromArray(); // THIS IS THE PROBLEM - we are setting random color every time the mouse moves?
// d.randomColor = 'blue'
d.fadeStartTime = Date.now();
fadingNodes.push(d);
}
if (d.isTouchingBoundary && !d.randomColor && color !== 'single') {
d.randomColor = getRandomColorFromArray()
}
}

// Redraw the chart
ticked();
}

function ticked() {
context.clearRect(0, 0, width, height);
context.save();
context.translate(width / 2, height / 2);

for (let i = 1; i < nodes.length; ++i) {
const d = nodes[i];
context.beginPath();
context.moveTo(d.x + d.r, d.y);
context.arc(d.x, d.y, d.r, 0, 2 * Math.PI);

// Calculate opacity based on elapsed time since fade started
if (fadingNodes.includes(d)) {
const elapsed = Date.now() - d.fadeStartTime;
const opacity = 1 - Math.min(elapsed * fadeOutRate, 1);

if (opacity <= 0) {
// Opacity has reached 0, remove the randomColor
delete d.randomColor;
}

// Interpolate between default color and the random/single color
const interpolatedColor = d3.interpolateRgb(defaultColor, color === 'single' ? singleBoundaryColor : d.randomColor)(opacity);

context.fillStyle = interpolatedColor;
}
else {
context.fillStyle = defaultColor;
}

context.fill();
}

context.restore();
}

return context.canvas;
}

Insert cell
data = {
const k = width / 200;
const r = d3.randomUniform(k, k * 4);
const n = 4;
return Array.from({length: 200}, (_, i) => ({r: r(), group: i && (i % n + 1)}));
}
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