2023-02-26 01:12:11 +00:00
|
|
|
import { useEffect, useMemo, useRef } from 'react'
|
2022-09-29 01:59:28 +00:00
|
|
|
import * as d3 from 'd3'
|
|
|
|
import { random } from 'colord'
|
|
|
|
|
|
|
|
type DataPoint = { x: number; y: number }
|
|
|
|
type LineChartProps = {
|
|
|
|
width: number
|
|
|
|
height: number
|
|
|
|
darkMode: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
function getRandomArbitrary(min: number, max: number): number {
|
|
|
|
return Math.random() * (max - min) + min
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateData(width: number, data: Array<DataPoint>): Array<DataPoint> {
|
|
|
|
while (data.length >= width) {
|
|
|
|
data.shift()
|
|
|
|
}
|
|
|
|
const prev = data.at(data.length - 1)
|
|
|
|
if (prev) {
|
|
|
|
data.push({
|
|
|
|
x: prev.x + 1,
|
|
|
|
y: Math.max(0, prev.y + getRandomArbitrary(-120, 120)),
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
data.push({
|
|
|
|
x: 0,
|
|
|
|
y: getRandomArbitrary(0, 100),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
function fetchData() {
|
|
|
|
const storedData =
|
|
|
|
typeof window === 'undefined'
|
|
|
|
? '[]'
|
|
|
|
: localStorage.getItem('chartData') || '[]'
|
|
|
|
const data = JSON.parse(storedData)
|
|
|
|
return data || []
|
|
|
|
}
|
2023-02-26 01:12:11 +00:00
|
|
|
const data = fetchData()
|
2022-09-29 01:59:28 +00:00
|
|
|
|
|
|
|
export const LineChart = ({ width, height, darkMode }: LineChartProps) => {
|
|
|
|
const timerRef = useRef<NodeJS.Timeout>()
|
|
|
|
const svgRef = useRef(null)
|
2023-02-26 01:12:11 +00:00
|
|
|
const stroke = useMemo(() => random(), [])
|
2022-09-29 01:59:28 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const doIt = (data: DataPoint[]) => {
|
|
|
|
data = updateData(width, data)
|
|
|
|
|
|
|
|
// Y axis
|
|
|
|
const [min, max] = d3.extent(data, (d) => d.y)
|
|
|
|
const yScale = d3
|
|
|
|
.scaleLinear()
|
|
|
|
.domain([min || 0, max || 0])
|
|
|
|
.range([height, 0])
|
|
|
|
|
|
|
|
// X axis
|
2023-02-26 01:12:11 +00:00
|
|
|
const [, xMax] = d3.extent(data, (d) => d.x)
|
2022-09-29 01:59:28 +00:00
|
|
|
const xScale = d3
|
|
|
|
.scaleLinear()
|
|
|
|
.domain([(xMax || 0) - width, xMax || 0])
|
|
|
|
.range([0, width])
|
|
|
|
|
|
|
|
if (!svgRef.current) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
d3.select(svgRef.current)
|
|
|
|
.selectAll('rect')
|
|
|
|
.data(data)
|
|
|
|
.join(
|
|
|
|
(enter) =>
|
|
|
|
enter
|
|
|
|
.append('rect')
|
|
|
|
.style(
|
|
|
|
'stroke',
|
|
|
|
darkMode ? stroke.lighten().toHex() : stroke.darken().toHex()
|
|
|
|
)
|
|
|
|
.style('stroke-opacity', '0.7')
|
|
|
|
.attr('x', (d) => xScale(d.x))
|
|
|
|
.attr('y', (d) => yScale(d.y))
|
|
|
|
.attr('height', (d) => yScale((max || 0) - d.y))
|
2023-02-26 01:12:11 +00:00
|
|
|
.attr('width', () => 1),
|
2022-09-29 01:59:28 +00:00
|
|
|
(update) =>
|
|
|
|
update
|
|
|
|
.transition()
|
2022-10-05 01:55:32 +00:00
|
|
|
.duration(1000)
|
2022-09-29 01:59:28 +00:00
|
|
|
.style(
|
|
|
|
'stroke',
|
|
|
|
darkMode ? stroke.lighten().toHex() : stroke.darken().toHex()
|
|
|
|
)
|
|
|
|
.attr('x', (d) => xScale(d.x))
|
|
|
|
.attr('y', (d) => yScale(d.y))
|
|
|
|
.attr('height', (d) => yScale((max || 0) - d.y))
|
2023-02-26 01:12:11 +00:00
|
|
|
.attr('width', () => 1),
|
2022-09-29 01:59:28 +00:00
|
|
|
(exit) => exit.call((d) => d.transition().remove())
|
|
|
|
)
|
|
|
|
|
|
|
|
localStorage.setItem('chartData', JSON.stringify(data))
|
|
|
|
|
|
|
|
timerRef.current = setTimeout(() => {
|
|
|
|
doIt(data)
|
2022-10-05 01:55:32 +00:00
|
|
|
}, 1000)
|
2022-09-29 01:59:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
doIt(data)
|
|
|
|
|
|
|
|
return () => clearTimeout(timerRef.current)
|
|
|
|
}, [darkMode, stroke, height, width])
|
|
|
|
|
|
|
|
return (
|
|
|
|
<svg
|
|
|
|
width={width}
|
|
|
|
height={height}
|
|
|
|
viewBox={`0 0 ${width} ${height}`}
|
|
|
|
preserveAspectRatio="none"
|
|
|
|
ref={svgRef}
|
|
|
|
className="fill-transparent bg-transparent grow"
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|