← Back to tutorials

Analytics

Plot 1X2 odds over time for a single match

Visualise the market’s movement by charting match_winner odds for Real Madrid vs Juventus with Chart.js.

Published on October 29, 2025 · 7 min read
Line chart sketch representing home, draw, and away odds.

Monitor how the market values Real Madrid vs Juventus (match id 9527) by charting the odds timeline pulled from GameForecastAPI.

Step 1 — Load Chart.js via CDN

Add the markup and include both Chart.js and the date-fns adapter. The chart lives inside a fixed-height shell so it stays responsive without overflowing your dashboard.

index.html
<!DOCTYPE html><html lang="en">  <head>    <meta charset="utf-8" />    <title>GameForecastAPI odds timeline</title>    <link rel="stylesheet" href="styles.css" />    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>    <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>  </head>  <body class="dashboard">    <main class="card">      <h1 class="card__title">Match odds timeline</h1>      <div class="chart-shell">        <canvas id="odds-chart"></canvas>      </div>      <p id="status" class="status"></p>    </main>    <script type="module" src="odds-timeline.js"></script>  </body></html>
styles.css
.chart-shell {  width: min(960px, 100%);  height: 420px;  margin-block: 24px;}.chart-shell canvas {  width: 100% !important;  height: 100% !important;  display: block;}

Feel free to tweak the height in .chart-shell or add media queries if you’d like a taller canvas on desktop than on mobile.

Step 2 — Fetch history and render datasets

The JavaScript module below requests the match, normalises the odds history, and renders home/draw/away lines. It also surfaces errors via the status element.

odds-timeline.js
const RAPID_API_HOST = "game-forecast-api.p.rapidapi.com";const MATCH_ID = 9527;const RAPID_API_KEY = "YOUR_RAPIDAPI_KEY";const headers = {  "X-RapidAPI-Key": RAPID_API_KEY,  "X-RapidAPI-Host": RAPID_API_HOST,};const canvas = document.getElementById("odds-chart");const status = document.getElementById("status");if (!(canvas instanceof HTMLCanvasElement) || !status) {  throw new Error("Required DOM nodes are missing. Check your HTML markup.");}const context = canvas.getContext("2d");if (!context) {  throw new Error("Unable to initialise the canvas context.");}const ChartJS = window.Chart;if (!ChartJS) {  throw new Error("Chart.js failed to load. Check the CDN script tag.");}let chartInstance = null;function setStatus(message) {  status.textContent = message;}function transformSnapshots(payload) {  const data = payload.data || [];  const event = data[0];  if (!event) {    throw new Error("No event returned for MATCH_ID " + MATCH_ID);  }  const odds = Array.isArray(event.odds) ? event.odds : [];  const matchWinnerSnapshots = odds.filter((entry) => entry.key === "match_winner");  if (matchWinnerSnapshots.length === 0) {    throw new Error("The event does not expose a match_winner market.");  }  const accumulator = new Map();  const pushSnapshot = (runAt, values) => {    if (!runAt || !values) {      return;    }    const normalisedKey = new Date(runAt).toISOString();    accumulator.set(normalisedKey, {      run_at: runAt,      values: {        Home: values.Home ?? values.home,        Draw: values.Draw ?? values.draw,        Away: values.Away ?? values.away,      },    });  };  matchWinnerSnapshots.forEach((entry) => {    pushSnapshot(entry.run_at, entry.values);    if (Array.isArray(entry.history)) {      entry.history.forEach((snapshot) => {        pushSnapshot(snapshot.run_at, snapshot.values);      });    }  });  return Array.from(accumulator.values()).sort(    (a, b) => new Date(a.run_at).getTime() - new Date(b.run_at).getTime()  );}function renderChart(snapshots) {  const labels = snapshots.map((item) => new Date(item.run_at));  const homeData = snapshots.map((item) => item.values.Home);  const drawData = snapshots.map((item) => item.values.Draw);  const awayData = snapshots.map((item) => item.values.Away);  if (chartInstance) {    chartInstance.destroy();  }  chartInstance = new ChartJS(context, {    type: "line",    data: {      labels,      datasets: [        {          label: "Home",          data: homeData,          borderColor: "#7c5fff",          backgroundColor: "rgba(124,95,255,0.15)",          tension: 0.2,        },        {          label: "Draw",          data: drawData,          borderColor: "#94a3b8",          backgroundColor: "rgba(148,163,184,0.15)",          tension: 0.2,        },        {          label: "Away",          data: awayData,          borderColor: "#f97316",          backgroundColor: "rgba(249,115,22,0.15)",          tension: 0.2,        },      ],    },    options: {      responsive: true,      maintainAspectRatio: false,      animation: false,      scales: {        x: {          type: "time",          time: { unit: "day" },          ticks: { color: "#cbd5f5" },          grid: { color: "rgba(148,163,184,0.2)" },        },        y: {          ticks: { color: "#cbd5f5" },          grid: { color: "rgba(148,163,184,0.15)" },        },      },      plugins: {        legend: { position: "top", labels: { color: "#e2e8f0" } },        tooltip: {          callbacks: {            label: (context) => {              const value = context.parsed.y;              return context.dataset.label + ": " + value;            },          },        },      },    },  });}async function fetchOdds() {  setStatus("Loading odds history…");  try {    const url = new URL("https://" + RAPID_API_HOST + "/events");    url.searchParams.set("id", String(MATCH_ID));    url.searchParams.set("include_all_history", "true");    const response = await fetch(url, { headers });    if (!response.ok) {      throw new Error("Failed to fetch odds history");    }    const payload = await response.json();    const snapshots = transformSnapshots(payload);    if (snapshots.length === 0) {      setStatus("No odds history available for this match.");      return;    }    renderChart(snapshots);    const lastSnapshot = snapshots[snapshots.length - 1];    setStatus("Last update: " + new Date(lastSnapshot.run_at).toLocaleString());  } catch (error) {    console.error(error);    setStatus("Unable to fetch odds history.");  }}fetchOdds();

Setting include_all_history=true unlocks archived snapshots, and the script also walks through every match_winner entry returned in event.odds. That combination guarantees you plot daily lines even when partners publish the timeline as separate rows instead of a nested history array, while de-duping by timestamp to keep the chart clean.

Pair the chart with your own risk thresholds: highlight when the away line closes in on the home favourite, or trigger Discord webhooks when the implied probability flips. Because the payload contains run_at, you can also overlay bookmaker news or community signals.

Step 3 — Extend the visual

  • Add setInterval(fetchOdds, 300000) to refresh every five minutes.
  • Layer annotations such as lineup news or injuries by drawing on the same canvas context.
  • Convert odds to implied probability with 1 / odd to compare with our prediction percentages.

Chart every market movement

Subscribe on RapidAPI to unlock higher rate limits and access to other markets like both teams to score, goal lines, and recommended bets. The Ultra tier adds daily odds history exports so you can backfill months of data for quant analysis.
Get your RapidAPI key