Public
Edited
Oct 6, 2023
1 fork
Insert cell
Insert cell
// FIXED flickering effect when node remains within boundary of cursor
// TODO: make cursor node less repulsive
// TODO: make circles fill up canvas?
// TODO: try other color combinations
// TODO: animation of charts

chart5 = {
const height = width;
const singleColor = "lightgray"; // Set a single color (gray) for all nodes
const context = DOM.context2d(width, height);
const nodes = data.map(Object.create);
const forceDistance = -20;
const maxBoundaryDistance = 0;

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))
.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);

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

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

const fadeDuration = 3000; // Adjust the duration as needed - originally 1000
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() {
// Define an array of predefined colors
// const colors = ["#FF5733", "#33FF57", "#5733FF", "#FFFF33", "#33FFFF"];
// const colors = [
// "#FF2200", // Deeper red
// "#007700", // Deeper green
// "#00008B", // Deeper blue
// "#D81B60", // Deeper yellow
// "#006666", // Deeper cyan
// // Add more colors as needed
// ];
const colors = [
"#FFA500", // Deeper red
"#00CC00", // Deeper green
"#000088", // Deeper blue
"#D81B60", // Deeper yellow
"#9B59B6", // Deeper cyan
// Add more colors as needed
];

// Pick a random index from the array
const randomIndex = Math.floor(Math.random() * colors.length);

// Return the color at the random index
return colors[randomIndex];
// return "red"
}

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) {
// // 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 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) {
// d.randomColor = 'blue'
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;
// d.randomColor = null
}

// Interpolate between red and the random color
const interpolatedColor = d3.interpolateRgb(singleColor, d.randomColor)(opacity);

context.fillStyle = interpolatedColor;
// d.singleColor = null
}
else {
context.fillStyle = singleColor;
// d.singleColor = singleColor
}

context.fill();
}

context.restore();
}

return context.canvas;
}

Insert cell
chart4 = {
const height = width;
const singleColor = "lightgray"; // Set a single color (gray) for all nodes
const context = DOM.context2d(width, height);
const nodes = data.map(Object.create);
const forceDistance = -20;
const maxBoundaryDistance = 0;

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))
.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);

console.log(nodes)

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

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

const fadeDuration = 5000; // Adjust the duration as needed - originally 1000
const fadeOutRate = 0.001; // 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() {
// Define an array of predefined colors
// const colors = ["#FF5733", "#33FF57", "#5733FF", "#FFFF33", "#33FFFF"];
// const colors = [
// "#FF2200", // Deeper red
// "#007700", // Deeper green
// "#00008B", // Deeper blue
// "#D81B60", // Deeper yellow
// "#006666", // Deeper cyan
// // Add more colors as needed
// ];
const colors = [
"#FFA500", // Deeper red
"#00CC00", // Deeper green
"#000088", // Deeper blue
"#D81B60", // Deeper yellow
"#9B59B6", // Deeper cyan
// Add more colors as needed
];

// Pick a random index from the array
const randomIndex = Math.floor(Math.random() * colors.length);

// Return the color at the random index
return colors[randomIndex];
// return "red"
}

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) {
d.randomColor = getRandomColorFromArray();
d.fadeStartTime = Date.now();
fadingNodes.push(d);
}

// // If it's touching the boundary and hasn't been set to a random color yet, set it
// if (distanceToBoundary <= maxBoundaryDistance && !d.randomColor) {
// d.randomColor = getRandomColorFromArray();
// d.fadeStartTime = Date.now();
// fadingNodes.push(d);
// } else if (distanceToBoundary > maxBoundaryDistance && d.randomColor) {
// // If it's outside the boundary and has a random color, remove the randomColor
// delete d.randomColor;
// }
// else if (!d.isTouchingBoundary && d.singleColor !== null) {
// // If it's outside the boundary, remove the randomColor
// delete d.randomColor;
// }
}

// 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 red and the random color
const interpolatedColor = d3.interpolateRgb(singleColor, d.randomColor)(opacity);

context.fillStyle = interpolatedColor;
d.singleColor = null
}
else {
context.fillStyle = singleColor;
d.singleColor = singleColor
}

context.fill();
}

context.restore();
}

return context.canvas;
}

Insert cell
chart3 = {
const height = width;
const singleColor = "gray"; // Set a single color (gray) for all nodes
const boundaryColor = "red"
const context = DOM.context2d(width, height);
const nodes = data.map(Object.create);
const forceDistance = -20;
const maxBoundaryDistance = 0;

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))
.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);

console.log(nodes)

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

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

const fadeDuration = 3000; // Adjust the duration as needed - originally 1000
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() {
// Define an array of predefined colors
const colors = ["#FF5733", "#33FF57", "#5733FF", "#FFFF33", "#33FFFF"];

// Pick a random index from the array
const randomIndex = Math.floor(Math.random() * colors.length);

// Return the color at the random index
return colors[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.fadeStartTime = Date.now();
fadingNodes.push(d);
}
}

// 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);

// Interpolate between red and gray
const interpolatedColor = d3.interpolateRgb(singleColor, boundaryColor)(opacity);

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

context.fill();
}

context.restore();
}

return context.canvas;
}

Insert cell
chart2 = {
const width = 800; // Define your canvas dimensions
const height = 800;
const singleColor = "gray"; // Default color for nodes
const forceDistance = 20; // Adjust as needed
const maxBoundaryDistance = 0; // Adjust as needed
// Create an SVG container and append it to the body of the document
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
// Append circles for each node
const nodeElements = svg.selectAll(".node")
.data(data)
.enter().append("circle")
.attr("class", "node")
.attr("r", d => d.r)
.style("fill", singleColor);
// Add mousemove event listener
svg.on("mousemove", pointermoved);

// Initialize nodes with initial positions
data.forEach(d => {
d.x = 0 //width / 2 // Math.random() * width;
d.y = 0 // height / 2 // Math.random() * height;
});

// const simulation = d3.forceSimulation(data)
// .force("collide", d3.forceCollide().radius(d => d.r + 1).iterations(3))
// .force("charge", d3.forceManyBody().strength(-20)) // Repulsion force between nodes

const simulation = d3.forceSimulation(data)
.alphaTarget(0.3)
.velocityDecay(0.1)
.force("x", d3.forceX(width/2).strength(0.01))
.force("y", d3.forceY(height/2).strength(0.01))
.force("collide", d3.forceCollide().radius(d => d.r + 1).iterations(3))
.force("charge", d3.forceManyBody().strength((d, i) => i ? 0 : -width * 2 / 50))
// .force('center', d3.forceCenter(width / 2, height / 2))

simulation.on('tick', () => {
// Update node positions
nodeElements.attr("cx", d => d.x)
.attr("cy", d => d.y);
})
function pointermoved(event) {
const [x, y] = d3.pointer(event);

// Update the position of the first node to follow the cursor
data[0].fx = x;
data[0].fy = y;

// Calculate distances from node centers to cursor node boundary
data.forEach(d => {
const dx = d.x - x;
const dy = d.y - y;
const distanceToBoundary = Math.sqrt(dx ** 2 + dy ** 2) - d.r - data[0].r + forceDistance;
// Set isTouchingBoundary based on distance
d.isTouchingBoundary = distanceToBoundary <= maxBoundaryDistance;
});

// Update node colors and opacities using D3 transitions
nodeElements.transition()
.duration(500) // Adjust the duration as needed
.style("fill", d => (d.isTouchingBoundary ? "red" : singleColor))
.style("opacity", d => (d.isTouchingBoundary ? 1 : 0.6));
}

return svg.node()
}

Insert cell
// // Initialize simulation and nodes for SVG VERSION
// simulation = d3.forceSimulation(data)
// .force("collide", d3.forceCollide().radius(d => d.r + 1).iterations(3))
// .force("charge", d3.forceManyBody().strength(-20)) // Repulsion force between nodes
// // .on("tick", ticked);
Insert cell
chart = {
const height = width;
const singleColor = "gray"; // Set a single color (gray) for all nodes
const context = DOM.context2d(width, height);
const nodes = data.map(Object.create);
const forceDistance = -20;
const maxBoundaryDistance = 0;

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))
.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);

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

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

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;
}

// 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);
// Set color based on whether the node is touching the cursor node boundary
context.fillStyle = d.isTouchingBoundary ? "red" : singleColor;
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