DEV Community

Cover image for Building your own Interactive Line Graph in ReactJS
Manish Kumar Sundriyal for CodeMasheen

Posted on

Building your own Interactive Line Graph in ReactJS

Basic SVG Component

First, let's create a simple SVG component that accepts width and height as props. This will be the starting point for our graph.

import React from "react";

const LineGraph = ({ height, width }) => {
  return <svg height={height} width={width}></svg>;
};

export default LineGraph;
Enter fullscreen mode Exit fullscreen mode

Adding the X-Axis

Now, let's add the X-axis, which runs horizontally across the graph. We’ll use the <line> element for this.

const drawXAxis = () => {
  const middleY = height / 2;

  return (
    <line x1={0} y1={middleY} x2={width} y2={middleY} stroke={lineColor} />
  );
};
Enter fullscreen mode Exit fullscreen mode

Adding the Y-Axis

We’ll use another <line> element to draw the Y-axis, which will run vertically through the center of the graph.

const drawYAxis = () => {
  const middleX = width / 2;

  return (
    <line x1={middleX} y1={0} x2={middleX} y2={height} stroke={lineColor} />
  );
};
Enter fullscreen mode Exit fullscreen mode

Plotting coordinates as a line path

The key part of a line graph is the line connecting different points. Let's plot some sample coordinates and connect them using an SVG .

const drawPath = () => {
    const pathData = coordinates
      .map((coordinate, index) =>
        index === 0
          ? `M ${coordinate.x} ${coordinate.y}`
          : `L ${coordinate.x} ${coordinate.y}`
      )
      .join(" ");

    return <path d={pathData} stroke={pathColor} fill="none" />;
  };

Enter fullscreen mode Exit fullscreen mode

Option to fill area beneath the line

We can fill the area beneath the line with a color to enhance the graph. This can be done using an additional element. Consider prop isFillArea to show/hide this area.

const drawPath = () => {
  const pathData = coordinates
    .map((coordinate, index) =>
      index === 0
        ? `M ${coordinate.x} ${coordinate.y}`
        : `L ${coordinate.x} ${coordinate.y}`
    )
    .join(" ");

  const middleY = height / 2;
  const svgPath = showFillArea
    ? `${pathData} L ${width} ${middleY} L 0 ${middleY} Z`
    : pathData;
  const fillColor = showFillArea ? areaColor : "none";
  return (
    <path d={svgPath} fill={fillColor} stroke={pathColor} opacity="0.5" />
  );
};
Enter fullscreen mode Exit fullscreen mode

Tracking the cursor

Let’s add a circle that follows the cursor's movement across the graph path.

We will need a reference of our SVG component to access the bounding box of the SVG element. Also a reference for our tracking-circle that will be used for tracking the cursor over the graph.

const svgRef = useRef();
const circleRef = useRef();
// ...
const drawTrackingCircle = () => {
  return (
    <circle
      ref={circleRef}
      r={6}
      fill="red"
      style={{ display: "none" }} // Initially hidden
    />
  );
};
// ...
<svg ref={svgRef} width={width} height={height}>
// ...
</svg>
Enter fullscreen mode Exit fullscreen mode

Then, we need to add an event listener to our SVG element. This will listen to all our cursor movements over the graph.

useEffect(() => {
  const svgElement = svgRef.current;
  svgElement.addEventListener("mousemove", handleMouseMove);

  // clean up
  return () => svgElement.removeEventListener("mousemove", handleMouseMove);
}, []);
Enter fullscreen mode Exit fullscreen mode

Next, we need a method to find the intersection coordinate between the cursor position and the path.

const getIntersectionPoint = (cursorX) => {
  // Find the segment (p1, p2) where cursorX lies between two consecutive coordinates.
  const segment = coordinates.find((p1, i) => {
    // Get the next point
    const p2 = coordinates[i + 1]; 
    // Check if cursorX falls between the two coordinates horizontally.
    return (
      p2 &&
      ((p1.x <= cursorX && p2.x >= cursorX) ||
        (p1.x >= cursorX && p2.x <= cursorX))
    );
  });

  // Return null if no valid segment is found.
  if (!segment) return null; 

  // Destructure the two coordinates in the segment.
  const [p1, p2] = [segment, coordinates[coordinates.indexOf(segment) + 1]];

  // Calculate 't' to determine the relative position between p1 and p2.
  const t = (cursorX - p1.x) / (p2.x - p1.x);

  // Interpolate the Y-coordinate using 't'.
  const y = p1.y + t * (p2.y - p1.y);

  return { x: cursorX, y };
};
Enter fullscreen mode Exit fullscreen mode

Cursor movement tracker method. It uses the getIntersectionPoint method to find the current intersection coordinate.

const handleMouseMove = (event) => {
  // Get SVG position
  const svgRect = svgRef.current.getBoundingClientRect();
  // Calculate cursor's X within the SVG
  const cursorX = event.clientX - svgRect.left;

  // Find the intersection point
  const intersectionPoint = getIntersectionPoint(cursorX);
  if (intersectionPoint) {
    // Move the intersection circle to the calculated point
    circleRef.current.setAttribute("cx", intersectionPoint.x);
    circleRef.current.setAttribute("cy", intersectionPoint.y);
    circleRef.current.style.display = "block";
  }
};
Enter fullscreen mode Exit fullscreen mode

Finally, this would be the structure of our graph component

return (
  <svg ref={svgRef} height={height} width={width}>
    {drawPath()}
    {drawXAxis()}
    {drawYAxis()}
    {drawTrackingCircle()}
    {drawDataPointCircles()}
  </svg>
);
Enter fullscreen mode Exit fullscreen mode

This is how we can use our Graph component

<LineGraph
  width={300}
  height={400}
  coordinates={samplePoints}
  lineColor="#000"
  pathColor="#00008B"
  areaColor="#ADD8E6"
  dataPointColor="#008000"
  showFillArea
  showDataPointCircle
/>
Enter fullscreen mode Exit fullscreen mode

Codesandbox link for the LineGraph demo

Blog Photo by Isaac Smith on Unsplash

Thanks for reading ❤

Top comments (1)

Collapse
 
akshay_gupta profile image
Akshay Gupta

Just an extra point for people reading, we can also add event listener for mouseleave along with mousemove, which will hide the circle

  const handleMouseLeave = () => {
    if (circleRef?.current) {
      circleRef.current.style.display = 'none';
    }
  };
Enter fullscreen mode Exit fullscreen mode