Skip to content

Olympic medals ​

The classic data set of olympic medal winners (8.618 rows) — loaded from a json file.

How it works ​

  • Loading data: rows is undefined until the file is loaded, the grid shows its loading indicator meanwhile. The dates (dd/mm/yyyy) are converted to ISO dates, so the date filter works.
  • Grouping: v-model:columnGroups switches between grouping by country, sport and country or no grouping. Users can also drag columns into the group area.
  • Aggregation: the medal columns use the custom column type medal with aggFunc: "sum", the athlete column counts rows, the age column shows the average. The total row sums up everything that matches the filters.
  • Custom column type: medal extends the built-in number type (right alignment, number filter) and adds the aggregation and width.
  • Set filters: country, sport and year show a list of all values.
  • Initial sort: v-model:sortedColumns sorts by gold medals. Groups are sorted by their aggregated gold medals.
  • Styling: cellClass colors medal counts, zero values are dimmed.
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <input v-model="search" type="search" placeholder="Search athlete, country, sport…" />
      <button @click="groups = ['country']">By country</button>
      <button @click="groups = ['sport', 'country']">Sport › Country</button>
      <button @click="groups = []">Flat</button>
      <span>{{ rows?.length.toLocaleString() ?? "…" }} results</span>
    </div>
    <Datagrid
      :columns="columns"
      :rows="rows"
      :options="options"
      v-model:globalFilter="search"
      v-model:columnGroups="groups"
      v-model:sortedColumns="sort"
    />
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref, shallowRef } from "vue";
import { withBase } from "vitepress";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";

type Winner = {
  athlete: string;
  age: number;
  country: string;
  year: number;
  date: string;
  sport: string;
  gold: number;
  silver: number;
  bronze: number;
  total: number;
};

const rows = shallowRef<Array<Winner>>();
const search = ref("");
const groups = ref(["country"]);
const sort = ref([{ column: "gold", asc: false }]);

onMounted(async () => {
  const response = await fetch(withBase("/data/olympic-winners.json"));
  const data: Array<Winner> = await response.json();
  // dates are delivered as dd/mm/yyyy, convert them to ISO dates
  rows.value = data.map((row) => ({
    ...row,
    date: row.date.split("/").reverse().join("-"),
  }));
});

// medal columns share their configuration with a custom column type
const medal = (field: "gold" | "silver" | "bronze", icon: string): ColumnConfig<Winner> => ({
  field,
  text: icon,
  type: "medal",
  cellClass: ({ value }) => (value > 0 ? `medal-${field}` : "medal-none"),
});

const columns: Array<ColumnConfig<Winner>> = [
  { field: "athlete", text: "Athlete", width: 200, pinned: "left", aggFunc: "count" },
  { field: "country", text: "Country", width: 160, groupable: true, filterType: "set" },
  { field: "sport", text: "Sport", width: 150, groupable: true, filterType: "set" },
  { field: "year", text: "Year", type: "number", width: 90, groupable: true, filterType: "set" },
  { field: "date", text: "Date", type: "date", width: 130 },
  { field: "age", text: "Age", type: "number", width: 80, aggFunc: "avg", valueFormatter: ({ value }) => (value == null ? "" : String(Math.round(value))) },
  {
    id: "medals",
    text: "Medals",
    children: [medal("gold", "🥇"), medal("silver", "🥈"), medal("bronze", "🥉"), { field: "total", text: "Total", type: "medal" }],
  },
];

const options: GridOptions<Winner> = {
  columnMenu: true,
  totalRow: "bottom",
  groupDefaultExpanded: 0,
  columnTypes: {
    medal: { type: "number", align: "right", headerAlign: "right", filterType: "number", aggFunc: "sum", width: 80 },
  },
  defaultColumn: { sortable: true, filterable: true, resizeable: true, moveable: true },
};
</script>

<style>
.medal-gold {
  color: #b08600;
  font-weight: 700;
}
.medal-silver {
  color: #6e7781;
  font-weight: 700;
}
.medal-bronze {
  color: #a0522d;
  font-weight: 700;
}
.medal-none {
  color: var(--ut-border-color);
}
</style>
tsx
import { useEffect, useState } from "react";
import { withBase } from "vitepress";
import { Datagrid, type ColumnConfig, type GridOptions, type SortModel } from "@datagrid/react-ui";

type Winner = {
  athlete: string;
  age: number;
  country: string;
  year: number;
  date: string;
  sport: string;
  gold: number;
  silver: number;
  bronze: number;
  total: number;
};

// medal columns share their configuration with a custom column type
const medal = (field: "gold" | "silver" | "bronze", icon: string): ColumnConfig<Winner> => ({
  field,
  text: icon,
  type: "medal",
  cellClass: ({ value }) => (value > 0 ? `medal-${field}` : "medal-none"),
});

const columns: Array<ColumnConfig<Winner>> = [
  { field: "athlete", text: "Athlete", width: 200, pinned: "left", aggFunc: "count" },
  { field: "country", text: "Country", width: 160, groupable: true, filterType: "set" },
  { field: "sport", text: "Sport", width: 150, groupable: true, filterType: "set" },
  { field: "year", text: "Year", type: "number", width: 90, groupable: true, filterType: "set" },
  { field: "date", text: "Date", type: "date", width: 130 },
  { field: "age", text: "Age", type: "number", width: 80, aggFunc: "avg", valueFormatter: ({ value }) => (value == null ? "" : String(Math.round(value))) },
  {
    id: "medals",
    text: "Medals",
    children: [medal("gold", "🥇"), medal("silver", "🥈"), medal("bronze", "🥉"), { field: "total", text: "Total", type: "medal" }],
  },
];

const options: GridOptions<Winner> = {
  columnMenu: true,
  totalRow: "bottom",
  groupDefaultExpanded: 0,
  columnTypes: {
    medal: { type: "number", align: "right", headerAlign: "right", filterType: "number", aggFunc: "sum", width: 80 },
  },
  defaultColumn: { sortable: true, filterable: true, resizeable: true, moveable: true },
};

export default function ShowcaseOlympics() {
  // undefined shows the loading indicator
  const [rows, setRows] = useState<Array<Winner>>();
  const [search, setSearch] = useState<unknown>("");
  const [groups, setGroups] = useState(["country"]);
  const [sort, setSort] = useState<SortModel>([{ column: "gold", asc: false }]);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      const response = await fetch(withBase("/data/olympic-winners.json"));
      const data: Array<Winner> = await response.json();
      // dates are delivered as dd/mm/yyyy, convert them to ISO dates
      if (!cancelled) setRows(data.map((row) => ({ ...row, date: row.date.split("/").reverse().join("-") })));
    })();
    return () => {
      cancelled = true;
    };
  }, []);

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <input
          type="search"
          placeholder="Search athlete, country, sport…"
          value={String(search ?? "")}
          onChange={(e) => setSearch(e.target.value)}
        />
        <button onClick={() => setGroups(["country"])}>By country</button>
        <button onClick={() => setGroups(["sport", "country"])}>Sport › Country</button>
        <button onClick={() => setGroups([])}>Flat</button>
        <span>{rows?.length.toLocaleString() ?? "…"} results</span>
      </div>
      <Datagrid
        columns={columns}
        rows={rows}
        options={options}
        globalFilter={search}
        onGlobalFilterChange={setSearch}
        columnGroups={groups}
        onColumnGroupsChange={setGroups}
        sortedColumns={sort}
        onSortedColumnsChange={setSort}
      />
      <style>{`
        .medal-gold {
          color: #b08600;
          font-weight: 700;
        }
        .medal-silver {
          color: #6e7781;
          font-weight: 700;
        }
        .medal-bronze {
          color: #a0522d;
          font-weight: 700;
        }
        .medal-none {
          color: var(--ut-border-color);
        }
      `}</style>
    </div>
  );
}

Released under the ISC License.