Search code examples
javascriptd3.jsplotobservable-plot

Get Observable Plot x,y data values from mouse (hover) position


enter image description here

Question: How can I get the x/y data values from an Obervable Plot for the point the mouse is hovering over? (Or touched on mobile.)

Background: I am trying to make an interactive timeline like on https://merrysky.net. So I would like to convert the mouse's X position to the time/temperature value from the data that has been plotted.

Current progress:


  • Currently, the red rule on the plot reactively updates (try playing/dragging the radar timeline.)
  • The next task is to reactively update in the other direction: the time and temperature data that is displayed at the very top change when the mouse hovers over the temperature plot. (The red rule should follow the mouse pointer and the appropriate data should be updated.)

I have examined the Observable Plot documentation. There seems to be API's for updating certain marks when the pointer is near them, but I could not see anything for extracting data from the plot based on mouse position. The Crosshair mark seems to be the closest to what I am looking for.

Perhaps the underlying D3 API must be used?


Solution

  • Wow, Observable Plot is extremely cool. Looks like you can completely replicate your target (and what I did 8 years ago) as easy as:

    const plot = Plot.plot({
      marks: [
        // draw the line graph
        Plot.line(data, { x: 'x', y: 'y' }),
        // add vertical interaction line
        Plot.ruleX(data, Plot.pointerX({ x: 'x', py: 'y', stroke: 'red' })),
        // add interaction dot
        Plot.dot(data, Plot.pointerX({ x: 'x', y: 'y', stroke: 'red' })),      
        // add interaction text
        Plot.text(
          data,
          Plot.pointerX({
            x: 'x',
            y: 'y',
            dx: 20,
            text: 'y',
          })
        ),
      ],
    });
    

    Running example:

    <!DOCTYPE html>
    <div id="myplot"></div>
    <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
    <script src="https://cdn.jsdelivr.net/npm/@observablehq/[email protected]"></script>
    <script type="module">
      const data = [];
      for (let i = 0; i < 20; i++) {
        data.push({
          x: i,
          y: Math.random() * 100,
        });
      }
    
      const plot = Plot.plot({
        marks: [
          Plot.line(data, { x: 'x', y: 'y' }),
          Plot.ruleX(data, Plot.pointerX({ x: 'x', py: 'y', stroke: 'red' })),
          Plot.dot(data, Plot.pointerX({ x: 'x', y: 'y', stroke: 'red' })),
          Plot.text(
            data,
            Plot.pointerX({
              x: 'x',
              y: 'y',
              dx: 20,
              text: 'y',
            })
          ),
        ],
      });
    
      const div = document.querySelector('#myplot');
      div.append(plot);
    </script>

    -- Edits based on comment --

    How about something like this. It provides an entry point to the plot so you can only tear down and recreate what you need:

    <!DOCTYPE html>
    <div id="myplot"></div>
    <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
    <script src="https://cdn.jsdelivr.net/npm/@observablehq/[email protected]"></script>
    <script type="module">
      const div = document.querySelector('#myplot');
    
      const data1 = [];
      for (let i = 0; i < 20; i++) {
        data1.push({
          x: i,
          y: Math.random() * 100,
        });
      }
    
      const plot1 = Plot.ruleX([0], {
        render: (i, s, v, d, c, next) => {
          const g = next(i, s, v, d, c);
          c.ownerSVGElement.updateRuleX = (value) => {
            const pg = d3.select('g');
            pg.select('.custom-rule').remove();
    
            const ig = pg.append('g')
              .attr('class','custom-rule');
            
            ig.append('line')
              .attr('x1', s.x(value))
              .attr('x2', s.x(value))
              .attr('y1', s.y.range()[0])
              .attr('y2', s.y.range()[1])
              .attr('stroke', 'red');
    
            ig.append('circle')
              .attr('r', 5)
              .attr('stroke', 'red')
              .attr('cx', s.x(value))
              .attr('cy', s.y(data1[value].y));
          };
        },
      }).plot({
        marks: [
          Plot.line(data1, {x: 'x', y: 'y'}),
          Plot.ruleX(data1, Plot.pointerX({ x: 'x', py: 'y', stroke: 'red' })),
          Plot.dot(data1, Plot.pointerX({ x: 'x', y: 'y', stroke: 'red' })),
          Plot.text(
            data1,
            Plot.pointerX({
              x: 'x',
              y: 'y',
              dx: 20,
              text: 'y',
            })
          ),
        ],
      });
      div.append(plot1);
    
      const updateRuleExternal = () => {
        const v = Math.floor(Math.random() * (20 - 1 + 1) + 1);
        plot1.updateRuleX(v)
      };
      setInterval(updateRuleExternal, 500);
    
    </script>