In this manual, we share our experience of using D3.js in React to build custom, scalable, and engaging charts via creating reusable components. We recommend this article for:
In the next sections, we’d like to discuss how to start working with D3.js, why you should use this library, and how to integrate it with React.
React and D3.js are JavaScript libraries that enable developers to build scalable, customizable, and appealing data-driven UI. We used these libraries to build one of the Grid Dynamics’ R&D projects called Trading Robo-Advisor. It is a software-based product designed for automated investment and asset management that is done largely through algorithms with minimal to zero human interactions. The idea was to create both - the algorithms to calculate users’ portfolios and automatically buy or sell stocks from the market, and the UI application (for Web and iOS) to show all this data to the user.
In this article, we share how we implemented charts in our Web application using both React and D3.js.
Large datasets visualization is as valuable as implementing modern interactive Web pages for JavaScript developers. D3.js library enables developers to build informative, comprehensive, and scalable visual data representations via shapes, lines, interpolation, etc. These features make D3.js a solid solution if you need to construct charts with unique style or animations, a large set of options, and custom handlers that the ready-to-use chart library can't provide.
In 8 steps, we developed a multiline chart from scratch using D3.js and React libraries. Find all details in the sections below.
We checked the D3.js API and didn’t find any examples there. So, we followed the provided links to the third-party examples. To get started, we could, as well, just search what we needed on the Internet. There are tons of solutions from other developers ready for reuse and extension.
When checking the documentation, we saw that D3.js had a strong set for manipulation with data. There were a lot of computing functions and methods to visualize data. As D3.js can calculate and modify anything for us, all we should do is visualize it. Since React also operates with DOM, we should follow the way of separating the responsibility in DOM manipulation.
Our task was to find a way to let D3.js work based on math, analytics, animations, scaling, and React work based on rendering the view. Keeping it in mind, we decided to start with the simple and quick solution described below.
We started with creating an <svg /> element in our component, saving the reference to it, and then adding all needed chart elements using D3 after SVG was mounted to the DOM.
As an example, we drew a multiline chart. This chart showed the performance of an investment user portfolio for the selected period of time. Also, it allowed us to compare user portfolio performance with other indexes from the market.
/** App.js */
import React from "react";
import MultilineChart from "./views/MultilineChart";
import schc from "./SCHC.json";
import vcit from "./VCIT.json";
import portfolio from "./PORTFOLIO.json";
const portfolioData = { name: "Portfolio", color: "#ffffff", items: portfolio };
const schcData = { name: "SCHC", color: "#d53e4f", items: schc };
const vcitData = { name: "VCIT", color: "#5e4fa2", items: vcit };
const dimensions = {
width: 600,
height: 300,
margin: { top: 30, right: 30, bottom: 30, left: 60 }
};
export default function App() {
return (
<div className="App">
<MultilineChart
data={[portfolioData, schcData, vcitData]}
dimensions={dimensions}
/>
/** MultilineChart.js */
import React from "react";
import * as d3 from "d3";
const MultilineChart = ({ data, dimensions }) => {
const svgRef = React.useRef(null);
const { width, height, margin } = dimensions;
const svgWidth = width + margin.left + margin.right;
const svgHeight = height + margin.top + margin.bottom;
React.useEffect(() => {
const xScale = d3.scaleTime()
.domain(d3.extent(data[0].items, (d) => d.date))
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([
d3.min(data[0].items, (d) => d.value) - 50,
d3.max(data[0].items, (d) => d.value) + 50
])
.range([height, 0]);
// Create root container where we will append all other chart elements
const svgEl = d3.select(svgRef.current);
svgEl.selectAll("*").remove(); // Clear svg content before adding new elements
const svg = svgEl
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Add X grid lines with labels
const xAxis = d3.axisBottom(xScale)
.ticks(5)
.tickSize(-height + margin.bottom);
const xAxisGroup = svg.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(xAxis);
xAxisGroup.select(".domain").remove();
xAxisGroup.selectAll("line").attr("stroke", "rgba(255, 255, 255, 0.2)");
xAxisGroup.selectAll("text")
.attr("opacity", 0.5)
.attr("color", "white")
.attr("font-size", "0.75rem");
// Add Y grid lines with labels
const yAxis = d3.axisLeft(yScale)
.ticks(5)
.tickSize(-width)
.tickFormat((val) => `${val}%`);
const yAxisGroup = svg.append("g").call(yAxis);
yAxisGroup.select(".domain").remove();
yAxisGroup.selectAll("line").attr("stroke", "rgba(255, 255, 255, 0.2)");
yAxisGroup.selectAll("text")
.attr("opacity", 0.5)
.attr("color", "white")
.attr("font-size", "0.75rem");
// Draw the lines
const line = d3.line()
.x((d) => xScale(d.date))
.y((d) => yScale(d.value));
svg.selectAll(".line")
.data(data)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", (d) => d.color)
.attr("stroke-width", 3)
.attr("d", (d) => line(d.items));
}, [data]); // Redraw chart if data changes
return <svg ref={svgRef} width={svgWidth} height={svgHeight} />;
};
export default MultilineChart;
All react d3 examples from this tutorial you can find in codesandbox.
To see how the chart was rendered using this code, go to https://codesandbox.io/s/d3react-multiline-chart-version-1-simple-5nxwo
As a result, we created a chart to display the user's portfolio performance (white line) compared to the market’s stocks, marked with pink (SCHC) and purple (VCIT) colors.
The D3.js API provides excellent tooling for work with streams. We can easily select the relevant nodes downstream and manipulate their attributes or retrieve the necessary, filtered collection. See how we added some interaction—choosing the lines to be displayed—below:
/** App.js */
***
import Legend from "./views/Legend";
***
export default function App() {
const [selectedItems, setSelectedItems] = React.useState([]);
const legendData = [portfolioData, schcData, vcitData];
const chartData = [
portfolioData,
...[schcData, vcitData].filter((d) => selectedItems.includes(d.name))
];
const onChangeSelection = (name) => {
const newSelectedItems = selectedItems.includes(name)
? selectedItems.filter((item) => item !== name)
: [...selectedItems, name];
setSelectedItems(newSelectedItems);
};
return (
);
}
/** Legend.js */
import React from "react";
const Legend = ({ data, selectedItems, onChange }) => (
<div className="legendContainer">
{data.map((d) => (
<div style={{ color: d.color }} key={d.name}>
<label>
{d.name !== "Portfolio" && (
<input
type="checkbox"
checked={selectedItems.includes(d.name)}
onChange={() => onChange(d.name)}
/>
)}
{d.name}
</label>
</div>
))}
</div>
);
export default Legend;
Now, we allowed users to select a necessary market stock to compare it with their portfolio performance. To see how the chart was rendered using this code, go to https://codesandbox.io/s/d3react-multiline-chart-version-2-interaction-4uld2.
Modern user-interactive charts claimed to be built with smooth and appealing transitions. The D3.js API gives the opportunity to animate elements using transitions, interpolations, etc. Here is how we added some animation.
/** MuiltilineCharts.js */
***
const MultilineChart = ({ data, dimentions }) => {
const svgRef = React.useRef(null);
// to detect what line to animate we should store previous data state
const [prevItems, setPrevItems] = React.useState([]);
***
React.useEffect(() => {
***
const line = d3.line()
.x((d) => xScale(d.date))
.y((d) => yScale(d.value));
const lines = svg.selectAll(".line")
.data(data)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", (d) => d.color)
.attr("stroke-width", 3)
.attr("d", (d) => line(d.items));
// Use stroke-dashoffset for transition
lines.each((d, i, nodes) => {
const element = nodes[i];
const length = element.getTotalLength();
if (!prevItems.includes(d.name)) {
d3.select(element)
.attr("stroke-dasharray", `${length},${length}`)
.attr("stroke-dashoffset", length)
.transition()
.duration(750)
.ease(d3.easeLinear)
.attr("stroke-dashoffset", 0);
}
});
setPrevItems(data.map(({ name }) => name));
}, [data, margin]);
return <svg ref={svgRef} width={svgWidth} height={svgHeight} />;
};
To see how the chart was rendered using this code, go to https://codesandbox.io/s/d3react-multiline-chart-version-3-animation-o5y57
The chart provided in the article was built with SVG; the animation was added using D3.js via d3-transition and d3-selection on SVG nodes. D3.js helped with displaying and animating data with efficient performance. It was possible to use animation with both Canvas and SVG. The Trading Robo-Advisor project requested the user's interaction in charts and handling the user’s interactions with adding and deleting data for rendered charts. After carrying off these points, we decided to choose SVG to implement reusable components as the data visualization blocks.
After adding new functionality to our chart, the useEffect() hook became bigger and made it difficult to figure out what line of code it was responsible for. Usually, at this stage, one function could be split to separate functions that we could dispatch. In some cases, it can even be possible to extract these functions to utils and use them later in other chart components.
/** utils.js */
import * as d3 from "d3";
export const getXScale = (data, width) => d3.scaleTime()
.domain(d3.extent(data, (d) => d.date))
.range([0, width]);
export const getYScale = (data, height, intention) => d3.scaleLinear()
.domain([
d3.min(data, (d) => d.value) - intention,
d3.max(data, (d) => d.value) + intention
])
.range([height, 0]);
const applyAxisStyles = (container) => {
container.select(".domain").remove();
container.selectAll("line").attr("stroke", "rgba(255, 255, 255, 0.2)");
return container.selectAll("text")
.attr("opacity", 0.5)
.attr("color", "white")
.attr("font-size", "0.75rem");
};
export const drawAxis = ({
container, xScale, yScale, ticks, tickSize, tickFormat, transform
}) => {
const scale = xScale || yScale;
const axisType = xScale ? d3.axisBottom : d3.axisLeft;
const axis = axisType(scale)
.ticks(ticks)
.tickSize(tickSize)
.tickFormat(tickFormat);
const axisGroup = container.append("g")
.attr("class", "axis")
.attr("transform", transform)
.call(axis);
return applyAxisStyles(axisGroup);
};
export const drawLine = ({ container, data, xScale, yScale }) => {
const line = d3.line()
.x((d) => xScale(d.date))
.y((d) => yScale(d.value));
return container.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", (d) => d.color)
.attr("stroke-width", 3)
.attr("d", (d) => line(d.items));
};
export const animateLine = ({ element }) => {
const length = element.getTotalLength();
return d3.select(element)
.attr("stroke-dasharray", `${length},${length}`)
.attr("stroke-dashoffset", length)
.transition()
.duration(750)
.ease(d3.easeLinear)
.attr("stroke-dashoffset", 0);
};
In our MultilineChart component, we had two useEffect() hooks. One hook was used for drawing axes with scale and dimension in dependencies as it may re-render when data or size has changed. This approach saved our performance from getting low and laggy. Another hook was used for drawing the lines with scale and data in dependencies. In both hooks, do not forget to remove old svg elements before adding new ones.
/** MultilineChart.js */
import React from "react";
import * as d3 from "d3";
import { getXScale, getYScale, drawAxis, drawLine, animateLine } from "../../utils";
const MultilineChart = ({ data = [], dimensions = {} }) => {
***
const [portfolioData] = data;
const xScale = React.useMemo(
() => getXScale(portfolioData.items, width),
[portfolioData, width]
);
const yScale = React.useMemo(
() => getYScale(portfolioData.items, height, 50),
[portfolioData, height]
);
React.useEffect(() => {
const svg = d3.select(".container");
svg.selectAll(".axis").remove();
// Add X grid lines with labels
drawAxis({
xScale,
container: svg,
tickSize: -height + margin.bottom,
ticks: 5,
transform: `translate(0, ${height - margin.bottom})`
});
// Add Y grid lines with labels
drawAxis({
yScale,
container: svg,
tickSize: -width,
ticks: 5,
tickFormat: (val) => `${val}%`
});
}, [xScale, yScale, width, height, margin]);
React.useEffect(() => {
const svg = d3.select(".container");
svg.selectAll("path").remove();
// Draw the lines
data.forEach((d) => {
const line = drawLine({ container: svg, data: d, xScale, yScale });
if (!prevItems.includes(d.name)) {
animateLine({ element: line.node() });
}
});
setPrevItems(data.map(({ name }) => name));
}, [data, xScale, yScale]);
return (
<svg ref={svgRef} width={svgWidth} height={svgHeight}>
{/* As g element is a container for axises and lines, */}
{/* we moved it from hook to jsx directly */}
<g className="container" transform={`translate(${margin.left},${margin.top})`}/>
</svg>
);
};
export default MultilineChart;
The chart looked much better, but it still didn’t come as natural for React developers. It was like adding a <div /> element to the DOM using React, and then including jQuery to the project and starting using it for DOM manipulation. It made sense to split our chart to separate components instead of separate methods, which were actually just manipulating mounted DOM elements.
First, we looked at the elements tree that D3 constructed for us and thought about what we could add to the separate components.
Then, we splitted the elements into separate components — Axis returned a g element, and Line returned a path. Also we created a GridLine component and separated it from Axis, so we would have more control. For example, we could draw grid lines for 0, max, and min values, and title only for max and min values.
/** GridLine.js */
import React from "react";
import PropTypes from "prop-types";
import * as d3 from "d3";
const GridLine = ({
type, scale, ticks, size, transform, disableAnimation, ...props
}) => {
const ref = React.useRef(null);
React.useEffect(() => {
const axisGenerator = type === "vertical" ? d3.axisBottom : d3.axisLeft;
const axis = axisGenerator(scale).ticks(ticks).tickSize(-size);
const gridGroup = d3.select(ref.current);
if (disableAnimation) {
gridGroup.call(axis);
} else {
gridGroup.transition().duration(750).ease(d3.easeLinear).call(axis);
}
gridGroup.select(".domain").remove();
gridGroup.selectAll("text").remove();
gridGroup.selectAll("line").attr("stroke", "rgba(255, 255, 255, 0.1)");
}, [scale, ticks, size, disableAnimation]);
return <g ref={ref} transform={transform} {...props} />;
};
GridLine.propTypes = {
type: PropTypes.oneOf(["vertical", "horizontal"]).isRequired
};
export default GridLine;
/** Axis.js */
import React from "react";
import PropTypes from "prop-types";
import * as d3 from "d3";
const Axis = ({
type, scale, ticks, transform, tickFormat, disableAnimation, ...props
}) => {
const ref = React.useRef(null);
React.useEffect(() => {
const axisGenerator = type === "left" ? d3.axisLeft : d3.axisBottom;
const axis = axisGenerator(scale).ticks(ticks).tickFormat(tickFormat);
const axisGroup = d3.select(ref.current);
if (disableAnimation) {
axisGroup.call(axis);
} else {
axisGroup.transition().duration(750).ease(d3.easeLinear).call(axis);
}
axisGroup.select(".domain").remove();
axisGroup.selectAll("line").remove();
axisGroup.selectAll("text")
.attr("opacity", 0.5)
.attr("color", "white")
.attr("font-size", "0.75rem");
}, [scale, ticks, tickFormat, disableAnimation]);
return <g ref={ref} transform={transform} {...props} />;
};
Axis.propTypes = {
type: PropTypes.oneOf(["left", "bottom"]).isRequired
};
export default Axis;
/** Line.js */
import React from "react";
import * as d3 from "d3";
const Line = ({
xScale, yScale, color, data, animation, ...props
}) => {
const ref = React.useRef(null);
// Define different types of animation that we can use
const animateLeft = React.useCallback(() => {
const totalLength = ref.current.getTotalLength();
d3.select(ref.current)
.attr("opacity", 1)
.attr("stroke-dasharray", `${totalLength},${totalLength}`)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(750)
.ease(d3.easeLinear)
.attr("stroke-dashoffset", 0);
}, []);
const animateFadeIn = React.useCallback(() => {
d3.select(ref.current)
.transition()
.duration(750)
.ease(d3.easeLinear)
.attr("opacity", 1);
}, []);
const noneAnimation = React.useCallback(() => {
d3.select(ref.current).attr("opacity", 1);
}, []);
React.useEffect(() => {
switch (animation) {
case "left":
animateLeft();
break;
case "fadeIn":
animateFadeIn();
break;
case "none":
default:
noneAnimation();
break;
}
}, [animateLeft, animateFadeIn, noneAnimation, animation]);
// Recalculate line length if scale has changed
React.useEffect(() => {
if (animation === "left") {
const totalLength = ref.current.getTotalLength();
d3.select(ref.current).attr(
"stroke-dasharray",
`${totalLength},${totalLength}`
);
}
}, [xScale, yScale, animation]);
const line = d3.line()
.x((d) => xScale(d.date))
.y((d) => yScale(d.value));
return (
<path
ref={ref}
d={line(data)}
stroke={color}
strokeWidth={3}
fill="none"
opacity={0}
{...props}
/>
);
};
export default Line;
For the portfolio line we can also add gradient areas just to make our chart look more appealing.
/** Area.js */
import React from "react";
import * as d3 from "d3";
const Area = ({ xScale, yScale, color, data, disableAnimation, ...props }) => {
const ref = React.useRef(null);
React.useEffect(() => {
if (disableAnimation) {
d3.select(ref.current).attr("opacity", 1);
return;
}
d3.select(ref.current).transition()
.duration(750)
.ease(d3.easeBackIn)
.attr("opacity", 1);
}, [disableAnimation]);
const d = React.useMemo(() => {
const area = d3.area()
.x(({ date }) => xScale(date))
.y1(({ value }) => yScale(value))
.y0(() => yScale(yScale.domain()[0]));
return area(data);
}, [xScale, yScale, data]);
return (
<>
<path ref={ref} d={d} fill={`url(#gradient-${color})`} opacity={0} {...props}/>
<defs>
<linearGradient id={`gradient-${color}`} x1="0%" x2="0%" y1="0%" y2="100%">
<stop offset="0%" stopColor={color} stopOpacity={0.2} />
<stop offset="100%" stopColor={color} stopOpacity={0} />
</linearGradient>
</defs>
</>
);
};
export default Area;
/** MultilineChart.js */
import React from "react";
import { Line, Axis, GridLine, Area } from "../components";
import useController from "./MultilineChart.controller";
const MultilineChart = ({ data = [], dimensions = {} }) => {
const { width, height, margin = {} } = dimensions;
const svgWidth = width + margin.left + margin.right;
const svgHeight = height + margin.top + margin.bottom;
const controller = useController({ data, width, height });
const { yTickFormat, xScale, yScale, yScaleForAxis } = controller;
return (
<svg width={svgWidth} height={svgHeight}>
<g transform={`translate(${margin.left},${margin.top})`}>
<GridLine
type="vertical"
scale={xScale}
ticks={5}
size={height}
transform={`translate(0, ${height})`}
/>
<GridLine type="horizontal" scale={yScaleForAxis} ticks={2} size={width} />
<GridLine
type="horizontal"
className="baseGridLine"
scale={yScale}
ticks={1}
size={width}
disableAnimation
/>
{data.map(({ name, items = [], color }) => (
<Line
key={name}
data={items}
xScale={xScale}
yScale={yScale}
color={color}
animation="left"
/>
))}
<Area
data={data[0].items}
color={data[0].color}
xScale={xScale}
yScale={yScale}
/>
<Axis
type="left"
scale={yScaleForAxis}
transform="translate(0, -10)"
ticks={5}
tickFormat={yTickFormat}
/>
<Axis
type="bottom"
className="axisX"
scale={xScale}
transform={`translate(10, ${height - height / 6})`}
ticks={5}
/>
</g>
</svg>
);
};
export default MultilineChart;
/** MultilineChart.controller.js */
import { useMemo } from "react";
import * as d3 from "d3";
const useController = ({ data, width, height }) => {
const xMin = useMemo(
() => d3.min(data, ({ items }) => d3.min(items, ({ date }) => date)),
[data]
);
const xMax = useMemo(
() => d3.max(data, ({ items }) => d3.max(items, ({ date }) => date)),
[data]
);
const xScale = useMemo(
() => d3.scaleTime().domain([xMin, xMax]).range([0, width]),
[xMin, xMax, width]
);
const yMin = useMemo(
() => d3.min(data, ({ items }) => d3.min(items, ({ value }) => value)),
[data]
);
const yMax = useMemo(
() => d3.max(data, ({ items }) => d3.max(items, ({ value }) => value)),
[data]
);
const yScale = useMemo(() => {
const indention = (yMax - yMin) * 0.5;
return d3.scaleLinear()
.domain([yMin - indention, yMax + indention])
.range([height, 0]);
}, [height, yMin, yMax]);
const yScaleForAxis = useMemo(
() => d3.scaleBand().domain([yMin, yMax]).range([height, 0]),
[height, yMin, yMax]
);
const yTickFormat = (d) =>
`${parseFloat(d) > 0 ? "+" : ""}${d3.format(".2%")(d / 100)}`;
return {
yTickFormat,
xScale,
yScale,
yScaleForAxis
};
};
export default useController;
To see how the chart is rendered using this code, go to
https://codesandbox.io/s/d3react-multiline-chart-version-5-separate-components-k1k5t
At this point, it turned out that we used D3.js just to calculate data for each element, define x and y scales, and add animations as we planned in the beginning.
Using this approach we could easily add other components to our chart, and also use these components for other types of charts. Another advantage was that we did not need to clear the svg content before each rerender: we had to do something like svg.selectAll(".axis").remove(). Every component was independent and had its own life cycle, and knew when to rerender and when to stay the same. It was a great performance improvement, we just had to pay attention to what to put into the hook dependency array.
As we were building an informative and user-interactive chart, we decided to add some mouse events to our chart, namely tooltip and highlighting bottom axis text on hover. These additions helped us to show data in a more comprehensive and engaging way to our users.
To track mouse events, we needed an area which was invisible and had the size of our svg. The reference to this component we stored in Multiline.js and passed it to the Axis and Tooltip components as an anchorEl.
/** Overlay.js */
import React from "react";
/**
* Use Overlay as a wrapper for components that need mouse events to be handled.
* For example: Tooltip, AxisX.
*/
const Overlay = React.forwardRef(({ width, height, children }, ref) => (
<g>
{children}
<rect ref={ref} width={width} height={height} opacity={0} />
</g>
));
export default Overlay;
Then, we needed to modify our Axis component and add an event listener using D3.js.
/** AxisX.js */
import React from "react";
import * as d3 from "d3";
const Axis = ({ *** anchorEl, ...props}) => {
const ref = React.useRef(null);
***
// Add new hook to handle mouse events
React.useEffect(() => {
d3.select(anchorEl)
.on("mouseout.axisX", () => {
d3.select(ref.current)
.selectAll("text")
.attr("opacity", 0.5)
.style("font-weight", "normal");
})
.on("mousemove.axisX", () => {
const [x] = d3.mouse(anchorEl);
const xDate = scale.invert(x);
const textElements = d3.select(ref.current).selectAll("text");
const data = textElements.data();
const index = d3.bisector((d) => d).left(data, xDate);
textElement
.attr("opacity", (d, i) => (i === index - 1 ? 1 : 0.5))
.style("font-weight", (d, i) => i === index - 1 ? "bold" : "normal");
});
}, [anchorEl, scale]);
return <g ref={ref} transform={transform} {...props} />;
};
export default Axis;
/** MultilineChart.js */
import React from "react";
import { Line, Axis, GridLine, Area, Overlay, Tooltip } from "../components";
import useController from "./MultilineChart.controller";
import useDimensions from "../../utils/useDimensions";
const MultilineChart = ({ data = [], margin = {} }) => {
// Add ref for overlay that we will use as anchorEl in Axis
const overlayRef = React.useRef(null);
// Add useDimensions custom hook to resize our chart depending on screen size
const [containerRef, { svgWidth, svgHeight, width, height }] = useDimensions({
maxHeight: 400,
margin
});
const controller = useController({ data, width, height });
const { yTickFormat, xScale, yScale, yScaleForAxis } = controller;
return (
<svg width={svgWidth} height={svgHeight}>
<g transform={`translate(${margin.left},${margin.top})`}>
***
<Axis
type="left"
scale={yScaleForAxis}
transform="translate(0, -10)"
ticks={5}
tickFormat={yTickFormat}
/>
<Overlay ref={overlayRef} width={innerWidth} height={innerHeight}>
<Axis
type="bottom"
className="axisX"
anchorEl={overlayRef.current}
scale={xScale}
transform={`translate(10, ${height - height / 6})`}
ticks={5}
/>
<Tooltip
className="tooltip"
anchorEl={overlayRef.current}
width={width}
height={height}
margin={margin}
xScale={xScale}
yScale={yScale}
data={data}
/>
</Overlay>
</g>
</svg>
);
};
export default MultilineChart;
As a result, we’ve implemented reusable and scalable components. For the final implementation of our chart along with Tooltip code and some useful custom hooks like useDimenssion, visit https://codesandbox.io/s/d3react-multiline-chart-version-6-separate-components-tooltip-u9igm.
After we had built our final implementation, we started testing it to ensure that our application was reliable and fault tolerant.
Here we took the Line as an example and defined the testing strategy to follow.
First we looked at jsx.
<path
ref={ref}
d={line(data)}
stroke={color}
strokeWidth={3}
fill="none"
opacity={0}
{...props}
/>
The “d” attribute defined the path to be drawn. According to the documentation, it should be a string containing some of 20 commands (letters), numbers, commas, and spaces. We also can check that it didn’t contain NaN or undefined that meant bad calculation.
/** Line.test.js */
import React from "react";
import { render, waitFor } from "@testing-library/react";
import * as d3 from "d3";
import Line from "../Line";
describe("test Line component", () => {
const TOTAL_LENGTH = 500;
const xScale = d3.scaleTime();
const yScale = d3.scaleLinear();
const data = [
{ date: new Date("2018-01-01"), value: 100 },
{ date: new Date("2019-01-01"), value: 200 },
{ date: new Date("2020-01-01"), value: 300 }
];
beforeAll(() => {
// Add getTotalLength support for jsdom
if (!SVGElement.prototype.getTotalLength) {
SVGElement.prototype.getTotalLength = () => TOTAL_LENGTH;
}
});
test("should render path with proper d attribute", () => {
const { container } = render(
<svg>
<Line xScale={xScale} yScale={yScale} data={data} />
</svg>
);
const path = container.querySelector("path");
expect(path.getAttribute("d")).not.toMatch(/[^MmLlHhVvCcSsQqTtAaZz\.\,\s\d]/gi);
expect(path.getAttribute("d")).not.toMatch(/NaN|undefined/);
expect(path.getAttribute("d")).not.toBe("");
});
test("should render path with custom color and accept additional props", () => {
const color = "red";
const className = "my-class";
const id = "my-id";
const { container } = render(
<svg>
<Line
xScale={xScale}
yScale={yScale}
data={data}
id={id}
className={className}
color={color}
/>
</svg>
);
const path = container.querySelector("path");
expect(path.getAttribute("stroke")).toBe(color);
expect(path.getAttribute("id")).toBe(id);
expect(path.getAttribute("class")).toBe(className);
});
test("should handle fadeIn animation", async () => {
const { container } = render(
<svg>
<Line xScale={xScale} yScale={yScale} data={data} animation="fadeIn" />
</svg>
);
const path = container.querySelector("path");
expect(path.getAttribute("opacity")).toBe("0");
expect(path.getAttribute("stroke-dasharray")).toBe(null);
await waitFor(() => expect(path.getAttribute("opacity")).toBe("1"));
});
test("should handle left animation", async () => {
const { container } = render(
<svg>
<Line xScale={xScale} yScale={yScale} data={data} animation="left" />
</svg>
);
const path = container.querySelector("path");
expect(path.getAttribute("opacity")).toBe("1");
expect(path.getAttribute("stroke-dasharray")).toBe(
`${TOTAL_LENGTH},${TOTAL_LENGTH}`
);
expect(path.getAttribute("stroke-dashoffset")).toBe(`${TOTAL_LENGTH}`);
await waitFor(() => {
expect(path.getAttribute("opacity")).toBe("1");
expect(path.getAttribute("stroke-dasharray")).toBe(
`${TOTAL_LENGTH},${TOTAL_LENGTH}`
);
expect(path.getAttribute("stroke-dashoffset")).toBe("0");
});
});
});
To sum this up, we have successfully used the D3.js and React libraries when working on the Trading Robo-Advisor R&D project. In this article, we described how we
So, if you feel like getting started with D3.js is challenging, just give it a try. This is a proven way to visualize data on a high level, and after you learn the fundamentals, you will be able to visualize data via attractive, interactive, and scalable charts.