From 4a431078d550e415816b17de54d28a02edb70791 Mon Sep 17 00:00:00 2001 From: Cee <cnell@usgs.gov> Date: Tue, 8 Oct 2024 14:17:58 -0700 Subject: [PATCH 1/5] preserve positions --- src/components/BeaufortSeaTimelineViz.vue | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/BeaufortSeaTimelineViz.vue b/src/components/BeaufortSeaTimelineViz.vue index b6ac839..aa6504f 100644 --- a/src/components/BeaufortSeaTimelineViz.vue +++ b/src/components/BeaufortSeaTimelineViz.vue @@ -199,9 +199,7 @@ const nodes = data.map((d) => ({ ...d, - radius: sizeScale(parseFloat(d.pct_abundance)), - x: bubbleChartDimensions.boundedWidth / 2, - y: bubbleChartDimensions.boundedHeight / 2 + radius: sizeScale(parseFloat(d.pct_abundance)) })); // set up nodes @@ -221,6 +219,7 @@ const newNodeGroups = nodeGroups.enter().append("g") .attr("class", "node") .attr("id", d => "group_" + d.species_id) + .attr("transform", d => `translate(${d.x || bubbleChartDimensions.boundedWidth / 2}, ${d.y || bubbleChartDimensions.boundedHeight / 2})`); newNodeGroups.append("circle") .attr("id", d => d.species_id) -- GitLab From 9517e836b619af1455421ae1c43fc748ade2bcc3 Mon Sep 17 00:00:00 2001 From: Cee <cnell@usgs.gov> Date: Tue, 8 Oct 2024 14:35:18 -0700 Subject: [PATCH 2/5] tweak forces --- src/components/BeaufortSeaTimelineViz.vue | 50 +++++------------------ 1 file changed, 11 insertions(+), 39 deletions(-) diff --git a/src/components/BeaufortSeaTimelineViz.vue b/src/components/BeaufortSeaTimelineViz.vue index aa6504f..78298b5 100644 --- a/src/components/BeaufortSeaTimelineViz.vue +++ b/src/components/BeaufortSeaTimelineViz.vue @@ -229,60 +229,32 @@ .attr("r", 0) //instantiate w/ radius = 0 //update nodeGroups to include new nodes - nodeGroups = newNodeGroups.merge(nodeGroups) + nodeGroups = newNodeGroups.merge(nodeGroups); - const nodeGroupCircle = nodeGroups.select("circle") - - nodeGroupCircle + nodeGroups.select("circle") .transition(getUpdateTransition()) - .attr("r", d => d.radius) + .attr("r", d => d.radius); function ticked() { - nodeGroupCircle - // .transition(getUpdateTransition()) // BREAKS d3 force - // .attr("r", d => d.radius) - .attr("cx", (d) => d.x) - .attr("cy", (d) => d.y); + nodeGroups + .transition(getUpdateTransition()) + .attr("transform", d => `translate(${d.x}, ${d.y})`); } // set up d3 force simulation if (simulation.value) { - // simulation.value.stop() simulation.value .nodes(nodes) .alpha(0.9) - .restart() - // .force("x", d3.forceX(bubbleChartDimensions.boundedWidth / 2).strength(0.05)) - // .force("y", d3.forceY(bubbleChartDimensions.boundedHeight / 2).strength(0.05)) - // .force("center", d3.forceCenter(bubbleChartDimensions.boundedWidth / 2, bubbleChartDimensions.boundedHeight / 2)) - // .force( - // "collide", - // d3.forceCollide() - // .radius((d) => d.radius + 2) - // .iterations(1) - // ) - // .force('charge', d3.forceManyBody().strength(0.01)) - // .alphaMin(0.01) - // .alpha(0) - // .alphaDecay(0.005) - .velocityDecay(0.9) - .on("tick", ticked); + .restart(); } else { simulation.value = d3.forceSimulation(); simulation.value .nodes(nodes) - .force("x", d3.forceX(bubbleChartDimensions.boundedWidth / 2).strength(0.05)) - .force("y", d3.forceY(bubbleChartDimensions.boundedHeight / 2).strength(0.05)) - // .force("center", d3.forceCenter(bubbleChartDimensions.boundedWidth / 2, bubbleChartDimensions.boundedHeight / 2)) - .force( - "collide", - d3.forceCollide() - .radius((d) => d.radius + 2) - .iterations(1) - ) - .force('charge', d3.forceManyBody().strength(0.01)) - // .alphaMin(0.01) - // .alphaDecay(0.005) + .force("x", d3.forceX(bubbleChartDimensions.boundedWidth / 2).strength(0.2)) + .force("y", d3.forceY(bubbleChartDimensions.boundedHeight / 2).strength(0.2)) + .force("collide", d3.forceCollide().radius(d => d.radius + 2).iterations(1)) + .force('charge', d3.forceManyBody().strength(-5)) .velocityDecay(0.9) .on("tick", ticked); } -- GitLab From c5835fdad3c077062706c6768dd3f352e26f5a9e Mon Sep 17 00:00:00 2001 From: Cee <cnell@usgs.gov> Date: Tue, 8 Oct 2024 14:44:48 -0700 Subject: [PATCH 3/5] rm dup selectall --- src/components/BeaufortSeaTimelineViz.vue | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/components/BeaufortSeaTimelineViz.vue b/src/components/BeaufortSeaTimelineViz.vue index 78298b5..07adb22 100644 --- a/src/components/BeaufortSeaTimelineViz.vue +++ b/src/components/BeaufortSeaTimelineViz.vue @@ -203,20 +203,18 @@ })); // set up nodes - let nodeGroups = bubbleChartBounds.selectAll('.nodes') + let nodeGroups = bubbleChartBounds .selectAll(".node") .data(nodes, d => d.species_id) const oldNodeGroups = nodeGroups.exit() - oldNodeGroups.selectAll("circle") - .attr("r", 0); //radius to 0 - - // Remove old nodes - oldNodeGroups.transition(getExitTransition()).remove() + oldNodeGroups.selectAll("circle").attr("r", 0); //radius to 0 + oldNodeGroups.transition(getExitTransition()).remove() // Remove old nodes // Append new nodes - const newNodeGroups = nodeGroups.enter().append("g") + const newNodeGroups = nodeGroups.enter() + .append("g") .attr("class", "node") .attr("id", d => "group_" + d.species_id) .attr("transform", d => `translate(${d.x || bubbleChartDimensions.boundedWidth / 2}, ${d.y || bubbleChartDimensions.boundedHeight / 2})`); @@ -237,7 +235,6 @@ function ticked() { nodeGroups - .transition(getUpdateTransition()) .attr("transform", d => `translate(${d.x}, ${d.y})`); } @@ -248,9 +245,8 @@ .alpha(0.9) .restart(); } else { - simulation.value = d3.forceSimulation(); - simulation.value - .nodes(nodes) + simulation.value = d3.forceSimulation(nodes) + .force("center", d3.forceCenter(bubbleChartDimensions.boundedWidth / 2, bubbleChartDimensions.boundedHeight / 2)) .force("x", d3.forceX(bubbleChartDimensions.boundedWidth / 2).strength(0.2)) .force("y", d3.forceY(bubbleChartDimensions.boundedHeight / 2).strength(0.2)) .force("collide", d3.forceCollide().radius(d => d.radius + 2).iterations(1)) @@ -263,7 +259,7 @@ // define transitions function getUpdateTransition() { return d3.transition() - .duration(2000) + .duration(500) .ease(d3.easeCubicInOut) } function getExitTransition() { -- GitLab From 9fb19999b7a10e9d4c821978ded8ee4fb4531131 Mon Sep 17 00:00:00 2001 From: Cee <cnell@usgs.gov> Date: Tue, 8 Oct 2024 14:58:11 -0700 Subject: [PATCH 4/5] separate simulation from chart fxn --- src/components/BeaufortSeaTimelineViz.vue | 45 ++++++++++++----------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/components/BeaufortSeaTimelineViz.vue b/src/components/BeaufortSeaTimelineViz.vue index 07adb22..faf4ef8 100644 --- a/src/components/BeaufortSeaTimelineViz.vue +++ b/src/components/BeaufortSeaTimelineViz.vue @@ -207,10 +207,11 @@ .selectAll(".node") .data(nodes, d => d.species_id) - const oldNodeGroups = nodeGroups.exit() - - oldNodeGroups.selectAll("circle").attr("r", 0); //radius to 0 - oldNodeGroups.transition(getExitTransition()).remove() // Remove old nodes + // Handle exit + nodeGroups.exit().selectAll("circle") + .transition(getExitTransition()) + .attr("r", 0) //radius to 0 + .remove() // Remove old nodes // Append new nodes const newNodeGroups = nodeGroups.enter() @@ -225,6 +226,8 @@ .attr("stroke-width", 0.5) .attr("fill", d => d.hexcode) .attr("r", 0) //instantiate w/ radius = 0 + .transition(getUpdateTransition()) // Transition to final size + .attr("r", d => d.radius); //update nodeGroups to include new nodes nodeGroups = newNodeGroups.merge(nodeGroups); @@ -233,29 +236,29 @@ .transition(getUpdateTransition()) .attr("r", d => d.radius); + // Update the force simulation + updateSimulation(nodes, nodeGroups); + } + function updateSimulation(nodes, nodeGroups) { function ticked() { - nodeGroups - .attr("transform", d => `translate(${d.x}, ${d.y})`); + nodeGroups.attr("transform", d => `translate(${d.x}, ${d.y})`); } - // set up d3 force simulation if (simulation.value) { - simulation.value - .nodes(nodes) - .alpha(0.9) - .restart(); - } else { - simulation.value = d3.forceSimulation(nodes) - .force("center", d3.forceCenter(bubbleChartDimensions.boundedWidth / 2, bubbleChartDimensions.boundedHeight / 2)) - .force("x", d3.forceX(bubbleChartDimensions.boundedWidth / 2).strength(0.2)) - .force("y", d3.forceY(bubbleChartDimensions.boundedHeight / 2).strength(0.2)) - .force("collide", d3.forceCollide().radius(d => d.radius + 2).iterations(1)) - .force('charge', d3.forceManyBody().strength(-5)) - .velocityDecay(0.9) - .on("tick", ticked); + // Clear forces and reset simulation for new nodes + simulation.value.stop(); } - } + // Create a new force simulation + simulation.value = d3.forceSimulation(nodes) + .force("center", d3.forceCenter(bubbleChartDimensions.boundedWidth / 2, bubbleChartDimensions.boundedHeight / 2)) + .force("x", d3.forceX(bubbleChartDimensions.boundedWidth / 2).strength(0.3)) // Pull toward the center on x-axis + .force("y", d3.forceY(bubbleChartDimensions.boundedHeight / 2).strength(0.3)) // Pull toward the center on y-axis + .force("collide", d3.forceCollide(d => d.radius + 2).strength(1)) // Ensure no overlap + .force("charge", d3.forceManyBody().strength(-15)) // Weak repulsive force to spread nodes slightly + .velocityDecay(0.8) + .on("tick", ticked); // Update positions during simulation + } // define transitions function getUpdateTransition() { return d3.transition() -- GitLab From 521c6c64a1d07edf62bc88c1db581ba7eff68ffd Mon Sep 17 00:00:00 2001 From: Cee <cnell@usgs.gov> Date: Tue, 8 Oct 2024 16:21:15 -0700 Subject: [PATCH 5/5] make exiting circles invisible but still present --- src/components/BeaufortSeaTimelineViz.vue | 88 ++++++++++++----------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/src/components/BeaufortSeaTimelineViz.vue b/src/components/BeaufortSeaTimelineViz.vue index faf4ef8..721f6fc 100644 --- a/src/components/BeaufortSeaTimelineViz.vue +++ b/src/components/BeaufortSeaTimelineViz.vue @@ -183,91 +183,97 @@ .attr("class", "nodes") } - function drawBubbleChart(data, { - decade = 200 - }) { - // Set radius based on data values across all decades + function drawBubbleChart(data, { decade = 200 }) { const sizeScale = d3.scaleSqrt() - .domain([ - d3.min(data, (d) => parseFloat(d.pct_abundance)), - d3.max(data, (d) => parseFloat(d.pct_abundance)) - ]) + .domain([d3.min(data, d => parseFloat(d.pct_abundance)), d3.max(data, d => parseFloat(d.pct_abundance))]) .range([4, chart.value.offsetWidth / 7]); - // filter data to current decade and filter out decades where species is not present + // Filter data for the selected decade data = data.filter(d => d.decade === decade && d.pct_abundance > 0); - - const nodes = data.map((d) => ({ + + const nodes = data.map(d => ({ ...d, - radius: sizeScale(parseFloat(d.pct_abundance)) + radius: sizeScale(parseFloat(d.pct_abundance)), + x: d.x || Math.random() * bubbleChartDimensions.boundedWidth, // Retain previous position if available + y: d.y || Math.random() * bubbleChartDimensions.boundedHeight })); - - // set up nodes - let nodeGroups = bubbleChartBounds - .selectAll(".node") - .data(nodes, d => d.species_id) - // Handle exit - nodeGroups.exit().selectAll("circle") + // Join data to nodes, keyed by species_id + let nodeGroups = bubbleChartBounds.selectAll(".node") + .data(nodes, d => d.species_id); // Ensure the key is species_id + + // Handle exit (hide old nodes instead of removing them) + nodeGroups.exit().select("circle") .transition(getExitTransition()) - .attr("r", 0) //radius to 0 - .remove() // Remove old nodes + .attr("r", 0) // Shrink radius + .style("opacity", 0) // Set opacity to 0 (hide) + .on("end", function() { + d3.select(this).attr("visibility", "hidden"); // Hide from view but keep in DOM + }); - // Append new nodes + // Handle enter (new nodes) const newNodeGroups = nodeGroups.enter() .append("g") .attr("class", "node") .attr("id", d => "group_" + d.species_id) - .attr("transform", d => `translate(${d.x || bubbleChartDimensions.boundedWidth / 2}, ${d.y || bubbleChartDimensions.boundedHeight / 2})`); + .attr("transform", d => `translate(${d.x}, ${d.y})`); newNodeGroups.append("circle") .attr("id", d => d.species_id) .attr("stroke", "#000000") .attr("stroke-width", 0.5) .attr("fill", d => d.hexcode) - .attr("r", 0) //instantiate w/ radius = 0 - .transition(getUpdateTransition()) // Transition to final size + .attr("r", 0) // Start new circles at radius 0 + .transition(getUpdateTransition()) // Transition to final size .attr("r", d => d.radius); - //update nodeGroups to include new nodes + // Merge enter and update selections nodeGroups = newNodeGroups.merge(nodeGroups); + // Handle updates (transitioning circles based on new data) nodeGroups.select("circle") + .attr("visibility", "visible") // Make re-entered nodes visible again .transition(getUpdateTransition()) - .attr("r", d => d.radius); + .attr("r", d => d.radius) + .style("opacity", 1); // Ensure opacity is set back to 1 - // Update the force simulation + // Update the force simulation with the full set of nodes updateSimulation(nodes, nodeGroups); } + function updateSimulation(nodes, nodeGroups) { function ticked() { nodeGroups.attr("transform", d => `translate(${d.x}, ${d.y})`); } - if (simulation.value) { - // Clear forces and reset simulation for new nodes - simulation.value.stop(); + if (!simulation.value) { + simulation.value = d3.forceSimulation(); } - // Create a new force simulation - simulation.value = d3.forceSimulation(nodes) + // Restart the simulation and reapply forces + simulation.value.nodes(nodes) .force("center", d3.forceCenter(bubbleChartDimensions.boundedWidth / 2, bubbleChartDimensions.boundedHeight / 2)) - .force("x", d3.forceX(bubbleChartDimensions.boundedWidth / 2).strength(0.3)) // Pull toward the center on x-axis - .force("y", d3.forceY(bubbleChartDimensions.boundedHeight / 2).strength(0.3)) // Pull toward the center on y-axis - .force("collide", d3.forceCollide(d => d.radius + 2).strength(1)) // Ensure no overlap - .force("charge", d3.forceManyBody().strength(-15)) // Weak repulsive force to spread nodes slightly - .velocityDecay(0.8) - .on("tick", ticked); // Update positions during simulation + .force("x", d3.forceX(bubbleChartDimensions.boundedWidth / 2).strength(0.3)) // Pull toward center on x-axis + .force("y", d3.forceY(bubbleChartDimensions.boundedHeight / 2).strength(0.3)) // Pull toward center on y-axis + .force("collide", d3.forceCollide(d => d.radius + 2).strength(1)) // Prevent overlap + .force("charge", d3.forceManyBody().strength(-15)) // Slight repulsion to spread nodes slightly + .on("tick", ticked); // Call tick function on each simulation iteration + + // Restart the simulation with an alpha of 0.7 to ensure proper positioning + simulation.value.alpha(0.7).restart(); } + + + // define transitions function getUpdateTransition() { return d3.transition() - .duration(500) + .duration(700) .ease(d3.easeCubicInOut) } function getExitTransition() { return d3.transition() - .duration(500) + .duration(700) .ease(d3.easeCubicInOut) } -- GitLab