import * as d3 from "d3"
import { useEffect, useRef } from "react"
import useViewport from '../../utils/Hooks/useViewport'
import PropTypes from 'prop-types'
/**
* A Bar chart showing the user weight and calories burned. Filled by D3.js.
* @name BarChart
* @param {Object} props - props component
* @param {Array<Object>} props.data - user data
* @param {number} props.svgHeight - height of svg container
* @returns {ReactElement} a Bar Chart
* @component
*/
export default function BarChart({data, svgHeight}) {
//svg parent ref
const chartContainerRef = useRef()
//ref for resize event
const updateBars = useRef(false)
//responsive width
const { viewportWidth } = useViewport()
useEffect(() => {
//if resize remove the previous chart
updateBars.current ? d3.select('.barChart-svg').remove() : updateBars.current = true
// re-draw the chart with new dimensions after resize
DrawChart(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, viewportWidth])
const margin = {top: 50, left: 50, right: 20, bottom: 20}
const DrawChart = (data) => {
//dimentions
const graphWidth = parseInt(d3.select(chartContainerRef.current).style('width')) - margin.left - margin.right
const graphHeight = parseInt(d3.select(chartContainerRef.current).style('height')) - margin.top - margin.bottom
// create new chart
const svg = d3.select(chartContainerRef.current)
.append('svg')
.classed('barChart-svg', true)
.attr('width', graphWidth + margin.left + margin.right)
.attr('height', svgHeight)
.style('background-color', '#F5F7F9')
.style('border-radius', '5px')
// chart title
svg.append('text')
.attr("x", margin.right)
.attr("y", 30)
.text('Activité quotidienne')
.style("font-weight", "500")
// X axis
const extent = d3.extent(data.map(d => new Date(d.day).getDate()))
const x_Scale = d3.scaleLinear()
.domain(extent)
.range([margin.left, graphWidth - margin.right])
const x_Axis = d3.axisBottom(x_Scale)
.tickSize(0)
.tickPadding(margin.bottom)
.ticks(7)
svg.append('g')
.call(x_Axis)
.attr('transform', `translate(0, ${svgHeight - margin.top})`)
.attr("font-size", "1rem")
.select('path')
.attr('transform', 'scale(1.03) translate(-11,0)')
// Y axis
const max_weight = d3.max(data, d => d.kilogram)
const max_calories = d3.max(data, d => d.calories)
const y_Weight_Scale = d3.scaleLinear()
.domain([max_weight-12, max_weight+3])
.range([svgHeight - margin.top, margin.bottom])
const y_Calories_Scale = d3.scaleLinear()
.domain([0, max_calories])
.range([0, svgHeight / 2])
const y_Axis = d3.axisRight(y_Weight_Scale)
.ticks(3)
.tickSize(0)
.tickPadding(20)
svg.append('g')
.call(y_Axis)
.attr('transform', `translate(${graphWidth + margin.right}, 0)`)
.attr("font-size", "1rem")
.select('.domain').remove()
//grille
const gridTickValues = y_Axis.scale().ticks(3).slice(1)
const yAxisGrid = d3.axisLeft(y_Weight_Scale)
.tickSize(graphWidth - margin.right -margin.left)
.tickFormat('')
.tickValues(gridTickValues)
svg.append('g')
.style("stroke-dasharray", ("3, 3"))
.style('color', 'lightgray')
.attr('transform', `translate(${graphWidth - margin.right}, 0)`)
.call(yAxisGrid)
.select('path').remove()
//legend
const legend = svg.append('g')
//weight legend
legend.append('circle')
.attr('cx', graphWidth - 190)
.attr('cy', margin.bottom)
.attr('r', 4)
.attr('fill','black')
legend.append('text')
.text('Poids (kg)')
.attr("dx", graphWidth - 180)
.attr('dy', margin.bottom + 5)
.attr('fill', '#74798C')
.style('font-size', '14px')
//calories legend
legend.append('circle')
.attr('cx', graphWidth - 100)
.attr('cy', margin.bottom)
.attr('r', 4)
.attr('fill','#E60000')
//text legend
legend.append('text')
.text('Calories brûlées (kCal)')
.attr("dx", graphWidth - 90)
.attr('dy', margin.bottom + 5)
.attr('fill', '#74798C')
.style('font-size', '14px')
//data
//rounded weight line
svg.append('g')
.selectAll('line')
.data(data)
.enter()
.append('line')
.attr('x1', d => x_Scale(new Date(d.day).getDate()) - 7) // 7px offset to the right from the calorie line
.attr('x2', d => x_Scale(new Date(d.day).getDate()) - 7)
.attr('y1', d => graphHeight + margin.bottom - 5)
.attr('y2', d => graphHeight + margin.bottom - 5)
.attr( 'stroke', "black")
.attr('stroke-width', "8")
.attr('stroke-linecap',"round")
.transition()
.duration(700)
.attr('y2', d => y_Weight_Scale(d.kilogram) + 3)
// rect weight line
svg.append('g')
.selectAll('line')
.data(data)
.enter()
.append('line')
.attr('x1', d => x_Scale(new Date(d.day).getDate()) - 7)
.attr('x2', d => x_Scale(new Date(d.day).getDate()) - 7)
.attr('y1', d => graphHeight + margin.bottom)
.attr('y2', d => graphHeight + margin.bottom)
.transition()
.duration(700)
.attr('y2', d => y_Weight_Scale(d.kilogram)+3)
.attr( 'stroke', "black")
.attr('stroke-width', "8")
.attr('stroke-linecap',"butt")
//rounded calories line
svg.append('g')
.selectAll('line')
.data(data)
.enter()
.append('line')
.attr('x1', d => x_Scale(new Date(d.day).getDate()) + 7)
.attr('x2', d => x_Scale(new Date(d.day).getDate()) + 7)
.attr('y1', d => graphHeight + margin.bottom - 5)
.attr('y2', d => graphHeight + margin.bottom - 5)
.transition()
.duration(700)
.attr('y2', d => graphHeight - y_Calories_Scale(d.calories))
.attr( 'stroke', "#E60000")
.attr('stroke-width', "8")
.attr('stroke-linecap',"round")
//rect calories line
svg.append('g')
.selectAll('line')
.data(data)
.enter()
.append('line')
.attr('x1', d => x_Scale(new Date(d.day).getDate()) + 7)
.attr('x2', d => x_Scale(new Date(d.day).getDate()) + 7)
.attr('y1', d => graphHeight + margin.bottom)
.attr('y2', d => graphHeight + margin.bottom)
.transition()
.duration(700)
.attr('y2', d => graphHeight - y_Calories_Scale(d.calories))
.attr( 'stroke', "#E60000")
.attr('stroke-width', "8")
.attr('stroke-linecap',"butt")
// tooltips
data.forEach((d, index) => {
let group = svg.append("g")
.attr("id", `day${index+1}`)
// create gray rectangles for hover
group.append("rect")
.attr("x", x_Scale(index+1) - 40)
.attr("y", margin.top + 4)
.attr("width", 80)
.attr("height", 195)
.attr('fill', 'gray')
.attr("opacity", "0")
// make it appear on hover + make the infos appears
.on("mouseover", function () {
d3.select(this).transition()
.duration("150")
.attr("opacity", ".2")
d3.selectAll(`#day${index+1} > *:not(:first-child)`).transition()
.duration("150")
.attr("opacity", "1")
})
.on("mouseout", function () {
d3.select(this).transition()
.duration("150")
.attr("opacity", "0")
d3.selectAll(`#day${index+1} > *:not(:first-child)`).transition()
.attr("opacity", "0")
})
// infos bubble
group.append("rect")
.attr("x", displayTooltip(index+1) + 25)
.attr("y", 40)
.attr("width", 60)
.attr("height", 70)
.attr("opacity", "0")
.attr('fill', 'red')
group.append("text")
.attr("x", displayTooltip(index+1) + 55)
.attr("y", 65)
.text(d.kilogram + "Kg")
.style("text-anchor", "middle")
.style("font-size", "13px")
.style('fill', '#fff')
.attr("opacity", "0")
group.append("text")
.attr("x", displayTooltip(index+1) + 55)
.attr("y", 95)
.text(d.calories + "Kcal")
.style("text-anchor", "middle")
.style("font-size", "13px")
.style('fill', '#fff')
.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) -100
}
}
return <div className="barchart-container" ref={chartContainerRef} style={{ height:svgHeight}}>
</div>
}
BarChart.propTypes = {
data: PropTypes.arrayOf(PropTypes.object).isRequired,
svgHeight: PropTypes.number,
}
Source