diff --git a/package.json b/package.json index cedc8b4..cdb9374 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "1.6.1", + "version": "1.6.2", "private": true, "workspaces": [ "packages/*" diff --git a/src/components/airdrop/CampaignHero.tsx b/src/components/airdrop/CampaignHero.tsx index 9e0a843..dbdb0c6 100644 --- a/src/components/airdrop/CampaignHero.tsx +++ b/src/components/airdrop/CampaignHero.tsx @@ -26,15 +26,43 @@ interface StatusData { /* ─── Chart helpers ─── */ -const Y_FLOOR = 1_000; // $1K minimum for log scale - -/** Map MCap → 0–1 on log Y scale (inverted: 0 = top, 1 = bottom) */ -function mcapToY(mcap: number, yMin: number, yMax: number): number { - if (mcap <= 0) return 1; - const logMin = Math.log10(yMin); - const logMax = Math.log10(yMax); - const clamped = Math.min(Math.max(mcap, yMin), yMax); - return 1 - (Math.log10(clamped) - logMin) / (logMax - logMin); +/** + * Piecewise-linear-banded Y-axis: each of the 4 milestones occupies an equal + * 25% band of chart height. Within a band, position is log-interpolated + * between that band's lower and upper milestone values. + * + * knot 0 = bronze / 100 → 100% (chart bottom) + * knot 1 = bronze → 75% + * knot 2 = silver → 50% + * knot 3 = gold → 25% + * knot 4 = diamond → 0% (chart top) + * + * Returns a fn that maps MCap → 0–1 (0 = top, 1 = bottom). Always returns a + * value inside the chart region, so milestone labels are 25% apart and + * cannot collide regardless of MCap ratios. + */ +function makeMcapToY(milestones: StatusData["milestones"]) { + const knots = [ + milestones.bronze.mcap / 100, + milestones.bronze.mcap, + milestones.silver.mcap, + milestones.gold.mcap, + milestones.diamond.mcap, + ]; + const zoneCount = knots.length - 1; + return (mcap: number): number => { + if (mcap <= knots[0]) return 1; + if (mcap >= knots[knots.length - 1]) return 0; + for (let i = 1; i < knots.length; i++) { + if (mcap <= knots[i]) { + const t = + (Math.log10(mcap) - Math.log10(knots[i - 1])) / + (Math.log10(knots[i]) - Math.log10(knots[i - 1])); + return 1 - (i - 1 + t) / zoneCount; + } + } + return 0; + }; } /** Map date → 0–1 on X (time) axis */ @@ -150,13 +178,13 @@ function MCapChart({ }; }); - // Y-axis domain: floor to diamond milestone (config-driven) - const yMax = milestones.diamond.mcap; - const yMin = Math.min(Y_FLOOR, yMax / 1000); + // Banded Y-axis (each milestone gets equal 25% of chart height) + const mcapToY = makeMcapToY(milestones); + const floorMcap = milestones.bronze.mcap / 100; // X/Y coordinate helpers (within chart area) const toX = (d: Date) => pad.left + dateToX(d, start, end) * chartW; - const toY = (mcap: number) => pad.top + mcapToY(mcap, yMin, yMax) * chartH; + const toY = (mcap: number) => pad.top + mcapToY(mcap) * chartH; // Historical line path + area fill const points = (history ?? []).map((p) => ({ @@ -173,13 +201,13 @@ function MCapChart({ // Projection line: start FDV → diamond milestone at campaign end const startFdv = history && history.length > 0 ? history[0].fdv : currentFdv; const projX1 = toX(start); - const projY1 = toY(startFdv > 0 ? startFdv : yMin); + const projY1 = toY(startFdv > 0 ? startFdv : floorMcap); const projX2 = toX(end); - const projY2 = toY(yMax); + const projY2 = toY(milestones.diamond.mcap); // Current dot position const dotX = toX(now); - const dotY = toY(currentFdv > 0 ? currentFdv : yMin); + const dotY = toY(currentFdv > 0 ? currentFdv : floorMcap); // X-axis time ticks const totalDays = Math.ceil((end.getTime() - start.getTime()) / 86_400_000); @@ -189,8 +217,13 @@ function MCapChart({ xTicks.push(new Date(d)); } - // Y-axis ticks at milestone values + floor - const yTicks = [yMin, ...milestoneEntries.map((m) => m.mcap)]; + // Alternating band stripes (subtle visual band separators) + const bands = [ + { yTop: 0, yBottom: 0.25 }, // gold→diamond + { yTop: 0.25, yBottom: 0.5 }, // silver→gold + { yTop: 0.5, yBottom: 0.75 }, // bronze→silver + { yTop: 0.75, yBottom: 1 }, // floor→bronze + ]; return (