Source

Components/D3/LineChart.jsx

import * as d3 from 'd3'
import { useEffect, useRef } from 'react'
import useViewport from '../../utils/Hooks/useViewport'
import PropTypes from 'prop-types'

/**
 * A Linear chart showing the user average sessions. Filled by D3.js.
 * @name LinearChart
 * @param {Object} props - props component
 * @param {Array<Object>} props.data - user data
 * @param {number} props.svgHeight - height of svg container
 * @returns {ReactElement} a Linear Chart
 * @component
 */
export default function LinearChart({ data, svgHeight }) {
	//svg parent ref
	const lineContainerRef = useRef()
	//ref for resize event
	const updateLines = useRef(false)
	//responsive width
	const { viewportWidth } = useViewport()

	useEffect(() => {
		//if resize remove the previous chart
		updateLines.current
			? d3.select('.line-chart-svg').remove()
			: (updateLines.current = true)
		DrawChart(data)

		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [data, viewportWidth])

	const margin = { top: 35, left: 20, right: 20, bottom: 35 }

	const DrawChart = (data) => {
		//dimentions
		const graphWidth = parseInt(d3.select(lineContainerRef.current).style('width')) - margin.left - margin.right
		const graphHeight = parseInt(d3.select(lineContainerRef.current).style('height')) - margin.top - margin.bottom
		// create new chart
		const svg = d3
			.select(lineContainerRef.current)
			.append('svg')
			.classed('line-chart-svg', true)
			.attr('width', graphWidth + margin.left + margin.right)
			.attr('height', graphHeight + margin.top + margin.bottom)
			.style('background-color', '#FF0000')
			.style('border-radius', '5px')
		// add a title
		svg.append('text')
			.attr('fill', '#fff')
			.attr('x', margin.right)
			.attr('y', margin.top)
			.text('Durée moyenne des sessions')
			.style('font-size', '1rem')
		// X axis
		const x_Scale = d3
			.scaleLinear()
			.domain([1, 7])
			.range([margin.left, graphWidth + margin.right])

		const tickLabels = ['L', 'M', 'M', 'J', 'V', 'S', 'D']
		const x_Axis = d3
			.axisBottom(x_Scale)
			.tickSize(0)
			.tickPadding(10)
			.ticks(7)
			.tickFormat((d, i) => tickLabels[i])
		const y_Scale = d3
			.scaleLinear()
			.domain([0, d3.max(data, (d) => d.sessionLength)])
			.range([graphHeight, margin.top + margin.bottom])

		svg.append('g')
			.call(x_Axis)
			.attr('color', '#fff')
			.attr('transform', `translate(0, ${graphHeight + margin.top - 10})`)
			.attr('font-size', '1rem')
			.select('.domain')
			.remove()

		//path
		const line = d3
			.line()
			.x((d) => x_Scale(d.day))
			.y((d) => y_Scale(d.sessionLength))
			.curve(d3.curveMonotoneX)

		const path = svg
			.append('path')
			.attr('d', line(data))
			.attr('stroke', '#fff')
			.attr('stroke-width', 2)
			.attr('fill', 'none')

		// animation
		const pathLength = path.node().getTotalLength()
		path.attr('stroke-dashoffset', pathLength)
			.attr('stroke-dasharray', pathLength)
			.transition()
			.duration(1500)
			.attr('stroke-dashoffset', 0)
			.ease(d3.easeSin)

		//tooltips
		data.forEach((coordinates, index) => {
			let group = svg.append('g').attr('id', 'day' + index + 'average')
			group
				.append('rect')
				.attr('x', x_Scale(index + 1))
				.attr('y', 0)
				.attr('width', '100%')
				.attr('height', graphHeight + margin.top + margin.bottom)
				.attr('fill', 'rgba(0, 0, 0, 0.1)')
				.attr('opacity', '0')
			group
				.append('rect')
				.attr('x', displayTooltip(index + 1))
				.attr('y', y_Scale(data[index].sessionLength) - 25)
				.attr('width', 50)
				.attr('height', 20)
				.attr('fill', '#fff')
				.attr('opacity', '0')
			group
				.append('text')
				.attr('x', displayTooltip(index + 1) + 25)
				.attr('y', y_Scale(data[index].sessionLength) - 10)
				.style('text-anchor', 'middle')
				.attr('fill', 'black')
				.text(data[index].sessionLength + 'min')
				.attr('opacity', '0')
			group
				.append('circle')
				.attr('fill', '#fff')
				.attr('cx', x_Scale(index + 1))
				.attr('cy', y_Scale(data[index].sessionLength))
				.attr('r', 4)
				.attr('opacity', '0')
			// hover area
			svg.append('rect')
				.attr('x', x_Scale(index + 1))
				.attr('y', 0)
				.attr('width', graphWidth / 7)
				.attr('height', 300)
				.attr('fill', 'transparent')
				.attr('opacity', '1')
				// make it appear on hover + make the infos appears
				.on('mouseover', function () {
					d3.selectAll(`#day${index}average > *`)
						.transition()
						.attr('opacity', '1')
				})
				.on('mouseout', function () {
					d3.selectAll(`#day${index}average > *`)
						.transition()
						.attr('opacity', '0')
				})
		})
		// Just to be sure a tooltip don't go outside the chart
		function displayTooltip(index) {
			if (x_Scale(index) <= graphWidth - margin.left - margin.right)
				return x_Scale(index)
			else return x_Scale(index) - margin.left - margin.right
		}
	}

	return <div className="line-chart-container" // add a class for styling
			ref={lineContainerRef} style={{height: svgHeight}}></div>
}

LinearChart.propTypes = {
	data: PropTypes.arrayOf(PropTypes.object).isRequired,
	svgHeight: PropTypes.number,
}