skip to Main Content

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?

2

Answers


  1. Chosen as BEST ANSWER

    I have adapted Mark's answer for my application:

    The radar timeline and plot are now mutually reactive:

    • Playing/dragging the radar timeline updates the plot
    • Hovering over the plot updates the radar timeline
    • (The date/time at the top is also updated)

    Notable changes/improvements to Mark's answer:

    • Make selector more specific: pg = d3.select(div).select('g'). For some reason d3.select('g') did not work in my app (and the selector was too broad, selecting another svg on the page).
    • Don't append (render) red ruleX if value is outside plot's domain.
    • Return a value from render(). (Not needed, but fixes type error.)

    Other major additions:


  2. 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>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search