diff --git a/ui/src/components/LanguageStreamGraph.tsx b/ui/src/components/LanguageStreamGraph.tsx index 49452b1..0b0c966 100644 --- a/ui/src/components/LanguageStreamGraph.tsx +++ b/ui/src/components/LanguageStreamGraph.tsx @@ -99,22 +99,17 @@ export function LanguageStreamGraph() { y1s.push(wy1); } - // Build SVG paths for each language layer + // Build SVG paths for each language layer using smooth curves + const xFor = (w: number) => + weeks.length > 1 ? (w / (weeks.length - 1)) * 100 : 50; + const paths = languages.map((_, l) => { if (weeks.length === 0) return ''; - // Top edge left-to-right - const top = weeks.map((_, w) => { - const x = weeks.length > 1 ? (w / (weeks.length - 1)) * 100 : 50; - return `${x},${y0s[w][l]}`; - }); - // Bottom edge right-to-left - const bottom = weeks - .map((_, w) => { - const x = weeks.length > 1 ? (w / (weeks.length - 1)) * 100 : 50; - return `${x},${y1s[w][l]}`; - }) + const topPts = weeks.map((_, w) => [xFor(w), y0s[w][l]] as [number, number]); + const bottomPts = weeks + .map((_, w) => [xFor(w), y1s[w][l]] as [number, number]) .reverse(); - return `M${top.join(' L')} L${bottom.join(' L')} Z`; + return `M${topPts[0][0]},${topPts[0][1]} ${smoothLine(topPts)} L${bottomPts[0][0]},${bottomPts[0][1]} ${smoothLine(bottomPts)} Z`; }); // Default colors for "Other" and fallback @@ -176,3 +171,28 @@ export function LanguageStreamGraph() { function fmt(d: Date): string { return d.toISOString().slice(0, 10); } + +/** Convert a series of points into smooth cubic bezier curve commands. + * Uses Catmull-Rom to Bezier conversion with tension 0.5. */ +function smoothLine(pts: [number, number][]): string { + if (pts.length < 2) return ''; + if (pts.length === 2) + return `L${pts[1][0]},${pts[1][1]}`; + + const commands: string[] = []; + for (let i = 1; i < pts.length; i++) { + const p0 = pts[Math.max(i - 2, 0)]; + const p1 = pts[i - 1]; + const p2 = pts[i]; + const p3 = pts[Math.min(i + 1, pts.length - 1)]; + + const t = 0.5; + const cp1x = p1[0] + (p2[0] - p0[0]) * t / 3; + const cp1y = p1[1] + (p2[1] - p0[1]) * t / 3; + const cp2x = p2[0] - (p3[0] - p1[0]) * t / 3; + const cp2y = p2[1] - (p3[1] - p1[1]) * t / 3; + + commands.push(`C${cp1x},${cp1y} ${cp2x},${cp2y} ${p2[0]},${p2[1]}`); + } + return commands.join(' '); +}