2

I use a chart library called nivo. I need to trigger a function each time I click on a specific part of the chart.

To this purpose, I believe it would be efficient to "capture" the pixel color of the clicked element and then trig the right function thanks to this information.

The chart is a high level component that renders a svg on the DOM. A div wraps it.

I think I need to know the position of my mouse within the scope of this component (there could be several charts on the same page, so no document.addEventListener). Here is my code:

const Component = () => {
  return (
    <div>
      <ResponsiveStream data={series} keys={keys}/>
    </div>);
};

How to know the color of the clicked element?

Józef Podlecki
  • 10,453
  • 5
  • 24
  • 50
DoneDeal0
  • 5,273
  • 13
  • 55
  • 114
  • It can't work, because I need to know the color of any part of a svg element when I click on it. This method is to get the background color of a div. – DoneDeal0 Jun 12 '20 at 10:15
  • Yes I saw you updated your question, I'll remove the comment. – Martin Jun 12 '20 at 10:28

1 Answers1

1

ResponsiveStream doesn't take any event handlers nor ref

In that case you can either send feature request in their github page and use pure js.

const Component = () => {
  const containerRef = useRef(null)

  const onChartMouseMove = ({target, screenX, screenY}) => {
    const rect = target.getBoundingClientRect(); // perhaps you'd like to get relative position of mouse from the element
    console.log(rect.left - screenX, rect.top - screenY);
  }

  useEffect(() => {
    const element = containerRef && containerRef.current;

    if(element) {
      const svg = element.querySelector("svg");

      svg.addEventListener("mousemove", onChartMouseMove)

      return () => {
        svg.removeEventListener("mousemove", onChartMouseMove);
      }
    }
  }, [containerRef])

  return (
    <div ref={containerRef}>
      <ResponsiveStream data={series} keys={keys}/>
    </div>);
};

--Edit

Unfortunately nivo doesnt have umd bundle so I prepared a sample component rendering an xml with logic above.

Getting the color is more difficult as you have to convert svg to image then put Image on canvas via CanvasRenderingContext2D and call getImageData

Now with ImageData you have to compute index where the element is located. MouseEvent.offsetX, MouseEvent.offsetY can help you with that.

source

const { useState, useEffect, useRef } = React;

const getValues = () => Array(400).fill(0).map((pr, index) => ({
  id: index,
  row: index % 20,
  column: Math.floor(index / 20),
  value: index * 16000
}))

const getImageDataFromSvg = (svg) => new Promise((resolve, reject) => {
  const canvas = document.createElement("canvas");
  const svgXml = (new XMLSerializer()).serializeToString(svg);

  const image = new Image();
  image.src = "data:image/svg+xml;base64," + btoa(svgXml);
  image.onload = function() {
      
      const width = image.naturalWidth;
      const height = image.naturalHeight;
      canvas.width = width;
      canvas.height = width;
      const context = canvas.getContext('2d');
      context.drawImage(image, 0, 0);
      resolve(context.getImageData(0, 0, width, height));
  }  
})

const ResponsiveStream = () => {
  const [colors, setColors] = useState(getValues());

  const fill = (value) => {
    return '#' + ("000000" + value.toString(16)).substr(-6);
  }

  return <svg width="200" height="200">
    {colors.map(pr => <rect fill={fill(pr.value)} x={pr.row * 10} y={pr.column * 10} width={10} height={10} key={pr.id}></rect>)}
  </svg>
}

const App = () => {
  const containerRef = useRef(null)

  const onChartMouseMove = ({target, offsetX, offsetY}, imageData) => {
    
    let index = (offsetY * imageData.width  + offsetX) * 4;
    const [red, green, blue, alpha] = imageData.data.slice(index, index + 4);
    const hex = '#' + red.toString(16)
    + green.toString(16)
    + blue.toString(16)
    console.log(hex);
        
  }

  useEffect(() => {
    let isUnmounted = false;
    let func = null;
    const element = containerRef && containerRef.current;

    if(element) {
      const svg = element.querySelector("svg");
      
      getImageDataFromSvg(svg)
        .then(imageData => {
          if(isUnmounted) {
            return;
          }
          
          func = (event) => onChartMouseMove(event, imageData)
          svg.addEventListener("mousemove", func)    
        })
      
      return () => {
        isUnmounted = true;
        svg.removeEventListener("mousemove", func);
      }
    }
  }, [containerRef])


return <div>
    <div ref={containerRef}>
      <ResponsiveStream/>
    </div>
</div>
}

ReactDOM.render(
    <App />,
    document.getElementById('root')
  );
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script src="https://unpkg.com/@material-ui/core@latest/umd/material-ui.development.js"></script>
<script src="https://unpkg.com/material-ui-lab-umd@4.0.0-alpha.32/material-ui-lab.development.js"></script>
<div id="root"></div>

--Edit

You have to wrap code in setTimeout if rendering takes some time.

 useEffect(() => {
    let isUnmounted = false;
    let func = null;
    let svg = null
    const element = chartRef && chartRef.current;
    if (element) {

      setTimeout(() => {
        svg = element.querySelector("svg");
        getImageDataFromSvg(svg).then(imageData => {
          if (isUnmounted) {
            return;
          }
          func = event => onChartMouseMove(event, imageData);
          svg.addEventListener("mousemove", func);
        });
      }, 0)

      return () => {
        isUnmounted = true;
        if(svg) {
          svg.removeEventListener("mousemove", func);
        }
      };
    }
  }, [chartRef]);
Józef Podlecki
  • 10,453
  • 5
  • 24
  • 50
  • Thanks. I've just changed one line so the app doesn't crash svg && svg.addEventListener("mousemove", onChartMouseMove(element)); . I also added const before onChartMouseMove. But the function is never triggered. Also, it would just give me the position of my mouse, how would you retrieve the color of the clicked element then? – DoneDeal0 Jun 12 '20 at 10:49
  • Updated answer and provided a snippet. Hope it helps! – Józef Podlecki Jun 12 '20 at 14:25
  • Hi Józef, thank you so much for your help, I really appreciate it. Your last block of code works when generating colors inside a svg, but when I replace it by a real Nivo's streamchart, I have a blank page. Here is your updated code with the streamchart in a sandbox: https://codesandbox.io/s/exciting-haslett-g4hwv?file=/src/App.js . What's missing? – DoneDeal0 Jun 12 '20 at 15:52
  • I wrapped what's inside the `if(element) {` in `setTimeout(() => { }, 10)` and it worked. Updated answer – Józef Podlecki Jun 12 '20 at 15:59