Animate line and circle when button is clicked

32 Views Asked by At

I'm new to d3.js and I'm struggling to fix a bug on a simple plot. I'm basically trying to create an animated lollipop plot that can be updated by clicking on a button. This is the code I wrote:

// !preview r2d3 data = structure(list(Name = c("A", "B", "C", "D"), Posterior = c(0.75, 0.45, 0.25, 0.1), Prior = c(0.5, 0.5, 0.55, 0.01)), class = "data.frame", row.names = c(NA, -3L)), dependencies = "d3-jetpack"

// set up constants used throughout script
const margin = { top: 80, right: 100, bottom: 40, left: 60 };
const plotWidth = 800 - margin.left - margin.right;
const plotHeight = 400 - margin.top - margin.bottom;

const lineWidth = 4;
const mediumText = 18;
const bigText = 28;

// set width and height of svg element (plot + margin)
svg.attr("width", plotWidth + margin.left + margin.right)
   .attr("height", plotHeight + margin.top + margin.bottom);

// create plot group and move it
let plotGroup = svg.append("g")
                   .attr("transform",
                         "translate(" + margin.left + "," + margin.top + ")");

// x-axis
// x-axis goes from 0 to width of plot
let xAxis = d3.scaleLinear()
    .domain([0, 1])
    .range([0, plotWidth]);

// y-axis
// y-axis goes from height of plot to 0
let yAxis = d3.scaleBand()
    .domain(data.map(function(d) { return d.Name; }))
    .padding(1)
    .range([plotHeight, 0]);

// add x-axis to plot
// move x axis to bottom of plot (height)
// format tick values as date (no comma in e.g. 2,001)
// set stroke width and font size
plotGroup.append("g")
   .attr("transform", "translate(0," + plotHeight + ")")
   .call(d3.axisBottom(xAxis).tickFormat(d3.format(".0%")))  // Format ticks as percentage
   .attr("stroke-width", lineWidth)
   .attr("font-size", mediumText);

// add y-axis to plot
// set stroke width and font size
plotGroup.append("g")
    .call(d3.axisLeft(yAxis))
    .attr("stroke-width", lineWidth)
    .attr("font-size", mediumText);

// Lines
plotGroup.selectAll("myline")
  .data(data)
  .enter()
  .append("line")
    .attr("x1", xAxis(0.5))
    .attr("x2", function(d) { return xAxis(d.Prior); })
    .attr("y1", function(d) { return yAxis(d.Name); })
    .attr("y2", function(d) { return yAxis(d.Name); })
    .attr("stroke", "grey");

// Circles -> start at priors
plotGroup.selectAll("mycircle")
  .data(data)
  .enter()
  .append("circle")
    .attr("cx", function(d) { return xAxis(d.Prior); })
    .attr("cy", function(d) { return yAxis(d.Name); })
    .attr("r", "7")
    .style("fill", "#69b3a2")
    .attr("stroke", "black");

// Change the X coordinates of line and circle
plotGroup.selectAll("circle")
  .transition()
  .duration(2000)
  .attr("cx", function(d) { return xAxis(d.Posterior); });

plotGroup.selectAll("line")
  .transition()
  .duration(2000)
  .attr("x2", function(d) { return xAxis(d.Posterior); });

// Add a dashed vertical line at x = 0.5
plotGroup.append("line")
    .attr("x1", xAxis(0.5))
    .attr("y1", 0)
    .attr("x2", xAxis(0.5))
    .attr("y2", plotHeight)
    .attr("stroke", "black")
    .attr("stroke-dasharray", "4");  // Make the line dashed

// Add title starting from the left
svg.append("text")
    .attr("x", margin.left)  // Start from the left margin
    .attr("y", margin.top / 2)
    .attr("text-anchor", "start")  // Align to the start (left)
    .attr("font-size", bigText)
    .attr("font-family", "Roboto, sans-serif")
    .attr("font-weight", "bold")
    .text("Probability of a positive impact");

// Add a button
const buttonWidth = 100;
const buttonHeight = 30;

let buttonText = "Posterior"; // Initial text

const button = svg.append("rect")
    .attr("x", plotWidth + margin.left - buttonWidth)
    .attr("y", margin.top / 2 - buttonHeight / 2)
    .attr("width", buttonWidth)
    .attr("height", buttonHeight)
    .attr("rx", 5) // rounded corners
    .attr("ry", 5)
    .attr("fill", "blue")  // Button color
    .attr("cursor", "pointer") // Change cursor on hover
    .on("click", buttonClick);

const buttonTextElement = svg.append("text")
    .attr("x", plotWidth + margin.left - buttonWidth / 2)
    .attr("y", margin.top / 2)
    .attr("text-anchor", "middle")
    .attr("alignment-baseline", "middle")
    .attr("font-family", "Roboto, sans-serif")
    .attr("font-size", mediumText)
    .attr("fill", "white")
    .attr("cursor", "pointer")
    .text(buttonText)
    .on("click", buttonClick);

// Click event handler for the button
function buttonClick() {
    // Toggle between "Posterior" and "Prior"
    buttonText = (buttonText === "Posterior") ? "Prior" : "Posterior";
    buttonTextElement.text(buttonText);

    // Update x-coordinates of circles based on the button text
    plotGroup.selectAll("circle")
      .transition()
      .duration(2000)
      .attr("cx", function(d) { return xAxis(buttonText === "Posterior" ? d.Posterior : d.Prior); });

  // Update x2-coordinates of lines based on the button text
    plotGroup.selectAll("line")
      .transition()
      .duration(2000)
      .attr("x2", function(d) { return xAxis(buttonText === "Posterior" ? d.Posterior : d.Prior); });

}

As you can see in the image bellow, the circles are moving correctly but I cannot figure out how to make the lines move with them.

enter image description here

1

There are 1 best solutions below

1
Frenchy On BEST ANSWER

The problem comes from your definition of line, you have to add a class to notify the type of items you want to select (select only line for data and no ticks or other lines). I have added a class line to specific line:

// Lines
plotGroup.selectAll("myline")
  .data(data)
  .enter()
  .append("line")
    .attr("class", "line")
    .attr("x1", xAxis(0.5))
    .attr("x2", function(d) { return xAxis(d.Prior); })
    .attr("y1", function(d) { return yAxis(d.Name); })
    .attr("y2", function(d) { return yAxis(d.Name); })
    .attr("stroke", "grey");

and in the event click i call the class:

  // Update x2-coordinates of lines based on the button text
    plotGroup.selectAll("line.line")
      .transition()
      .duration(2000)
      .attr("x2", function(d) {return buttonText === "Posterior" ? xAxis(d.Posterior) : xAxis(d.Prior); });

see my code for the result.

const data = [{"Name": "A", "Prior": 0.5, "Posterior": 0.75},{"Name": "B", "Prior":0.5, "Posterior": 0.45},{"Name": "C", "Prior":0.55, "Posterior": 0.25},{"Name": "D", "Prior":0.01, "Posterior": 0.1}]
 // set up constants used throughout script
const margin = { top: 80, right: 100, bottom: 40, left: 60 };
const plotWidth = 800 - margin.left - margin.right;
const plotHeight = 400 - margin.top - margin.bottom;

const lineWidth = 4;
const mediumText = 18;
const bigText = 28;

// set width and height of svg element (plot + margin)
var svg = d3.select("#my_dataviz")
  .append("svg")
  .attr("width", plotWidth + margin.left + margin.right)
  .attr("height", plotHeight + margin.top + margin.bottom);

// create plot group and move it
let plotGroup = svg.append("g")
                   .attr("transform",
                         "translate(" + margin.left + "," + margin.top + ")");

// x-axis
// x-axis goes from 0 to width of plot
let xAxis = d3.scaleLinear()
    .domain([0, 1])
    .range([0, plotWidth]);

// y-axis
// y-axis goes from height of plot to 0
let yAxis = d3.scaleBand()
    .domain(data.map(function(d) { return d.Name; }))
    .padding(1)
    .range([plotHeight, 0]);

// add x-axis to plot
// move x axis to bottom of plot (height)
// format tick values as date (no comma in e.g. 2,001)
// set stroke width and font size
plotGroup.append("g")
   .attr("transform", "translate(0," + plotHeight + ")")
   .call(d3.axisBottom(xAxis).tickFormat(d3.format(".0%")))  // Format ticks as percentage
   .attr("stroke-width", lineWidth)
   .attr("font-size", mediumText);

// add y-axis to plot
// set stroke width and font size
plotGroup.append("g")
    .call(d3.axisLeft(yAxis))
    .attr("stroke-width", lineWidth)
    .attr("font-size", mediumText);

// Lines
plotGroup.selectAll("myline")
  .data(data)
  .enter()
  .append("line")
    .attr("class", "line")
    .attr("x1", xAxis(0.5))
    .attr("x2", function(d) { return xAxis(d.Prior); })
    .attr("y1", function(d) { return yAxis(d.Name); })
    .attr("y2", function(d) { return yAxis(d.Name); })
    .attr("stroke", "grey");

// Circles -> start at priors
plotGroup.selectAll("mycircle")
  .data(data)
  .enter()
  .append("circle")
    .attr("cx", function(d) { return xAxis(d.Prior); })
    .attr("cy", function(d) { return yAxis(d.Name); })
    .attr("r", "7")
    .style("fill", "#69b3a2")
    .attr("stroke", "black");

// Change the X coordinates of line and circle
plotGroup.selectAll("circle")
  .transition()
  .duration(2000)
  .attr("cx", function(d) { return xAxis(d.Posterior); });

plotGroup.selectAll("line")
  .transition()
  .duration(2000)
  .attr("x2", function(d) { return xAxis(d.Posterior); });

// Add a dashed vertical line at x = 0.5
plotGroup.append("line")
    .attr("x1", xAxis(0.5))
    .attr("y1", 0)
    .attr("x2", xAxis(0.5))
    .attr("y2", plotHeight)
    .attr("stroke", "black")
    .attr("stroke-dasharray", "4");  // Make the line dashed

// Add title starting from the left
svg.append("text")
    .attr("x", margin.left)  // Start from the left margin
    .attr("y", margin.top / 2)
    .attr("text-anchor", "start")  // Align to the start (left)
    .attr("font-size", bigText)
    .attr("font-family", "Roboto, sans-serif")
    .attr("font-weight", "bold")
    .text("Probability of a positive impact");

// Add a button
const buttonWidth = 100;
const buttonHeight = 30;

let buttonText = "Posterior"; // Initial text

const button = svg.append("rect")
    .attr("x", plotWidth + margin.left - buttonWidth)
    .attr("y", margin.top / 2 - buttonHeight / 2)
    .attr("width", buttonWidth)
    .attr("height", buttonHeight)
    .attr("rx", 5) // rounded corners
    .attr("ry", 5)
    .attr("fill", "blue")  // Button color
    .attr("cursor", "pointer") // Change cursor on hover
    .on("click", buttonClick);

const buttonTextElement = svg.append("text")
    .attr("x", plotWidth + margin.left - buttonWidth / 2)
    .attr("y", margin.top / 2)
    .attr("text-anchor", "middle")
    .attr("alignment-baseline", "middle")
    .attr("font-family", "Roboto, sans-serif")
    .attr("font-size", mediumText)
    .attr("fill", "white")
    .attr("cursor", "pointer")
    .text(buttonText)
    .on("click", buttonClick);

// Click event handler for the button
function buttonClick() {
    // Toggle between "Posterior" and "Prior"
    buttonText = (buttonText === "Posterior") ? "Prior" : "Posterior";
    buttonTextElement.text(buttonText);

    // Update x-coordinates of circles based on the button text
    plotGroup.selectAll("circle")
      .transition()
      .duration(2000)
      .attr("cx", function(d) {return buttonText === "Posterior" ? xAxis(d.Posterior) : xAxis(d.Prior); });

  // Update x2-coordinates of lines based on the button text
    plotGroup.selectAll("line.line")
      .transition()
      .duration(2000)
      .attr("x2", function(d) {return buttonText === "Posterior" ? xAxis(d.Posterior) : xAxis(d.Prior); });

}
<script src="https://d3js.org/d3.v7.min.js"></script>

<div id="my_dataviz"></div>