diff --git a/package.json b/package.json index 4abc81c..9289a34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "1.7.1", + "version": "1.8.0", "private": true, "workspaces": [ "packages/*" diff --git a/src/components/airdrop/CampaignHero.tsx b/src/components/airdrop/CampaignHero.tsx index e9bff43..565a43d 100644 --- a/src/components/airdrop/CampaignHero.tsx +++ b/src/components/airdrop/CampaignHero.tsx @@ -128,7 +128,7 @@ function useCountdown(endDateStr: string) { return remaining; } -/* ─── MCap Milestone Progression Chart ─── */ +/* ─── MCap Milestone Area Chart ─── */ function MCapChart({ currentFdv, @@ -139,14 +139,13 @@ function MCapChart({ }) { // Banded X-axis (each milestone gets equal 25% of chart width) const mcapToX = makeMcapToX(milestones); - const floorMcap = milestones.bronze.mcap / 100; - // SVG geometry — short horizontal band, room for top labels on desktop + // SVG geometry — area chart with room for top labels and Y-axis ticks const svgW = 600; - const svgH = 110; - const pad = { top: 50, right: 50, bottom: 14, left: 24 }; + const svgH = 280; + const pad = { top: 78, right: 16, bottom: 30, left: 48 }; const chartW = svgW - pad.left - pad.right; - const barY = pad.top + (svgH - pad.top - pad.bottom) / 2; + const chartH = svgH - pad.top - pad.bottom; // Milestone entries — derive label/CMC rank from config so test/prod both work const milestoneEntries = Object.entries(milestones).map(([key, val], idx) => { @@ -161,10 +160,39 @@ function MCapChart({ }; }); - const dotXFrac = mcapToX(currentFdv > 0 ? currentFdv : floorMcap); - const dotX = pad.left + dotXFrac * chartW; - const startX = pad.left; - const endX = pad.left + chartW; + // Y-axis: linear, $0 → diamond. 5 evenly-spaced ticks at 0/25/50/75/100% of diamond. + const yMax = milestones.diamond.mcap; + const yTicks = [0, 0.25, 0.5, 0.75, 1.0].map((f) => f * yMax); + const toX = (xFrac: number) => pad.left + xFrac * chartW; + const toY = (mcap: number) => pad.top + (1 - Math.min(1, Math.max(0, mcap / yMax))) * chartH; + + // Smooth curve: sample 20 points per band using the same banded log + // interpolation as mcapToX, so the curve passes exactly through the + // milestone knots and the heartbeat dot lies exactly on the curve. + const knots = [ + milestones.bronze.mcap / 100, + milestones.bronze.mcap, + milestones.silver.mcap, + milestones.gold.mcap, + milestones.diamond.mcap, + ]; + const curve: { x: number; y: number }[] = [{ x: toX(0), y: toY(0) }]; + for (let i = 1; i < knots.length; i++) { + const lo = Math.log10(knots[i - 1]); + const hi = Math.log10(knots[i]); + const samples = 24; + for (let s = 1; s <= samples; s++) { + const m = Math.pow(10, lo + (hi - lo) * (s / samples)); + curve.push({ x: toX(mcapToX(m)), y: toY(m) }); + } + } + const linePath = curve.map((p, i) => `${i === 0 ? "M" : "L"}${p.x},${p.y}`).join(" "); + const areaPath = `${linePath} L${curve[curve.length - 1].x},${pad.top + chartH} L${curve[0].x},${pad.top + chartH} Z`; + + // Current MCap dot — clamp at floor so dot is always visible if currentFdv = 0 + const dotMcap = currentFdv > 0 ? currentFdv : 0; + const dotX = toX(mcapToX(dotMcap)); + const dotY = toY(dotMcap); return (