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;
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} />
);
};
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} />
);
};
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" />;
};
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" />
);
};
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>
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);
}, []);
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 };
};
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";
}
};
Finally, this would be the structure of our graph component
return (
<svg ref={svgRef} height={height} width={width}>
{drawPath()}
{drawXAxis()}
{drawYAxis()}
{drawTrackingCircle()}
{drawDataPointCircles()}
</svg>
);
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
/>
Codesandbox link for the LineGraph demo
Blog Photo by Isaac Smith on Unsplash
Thanks for reading ❤
Top comments (1)
Just an extra point for people reading, we can also add event listener for
mouseleave
along withmousemove
, which will hide the circle