DataViz.manishdatt.com

Agricultural Production Statistics in New Zealand

Racing bar for annual yield of cereals and legumes.

By Manish Datt

TidyTuesday dataset of 2026-02-17

Racing Bars by Year

Plotting code



<head>
  <script src="https://cdn.tailwindcss.com"></script>

  <link href="https://unpkg.com/tabulator-tables@6.3.0/dist/css/tabulator.min.css" rel="stylesheet" />
  <script src="https://unpkg.com/tabulator-tables@6.3.0/dist/js/tabulator.min.js"></script>

  <script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>

  <style>
    .tabulator {
      border: 1px solid #e5e7eb;
      border-radius: 0.75rem;
      overflow: hidden;
      font-size: 0.9rem;
    }
    .tabulator .tabulator-header {
      border-bottom: 1px solid #e5e7eb;
      background: #f8fafc;
    }
    .tabulator .tabulator-header .tabulator-col {
      background: #f8fafc;
    }
  </style>
</head>
<body class="bg-slate-50 text-slate-900">
  <main class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">

    <section class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm sm:p-6">
      <div id="table"></div>
    </section>

    <section class="mt-6 rounded-xl border border-slate-200 bg-white p-4 shadow-sm sm:p-6">
      <div class="mb-1.5 flex items-center justify-between gap-3">
        <h2 class="text-lg font-semibold">Racing Bars by Year</h2>
      </div>
      <div id="yield-race" class="h-[560px] w-full"></div>
    </section>
  </main>

  <script>
    function renderTable(rows) {
      const firstRow = rows[0] || {};
      const columns = Object.keys(firstRow).map((key) => ({
        title: key,
        field: key,
        headerFilter: "input",
        hozAlign: "left",
      }));

      new Tabulator("#table", {
        data: rows,
        layout: "fitDataStretch",
        pagination: true,
        paginationSize: 10,
        movableColumns: true,
        resizableRows: false,
        columns,
      });
    }

    function renderYieldRace(rows) {
      const yieldRows = rows
        .filter((row) => row.measure && row.measure.toLowerCase().includes("yield"))
        .map((row) => ({
          year: Number(row.year_ended_june),
          measure: row.measure,
          value: Number(row.value),
        }))
        .filter((row) => Number.isFinite(row.year) && Number.isFinite(row.value));

      if (!yieldRows.length) {
        document.getElementById("yield-race").innerHTML = '<div class="rounded-lg border border-yellow-200 bg-yellow-50 p-4 text-sm text-yellow-800">No yield rows found.</div>';
        return;
      }

      const rawMaxYieldValue = Math.max(...yieldRows.map((row) => row.value), 0);
      const maxYieldValue = Math.ceil(rawMaxYieldValue / 50000) * 50000;
      const years = [...new Set(yieldRows.map((row) => row.year))].sort((a, b) => a - b);
      const measures = [...new Set(yieldRows.map((row) => row.measure))];

      const valueByYearMeasure = new Map();
      for (const row of yieldRows) {
        valueByYearMeasure.set(`${row.year}||${row.measure}`, row.value);
      }

      const chart = echarts.init(document.getElementById("yield-race"));
      let yearIndex = 0;
      const frameMs = 1200;
      const barColors = ["#0f766e", "#0369a1", "#b45309", "#be123c", "#4f46e5"];

      function buildSeriesData(year) {
        return measures
          .map((measure) => ({
            name: measure,
            value: valueByYearMeasure.get(`${year}||${measure}`) ?? 0,
          }))
          .sort((a, b) => a.value - b.value);
      }

      function updateChart(targetYearIndex = yearIndex, shouldAdvance = true) {
        const year = years[targetYearIndex];
        const seriesData = buildSeriesData(year);

        chart.setOption({
          title: {
            text: "Timeline of annual yield (Tonnes) for cereals and legumes in New Zealand",
            left: "center",
            top: 4,
            textStyle: { fontSize: 18, fontWeight: 600 },
          },
          graphic: [
            {
              type: "text",
              right: 125,
              bottom: 100,
              z: 100,
              style: {
                text: String(year),
                font: "700 44px sans-serif",
                fill: "rgba(15, 23, 42, 0.22)",
              },
            },
          ],
          xAxis: {
            max: maxYieldValue,
            axisLabel: {
              formatter: (val) => {
                const num = Number(val);
                if (Math.abs(num) >= 1000) return `${(num / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 })}K`;
                return num.toLocaleString();
              },
            },
          },
          yAxis: {
            type: "category",
            inverse: true,
            max: 4,
            axisLine: { show: false },
            axisTick: { show: false },
            axisLabel: {
              formatter: (label) => String(label).replace(/\s*\(yield\)\s*$/i, ""),
            },
            animationDuration: 250,
            animationDurationUpdate: 250,
            data: seriesData.map((item) => item.name),
          },
          series: [
            {
              realtimeSort: true,
              type: "bar",
              data: seriesData,
              label: {
                show: true,
                position: "right",
                valueAnimation: true,
                formatter: (param) => Number(param.value).toLocaleString(),
              },
              itemStyle: {
                borderRadius: [0, 4, 4, 0],
                color: (param) => barColors[param.dataIndex % barColors.length],
              },
            },
          ],
        });

        if (shouldAdvance) {
          yearIndex = (targetYearIndex + 1) % years.length;
        }
      }

      chart.setOption({
        grid: {
          top: 30,
          bottom: 50,
          left: 100,
          right: 100,
        },
        tooltip: {
          trigger: "item",
          valueFormatter: (val) => Number(val).toLocaleString(),
        },
        xAxis: {
          type: "value",
        },
        yAxis: {
          type: "category",
          inverse: true,
          axisLine: { show: false },
          axisTick: { show: false },
          axisLabel: {
            formatter: (label) => String(label).replace(/\s*\(yield\)\s*$/i, ""),
          },
        },
        series: [
          {
            type: "bar",
            realtimeSort: true,
          },
        ],
        animationDuration: 0,
        animationDurationUpdate: frameMs,
        animationEasing: "linear",
        animationEasingUpdate: "linear",
      });

      updateChart();
      setInterval(updateChart, frameMs);
      window.addEventListener("resize", () => chart.resize());
    }

    async function loadData() {
      const response = await fetch("dataset.csv");
      if (!response.ok) throw new Error("Failed to load dataset.csv");

      const csvText = await response.text();
      const parsed = Papa.parse(csvText, {
        header: true,
        dynamicTyping: true,
        skipEmptyLines: true,
      });

      const rows = parsed.data;
      renderTable(rows);
      renderYieldRace(rows);
    }

    loadData().catch((error) => {
      const tableEl = document.getElementById("table");
      const chartEl = document.getElementById("yield-race");

      const msg = '<div class="rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700">Could not load dataset.csv. Make sure you are serving this folder via a local server.</div>';
      tableEl.innerHTML = msg;
      chartEl.innerHTML = msg;
      console.error(error);
    });

  </script>
</body>