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:



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/plot@0.6"></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/plot@0.6"></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>