Skip to content

Instantly share code, notes, and snippets.

@QuadFlask
Last active January 29, 2021 10:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save QuadFlask/a71ab330148d41e4e26d26900e0fd945 to your computer and use it in GitHub Desktop.
Save QuadFlask/a71ab330148d41e4e26d26900e0fd945 to your computer and use it in GitHub Desktop.
import React, {FC} from "react";
const SimpleLineChart: FC<{
width: number;
height: number;
strokeWidth?: number;
strokeColor?: string;
data: { value: number; label?: string }[];
padding?: number;
}> = ({
width,
height,
data,
strokeWidth,
strokeColor = "#000a",
padding = 16,
}) => {
const legendHeight = 12;
const w = width - padding * 2;
const h = height - padding * 2 - legendHeight;
const maxValue = Math.max(...data.map(d => d.value), h, 100);
const ratio = h / maxValue;
const d = buildPath(data.map((d, i) => ([
i / (data.length - 1) * w,
h - d.value * ratio
])));
const d2 = d + `L ${w},${h} L ${0},${h}`;
const id = 'bg-' + funhash(strokeColor);
return <svg width={width} height={height}>
<defs>
<linearGradient id={id} gradientTransform="rotate(90)">
<stop offset="0%" stopColor={strokeColor} stopOpacity={0.25}/>
<stop offset="100%" stopColor={strokeColor} stopOpacity={0}/>
</linearGradient>
</defs>
<g transform={`translate(${padding},${padding})`}>
<path d={d} stroke={strokeColor} strokeWidth={strokeWidth || 4} strokeLinecap={"round"} fill="none"/>
{/* glow */}
<path d={d} stroke={strokeColor} strokeOpacity={0.2} strokeWidth={(strokeWidth || 4) + 2} strokeLinecap={"round"} fill="none"/>
<path d={d} stroke={strokeColor} strokeOpacity={0.1} strokeWidth={(strokeWidth || 4) + 5} strokeLinecap={"round"} fill="none"/>
<path d={d} stroke={strokeColor} strokeOpacity={0.05} strokeWidth={(strokeWidth || 4) + 8} strokeLinecap={"round"} fill="none"/>
{/* bg */}
<path d={d2} stroke={"none"} fill={`url('#${id}')`}/>
{
data.map((d, i) =>
(data.length - i) % 2 === 1
? <text fill="#0008" dy={16} fontSize={10} textAnchor="middle"
x={i / (data.length - 1) * w} y={h}>{d.label}</text>
: null)
}
</g>
</svg>
}
export default SimpleLineChart;
type Point = [number, number];
function buildPath(points: Point[], smoothing = 0.2) {
const line = (pointA: Point, pointB: Point) => {
const lengthX = pointB[0] - pointA[0];
const lengthY = pointB[1] - pointA[1];
return {
length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
angle: Math.atan2(lengthY, lengthX)
};
}
const controlPoint = (current: Point, previous: Point, next: Point, reverse: boolean = false) => {
const p = previous || current;
const n = next || current;
const o = line(p, n);
const angle = o.angle + (reverse ? Math.PI : 0);
const length = o.length * smoothing;
const x = current[0] + Math.cos(angle) * length;
const y = current[1] + Math.sin(angle) * length;
return [x, y];
}
const bezierCommand = (point: Point, i: number, a: Point[]) => {
const [cpsX, cpsY] = controlPoint(a[i - 1], a[i - 2], point);
const [cpeX, cpeY] = controlPoint(point, a[i - 1], a[i + 1], true);
return `C ${cpsX},${cpsY} ${cpeX},${cpeY} ${point[0]},${point[1]}`;
}
return points.reduce((acc, point, i, a) => i === 0
? `M ${point[0]},${point[1]}`
: `${acc} ${bezierCommand(point, i, a)}`
, '');
}
function funhash(s: string) {
for (var i = 0, h = 0xdeadbeef; i < s.length; i++)
h = Math.imul(h ^ s.charCodeAt(i), 2654435761);
return (h ^ h >>> 16) >>> 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment