Why this ticket exists
PR #1049 shipped the time-series chart from #1048 — correct concept, but the Y-axis math is broken in production.
Visible problems on https://plotlink.xyz/airdrop right now:
- Right-edge milestone labels overlap: `$50K — unlocks 100%` collides with `$35K — unlocks 50%`; `$10K — unlocks 30%` collides with `$7K — unlocks 10%`.
- Y-axis tick labels at the left edge stack on top of each other for the same reason.
- The chart bottom shows `$50` (the floor
Math.min(Y_FLOOR=\$1K, diamond/1000)), which is meaningless context.
- Most of the chart is empty space because milestones occupy a narrow strip near the top.
Root cause
Current code uses pure log Y-axis with domain [\$1K, diamond.mcap] (src/components/airdrop/CampaignHero.tsx:29-38, 154-155).
In test mode (diamond=$50K), milestones at $7K/$10K/$35K/$50K map to log positions:
- $7K → 50% from top
- $10K → 41% from top
- $35K → 9% from top
- $50K → 0% from top
So $35K and $50K labels sit ~9% apart vertically — they overlap. Same in prod ($50M and $100M would be ~7% apart).
This isn't fixable by tweaking the floor or domain. Pure log Y will always crowd milestones whose ratio is small relative to the total log range. ultrasound.money avoids this because their data range is ~2.3× (60M→140M ETH) on a linear Y; ours is 100× ($1M→$100M) which doesn't fit either pure linear or pure log cleanly.
The fix: 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 bottom and top milestone values.
y-position from bottom:
0% (chart bottom) = floor (= bronze / 100)
0%–25% zone 0 (pre-bronze) floor → bronze, log-interp within
25% (band line) = bronze
25%–50% zone 1 (bronze→silver) bronze → silver, log-interp
50% (band line) = silver
50%–75% zone 2 (silver→gold) silver → gold, log-interp
75% (band line) = gold
75%–100% zone 3 (gold→diamond) gold → diamond, log-interp
100% (chart top) = diamond
This guarantees:
- Milestone right-edge labels are 25% of chart height apart — never overlap, regardless of MCap ratios.
- Current FDV always lands in the visually meaningful band: e.g. $31K test mode → ~73% (between silver and gold), $31K prod mode → ~6% (well below bronze, in the pre-bronze zone).
- Same chart geometry works for test mode and prod mode — only milestone values change.
Concrete spec
mcapToY rewrite
Replace the log function in src/components/airdrop/CampaignHero.tsx:29-38 with a piecewise function that takes the milestones config and returns a Y position in [0,1] (0 = top, 1 = bottom).
function makeMcapToY(milestones: StatusData["milestones"]) {
const knots = [
milestones.bronze.mcap / 100, // floor: 2 decades below bronze
milestones.bronze.mcap,
milestones.silver.mcap,
milestones.gold.mcap,
milestones.diamond.mcap,
];
const zoneCount = knots.length - 1; // 4 zones
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]));
const zoneIdx = i - 1;
return 1 - (zoneIdx + t) / zoneCount;
}
}
return 0;
};
}
Drop Y-axis tick labels entirely
The right-edge milestone labels already say what each band's top value is. Stacking another column of small labels at the left edge adds noise and re-introduces the overlap problem. Remove the yTicks rendering loop (src/components/airdrop/CampaignHero.tsx:319-332).
Right-edge milestone labels
Anchor each milestone label at its band's top Y position:
- Use
text-anchor="end", x = pad.left + chartW - 4
y = toY(milestone.mcap) - 4 (above the line)
- One label per band → 25% gaps → cannot collide
Drop the className="hidden sm:block" (Tailwind block on SVG <text> is non-portable). Wrap desktop-only elements in <g> with the responsive class instead, or render outside SVG via <foreignObject> if needed.
Optional band striping
Add subtle alternating rect backgrounds for the 4 bands (1% opacity accent, every other band) to make the milestone structure visually obvious without extra labels.
Acceptance criteria
Files
- `src/components/airdrop/CampaignHero.tsx` — replace
mcapToY + axis rendering inside MCapChart.
Reviewer note
Block the PR if it:
- Reintroduces a single global
Math.log10-based Y mapping over the whole [floor, diamond] range
- Keeps Y-axis tick labels alongside right-edge milestone labels
- Hardcodes any color hex
Why this ticket exists
PR #1049 shipped the time-series chart from #1048 — correct concept, but the Y-axis math is broken in production.
Visible problems on https://plotlink.xyz/airdrop right now:
Math.min(Y_FLOOR=\$1K, diamond/1000)), which is meaningless context.Root cause
Current code uses pure log Y-axis with domain
[\$1K, diamond.mcap](src/components/airdrop/CampaignHero.tsx:29-38, 154-155).In test mode (diamond=$50K), milestones at $7K/$10K/$35K/$50K map to log positions:
So $35K and $50K labels sit ~9% apart vertically — they overlap. Same in prod ($50M and $100M would be ~7% apart).
This isn't fixable by tweaking the floor or domain. Pure log Y will always crowd milestones whose ratio is small relative to the total log range. ultrasound.money avoids this because their data range is ~2.3× (60M→140M ETH) on a linear Y; ours is 100× ($1M→$100M) which doesn't fit either pure linear or pure log cleanly.
The fix: 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 bottom and top milestone values.
This guarantees:
Concrete spec
mcapToYrewriteReplace the log function in
src/components/airdrop/CampaignHero.tsx:29-38with a piecewise function that takes the milestones config and returns a Y position in[0,1](0 = top, 1 = bottom).Drop Y-axis tick labels entirely
The right-edge milestone labels already say what each band's top value is. Stacking another column of small labels at the left edge adds noise and re-introduces the overlap problem. Remove the
yTicksrendering loop (src/components/airdrop/CampaignHero.tsx:319-332).Right-edge milestone labels
Anchor each milestone label at its band's top Y position:
text-anchor="end",x = pad.left + chartW - 4y = toY(milestone.mcap) - 4(above the line)Drop the
className="hidden sm:block"(Tailwindblockon SVG<text>is non-portable). Wrap desktop-only elements in<g>with the responsive class instead, or render outside SVG via<foreignObject>if needed.Optional band striping
Add subtle alternating rect backgrounds for the 4 bands (1% opacity accent, every other band) to make the milestone structure visually obvious without extra labels.
Acceptance criteria
Files
mcapToY+ axis rendering insideMCapChart.Reviewer note
Block the PR if it:
Math.log10-based Y mapping over the whole [floor, diamond] range