Topics & Video Statistics

/practice/statistics — a standalone page combining the research-area topic explorer, a searchable list of every season's debate resolutions, and the YouTube channel view/upload charts that used to live only in a modal.

  • Route: /practice/statistics (also /practice/statistics)
  • Package: debate-videos (the areas explorer comes from debate-webview)

What it shows

Three sections, stacked, with Debate Topics by Year at the top:

  • Debate Topics by Year (DebateTopicsExplorer) — a timeline (components/timeline/Timeline.tsx, adapted from the shadcn-space "timeline-02" block): a scrollable rail of every season, newest first, beside an animated pane for the selected year. The pane shows that year's Policy, College (NDT), LD and PF resolutions, each behind a style badge (LD and PF list each month's resolution with its short start month, such as "Sep" or "Nov", under the badge rather than in the resolution text), and — once /api/youtube-stats resolves — that year's video count, total views and average views from its byYear rows. Click a year, use the arrow / Home / End keys on the rail, or the up/down buttons beside it to scroll through every season; the selection survives filtering when the year is still shown. A search box filters by year or by resolution text. The data is the same topics field /api/videos/meta already returns for the video cards' topic tooltips (lib/debate-topics.ts's formatters), passed down from LecturesPage rather than fetched again here. Every resolution is a link: clicking one opens the CARDS search (/research/cards) for that topic's short name, filtered to that season's year and that format (Policy → CX, College → NDT, LD, PF), listing every card, outline and round that matches.
  • Research Areas (TopicAreasExplorer) — every NDT, Policy, LD and PF resolution since 2000, split into 44 focused research areas. Each format is its own collapsible panel with its own selection, so comparing an area's Policy history against its PF history is two clicks rather than a filter toggle that throws the first answer away. Within a panel the areas are ranked by resolution count as a bar chart; selecting one lists its resolutions newest first and draws its year-by-year trend line. The area assignments are heuristic research categories, not official NSDA or NDT classifications — the section says so in its footer. Above the format panels sits a Next season entry (NextSeasonPanel, e.g. "Next season · 2027–28"), shown before any of that season's resolutions are announced. It holds a poll — "Which topic area would you most like to debate next season?" It's ranked-choice: click up to five areas in order of preference (click again to remove one) and press Submit ranking. Each area scores Borda points — 5 for a first choice down to 1 for a fifth — and the list is ordered by points, then first choices. Anyone can see the tally; voting needs a sign-in, and each user has one ballot per season that they can update at any time.
  • YouTube Channel Statistics (YouTubeStatsCharts) — the top-channels table and the views/videos/avg-views-over-time charts YouTubeStatsModal already drew in its dialog, extracted so they can render inline on a full page instead. The four totals charts (views, videos and average views over time, and average views by elim round) are compact and sit four to a row on wide screens, two on tablets and one on phones. Backed by useYouTubeStats() (GET /api/youtube-stats, itself optional furniture — a deployment with no YouTube API key just leaves this section off rather than erroring).

Reachable from the sidebar tree's Practice section ("Topics & Video Statistics", alongside Glossary and Rankings) and from the /videos quick-link tile grid.

Merged from two pages

This page absorbed the standalone Debate Topics Explorer, which lived at /research/topics. That address now permanently redirects here. The two were drawing on the same underlying resolutions — every entry in the areas explorer's resolutions.json is the same resolution text as debate-topics.json, filed under an added area tag — so one page now answers "what has been debated, and in which research domain" and "how much of it is on the channel" without crossing two routes to join the halves. The areas explorer's sidebar row was removed from the Research section, where it would otherwise have been a second entry for the same page.

The 44 area definitions stay in debate-webview (lib/topic-areas/topic-areas.ts) rather than moving to debate-videos, where this page renders: debate-videos depends on debate-webview, not the reverse, so it cannot import the data. The host page mounts the explorer and hands it down as a topicAreasSlot element, the same way dockSlot carries the app dock into the video library.

Data flow

panels/LecturesPage.tsx — currentCategory === "statistics" branch
  → panels/statistics/StatisticsPage.tsx
    → components/topic-explorer/DebateTopicsExplorer.tsx   (topics, youtubeStats.byYear)
      → components/timeline/Timeline.tsx
    → components/topic-explorer/TopicAreasExplorer.tsx
    → components/youtube-stats-modal/YouTubeStatsCharts.tsx (youtubeStats prop)

StatisticsPage takes both as props rather than fetching them itself: topics comes from the same useVideoMeta() call every other /videos branch already makes, and youtubeStats from the same useYouTubeStats() call LecturesVideoGridView's own stats-modal button uses — so landing on this page makes no request the rest of /videos wasn't already making.

Known gaps

No per-style filter on the topics grid — search matches across every style's text at once, there's no "Policy only" toggle. Fixed: DebateTopicsExplorer has an "All / Policy / College / LD / PF" filter row above the search box (labels match the same DEBATE_STYLE_LABELS the per-year badges already use — College here is the NDT resolution). Picking a style hides any year with no resolution for that style, restricts the search box to that style's text (plus the year itself), and shows only that style's badge in each remaining year's pane — "Policy only" now works exactly as named. "All" restores the previous behavior.

The topic links search by the topic's short name (e.g. "Water"), not the full resolution — the cards search is a substring match, so a whole resolution sentence would match almost nothing. Some older short names in debate-topics.json were just the resolution's first two words ("All Young", "The United"), so those searches were broad. Fixed for the 2002–2009 PF topics, which now carry real short names ("National Service", "War on Terror"). Other seasons' names may still need the same pass; the fix is better names in the data, not in the link.

The next-season poll has no admin view or reset; ballots for a season stay in topic_area_rankings after that season starts, and the panel simply moves on to the following season in July (nextPollSeason).

Topic-area poll internals

  • Shared rules: debate-videos lib/topic-areas/topic-area-poll.ts — which season is polled (nextPollSeason: July starts a new season, labelled by its spring year), ballot validation (parseVoteBody: 1–5 distinct known areas), Borda scoring (pointsForRank), option order and the two client calls.
  • API: GET /api/topic-area-poll?season= (points, firstChoices, total ballots, plus myRanking when signed in) and PUT /api/topic-area-poll { season, ranking } (401 signed out; only next season, and one to five distinct known areas).
  • Storage: topic_area_rankings, primary key (season, user_id, rank) — one row per ranked area. A new ballot deletes the user's rows for the season and inserts the new ones in one batch. Declared in schema.ts, created by packages/debate-videos/migrations/0003_topic_area_rankings.sql, which also copies the old single-choice topic_area_votes rows in as first choices (that table is left in place, unread).