d3.jshtmlwidgetsr2d3

Animate line and circle when button is clicked


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


Solution

  • 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>