// Keyword ranks over the `aso rank` snapshots, drawn with visx (scales, axes, shapes, tooltip, responsive width). // Rank 1 is the top of the chart (the y axis runs from the worst rank shown down to 1); a keyword the app was not in // the first results for has no point that day. Reuses the line chart's styles (.lchart) and crosshair tooltip. import { AxisBottom, AxisLeft } from '@visx/axis'; import { curveMonotoneX } from '@visx/curve'; import { Group } from '@visx/group'; import { useParentSize } from '@visx/responsive'; import { scaleLinear } from '@visx/scale'; import { Bar, Circle, Line, LinePath } from '@visx/shape'; import { date } from '../lib/format'; import { ChartTip, useIndexHover } from './charts'; import { dateTicks } from './LineChart'; export interface RankLine { label: string; ranks: (number | null)[]; cls: 'l1' | 'l2' | 'l3' } const M = { top: 12, right: 18, bottom: 26, left: 40 }; /** The y domain: rank 1 at the top, a round worst rank at the bottom (at least 10) and up to 5 ticks. */ export function rankAxis(lines: (number | null)[][]): { worst: number; ticks: number[] } { const hi = Math.max(10, ...lines.flat().filter((v): v is number => v !== null)); const step = hi <= 10 ? 3 : hi <= 50 ? 10 : hi <= 100 ? 25 : 50; const worst = hi <= 10 ? 10 : Math.ceil(hi / step) * step; const ticks = [1, ...Array.from({ length: Math.floor(worst / step) }, (_, i) => (i + 1) * step)].filter((v, i, a) => a.indexOf(v) === i && v <= worst); return { worst, ticks }; } export function RankChart({ dates, lines, label, noRank }: { dates: string[]; lines: RankLine[]; label: string; noRank: string }) { const { parentRef, width } = useParentSize({ debounceTime: 50 }); const n = dates.length; const h = useIndexHover(n, width, M.left, M.right); if (n < 2 || !lines.length) return null; const height = Math.round(Math.max(170, Math.min(230, width * 0.28))); const { worst, ticks } = rankAxis(lines.map(l => l.ranks)); const x = scaleLinear({ domain: [0, n - 1], range: [M.left, Math.max(M.left + 1, width - M.right)] }); const y = scaleLinear({ domain: [1, worst], range: [M.top, height - M.bottom] }); const pts = (vs: (number | null)[]) => vs.map((v, i) => [i, v] as const); const defined = (d: readonly [number, number | null]) => d[1] !== null; const xs = dateTicks(n, Math.max(2, Math.floor(width / 110))); const hv = h.hover; return (
{width > 0 && ( {ticks.map(v => )} `#${Number(v)}`} hideAxisLine hideTicks tickClassName="ax" tickLabelProps={{ className: 'ax', textAnchor: 'end', dx: -4, dy: 4 }} /> date(dates[Number(v)])} hideAxisLine hideTicks tickLabelProps={(_, i) => ({ className: 'ax', textAnchor: i === 0 ? 'start' : i === xs.length - 1 ? 'end' : 'middle', dy: 4 })} /> {lines.map(l => x(d[0])} y={d => y(d[1] ?? worst)} curve={curveMonotoneX} />)} {lines.map(l => l.ranks.map((v, i) => (v === null ? null : )))} {hv !== null && ( )} )} {hv !== null && width > 0 && ( {date(dates[hv])} {lines.map(l => {l.label}{l.ranks[hv] == null ? noRank : `#${l.ranks[hv]}`})} )}
{lines.map(l => {l.label})}
); }