Skip to content

Airdrop chart: fix overlapping milestone labels via banded Y-axis #1051

@realproject7

Description

@realproject7

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

  • No two milestone labels overlap visually at any viewport ≥ 320px wide.
  • At currentFdv = $31K test mode (current production state), the heartbeat dot sits in the silver→gold band (between 50% and 75% of chart height).
  • At currentFdv = $31K prod mode (mock), the heartbeat dot sits in the pre-bronze band (between 75% and 100% of chart height — i.e. lower portion).
  • Y-axis tick labels are removed; only the 4 milestone right-edge labels remain.
  • No `$50`-style floor label artifact.
  • Historical line, area fill, projection line, and current dot all use the new piecewise mapping consistently.
  • Mobile (<640px): right-edge labels hidden, mobile legend below chart shows all 4 milestones (existing behavior preserved).
  • No hex literals in chart code (CSS vars only).
  • PR includes a screenshot at production live state showing the dot in band 2 with non-overlapping labels.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/T3Assigned to T3 builder agent

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions