🏠 back to Observable

Horizontal line through max point on dynamic scatter plot (beginner)

Hi, I’m trying to create a dynamic scatter plot where a red line goes through the maximum point (see image below). The points can be dragged around so the red line needs to automatically move if the highest point changes. This is the notebook.

The line correctly appears on the highest point when points are added or removed using click events. However, the line doesn’t change position when the points are dragged. Is anyone able to suggest how I can fix this?

I think my selections / event handling are set up incorrectly but I’m unsure so any help is appreciated!

1 Like

The basic problem is:

  1. You only draw the line once, when the cell updates.
  2. Your cell only “updates” when it gets reevaluated.
  3. Your cell is reevaluated whenever points, which it depends on, is updated.
  4. You’re not updating points while or after you drag a circle.

Approaches and issues:

  • You could update points in dragended to trigger an update, but the line won’t update during drag.
  • If you attempt to update points during drag it will cause your cell to be reevaluated (essentially torn down and rebuilt), which stops the dragging.

Recommendations:

  1. Make your cell a viewof cell with the points as values, and give it complete ownership over the points (i.e. no other cell can update the points). Execute update() in dragged.
  2. To capture add clicks, use a rect (with a transparent fill, if you don’t want a background) that covers the whole SVG area. That way you don’t have to account for bubbling / clicks on other elements.
1 Like

And here’s how I’d go about it:

viewof points = {
  const points = [{x: 100, y:200}];
  
  const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
  // svg.node() is our view, let points be its value.
  svg.node().value = points;
  
  const drag = d3.drag()
    .on("start", function() {
      d3.select(this).attr("stroke", "black");
    })
    .on("drag", (event, d) => {
      d.x = event.x;
      d.y = event.y;
      update();
    })
    .on("end", function() {
      d3.select(this).attr("stroke", null);
    });
  
  // Use a rect in the background to capture click events and handle point creation.
  svg.append("rect")
    .attr("width", width)
    .attr("height", height)
    // Transparent "white". A fill is required to capture pointer events, and "transparent" is not a valid color in SVGs.
    .attr("fill", "#fff0")
    .on("click", event => {
      points.push({x: event.offsetX, y: event.offsetY});
      update();
    });
  
  const line = svg.append("line")
    .attr("stroke", "red")
    .attr("x2", width);

  // Restrict circles to an SVG group, to set common attributes and avoid selecting
  // unwanted elements.
  const circles = svg.append("g")
    .attr("fill", "blue")
    // For demonstration purposes, let the group handle all click events.
    .on("click", event => {
      // Get the point object.
      const point = d3.select(event.target).datum();
      points.splice(points.indexOf(point), 1);
      update();
    });
  
  // Initial drawing.
  update();
  return svg.node();
  
  function update() {
    circles.selectAll("circle")
      .data(points)
      .join(
        // Special handling for new elements only.
        enter => enter.append("circle")
          .attr("r", 10)
          .call(drag)
      )
      // Applies to merged selection of old and new elements.
      .attr("cx", d => d.x)
      .attr("cy", d => d.y);
    
    const y = d3.min(points, p => p.y);
    line.attr("y1", y).attr("y2", y);
    
    // Notify Observable that the points have changed.
    svg.dispatch("input");
  }
}
1 Like

Thank you so much for the clear explanation and code! I have a much better understanding now :slight_smile: