import type { GraphDataPoint } from '../generated/model'; interface ReadingHistoryGraphProps { data: GraphDataPoint[]; } export interface SVGPoint { x: number; y: number; } function getSVGBezierOpposedLine( pointA: SVGPoint, pointB: SVGPoint ): { Length: number; Angle: number } { const lengthX = pointB.x - pointA.x; const lengthY = pointB.y - pointA.y; return { Length: Math.floor(Math.sqrt(lengthX * lengthX + lengthY * lengthY)), Angle: Math.trunc(Math.atan2(lengthY, lengthX)), }; } function getBezierControlPoint( currentPoint: SVGPoint, prevPoint: SVGPoint | null, nextPoint: SVGPoint | null, isReverse: boolean ): SVGPoint { let pPrev = prevPoint; let pNext = nextPoint; if (!pPrev) { pPrev = currentPoint; } if (!pNext) { pNext = currentPoint; } const smoothingRatio = 0.2; const directionModifier = isReverse ? Math.PI : 0; const opposingLine = getSVGBezierOpposedLine(pPrev, pNext); const lineAngle = opposingLine.Angle + directionModifier; const lineLength = opposingLine.Length * smoothingRatio; return { x: Math.floor(currentPoint.x + Math.trunc(Math.cos(lineAngle) * lineLength)), y: Math.floor(currentPoint.y + Math.trunc(Math.sin(lineAngle) * lineLength)), }; } function getSVGBezierPath(points: SVGPoint[]): string { if (points.length === 0) { return ''; } let bezierSVGPath = ''; for (let index = 0; index < points.length; index++) { const point = points[index]; if (!point) { continue; } if (index === 0) { bezierSVGPath += `M ${point.x},${point.y}`; continue; } const pointMinusOne = points[index - 1]; if (!pointMinusOne) { continue; } const pointPlusOne = points[index + 1] ?? point; const pointMinusTwo = index - 2 >= 0 ? (points[index - 2] ?? null) : null; const startControlPoint = getBezierControlPoint(pointMinusOne, pointMinusTwo, point, false); const endControlPoint = getBezierControlPoint(point, pointMinusOne, pointPlusOne, true); bezierSVGPath += ` C${startControlPoint.x},${startControlPoint.y} ${endControlPoint.x},${endControlPoint.y} ${point.x},${point.y}`; } return bezierSVGPath; } export interface SVGGraphData { LinePoints: SVGPoint[]; BezierPath: string; BezierFill: string; Offset: number; } export function getSVGGraphData( inputData: GraphDataPoint[], svgWidth: number, svgHeight: number ): SVGGraphData { let maxHeight = 0; for (const item of inputData) { if (item.minutes_read > maxHeight) { maxHeight = item.minutes_read; } } const sizePercentage = 0.5; const sizeRatio = maxHeight > 0 ? (svgHeight * sizePercentage) / maxHeight : 0; const blockOffset = inputData.length > 0 ? Math.floor(svgWidth / inputData.length) : 0; const linePoints: SVGPoint[] = []; let maxBX = 0; let maxBY = 0; let minBX = 0; for (let idx = 0; idx < inputData.length; idx++) { const item = inputData[idx]; if (!item) { continue; } const itemSize = Math.floor(item.minutes_read * sizeRatio); const itemY = svgHeight - itemSize; const lineX = (idx + 1) * blockOffset; linePoints.push({ x: lineX, y: itemY }); if (lineX > maxBX) { maxBX = lineX; } if (lineX < minBX) { minBX = lineX; } if (itemY > maxBY) { maxBY = itemY; } } return { LinePoints: linePoints, BezierPath: getSVGBezierPath(linePoints), BezierFill: `L ${Math.floor(maxBX)},${Math.floor(maxBY)} L ${Math.floor(minBX + blockOffset)},${Math.floor(maxBY)} Z`, Offset: blockOffset, }; } function formatDate(dateString: string): string { const date = new Date(dateString); const year = date.getUTCFullYear(); const month = String(date.getUTCMonth() + 1).padStart(2, '0'); const day = String(date.getUTCDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } export default function ReadingHistoryGraph({ data }: ReadingHistoryGraphProps) { const svgWidth = 800; const svgHeight = 70; if (!data || data.length < 2) { return (
No data available