Heatmap Table

Basic Usage

Click column headers to sort
Formula Eabove hull Egap Eform Created Date Calculated At Last Updated Synthesis Time
Fe₂O₃ 0.00 2.2 −8.5 2026-06-21 2026-06-22 09:30 2026-06-23 14:15 08:00
TiO₂ 0.00 3.2 −9.8 2026-06-20 2026-06-22 10:05 2026-06-23 16:45 08:45
ZnO 0.02 3.4 −3.6 2026-06-19 2026-06-22 11:20 2026-06-24 08:30 09:15
Cu₂O 0.05 2.1 −1.7 2026-06-18 2026-06-22 13:10 2026-06-24 12:00 10:30
SiO₂ 0.00 8.9 −9.1 2026-06-17 2026-06-22 14:25 2026-06-24 15:40 11:20
Al₂O₃ 0.00 8.8 −17.4 2026-06-16 2026-06-22 15:50 2026-06-25 09:10 13:05
MgO 0.00 7.8 −6.2 2026-06-15 2026-06-22 17:35 2026-06-25 11:25 14:35
CaTiO₃ 0.03 3.5 −16.1 2026-06-14 2026-06-22 18:45 2026-06-25 18:05 16:10
svelte<script lang="ts">
  import { HeatmapTable } from 'matterviz'

  const data = [
    [
      `Fe₂O₃`,
      0.0,
      2.2,
      -8.5,
      `2026-06-21`,
      `2026-06-22T09:30:00Z`,
      Date.parse(`2026-06-23T14:15:00Z`),
      new Date(`2026-06-23T08:00:00Z`),
    ],
    [
      `TiO₂`,
      0.0,
      3.2,
      -9.8,
      `2026-06-20`,
      `2026-06-22T10:05:00Z`,
      Date.parse(`2026-06-23T16:45:00Z`),
      new Date(`2026-06-23T08:45:00Z`),
    ],
    [
      `ZnO`,
      0.02,
      3.4,
      -3.6,
      `2026-06-19`,
      `2026-06-22T11:20:00Z`,
      Date.parse(`2026-06-24T08:30:00Z`),
      new Date(`2026-06-24T09:15:00Z`),
    ],
    [
      `Cu₂O`,
      0.05,
      2.1,
      -1.7,
      `2026-06-18`,
      `2026-06-22T13:10:00Z`,
      Date.parse(`2026-06-24T12:00:00Z`),
      new Date(`2026-06-24T10:30:00Z`),
    ],
    [
      `SiO₂`,
      0.0,
      8.9,
      -9.1,
      `2026-06-17`,
      `2026-06-22T14:25:00Z`,
      Date.parse(`2026-06-24T15:40:00Z`),
      new Date(`2026-06-24T11:20:00Z`),
    ],
    [
      `Al₂O₃`,
      0.0,
      8.8,
      -17.4,
      `2026-06-16`,
      `2026-06-22T15:50:00Z`,
      Date.parse(`2026-06-25T09:10:00Z`),
      new Date(`2026-06-25T13:05:00Z`),
    ],
    [
      `MgO`,
      0.0,
      7.8,
      -6.2,
      `2026-06-15`,
      `2026-06-22T17:35:00Z`,
      Date.parse(`2026-06-25T11:25:00Z`),
      new Date(`2026-06-25T14:35:00Z`),
    ],
    [
      `CaTiO₃`,
      0.03,
      3.5,
      -16.1,
      `2026-06-14`,
      `2026-06-22T18:45:00Z`,
      Date.parse(`2026-06-25T18:05:00Z`),
      new Date(`2026-06-25T16:10:00Z`),
    ],
  ].map(([v1, v2, v3, v4, v5, v6, v7, v8]) => ({
    Formula: v1,
    'E<sub>above hull</sub>': v2,
    'E<sub>gap</sub>': v3,
    'E<sub>form</sub>': v4,
    'Created Date': v5,
    'Calculated At': v6,
    'Last Updated': v7,
    'Synthesis Time': v8,
  }))

  // oxfmt-ignore
  const columns = [
    { label: `Formula` },
    {
      label: `E<sub>above hull</sub>`,
      better: `lower`,
      color_scale: `interpolateRdYlGn`,
      format: `.2f`,
    },
    {
      label: `E<sub>gap</sub>`,
      better: `higher`,
      color_scale: `interpolateViridis`,
      format: `.1f`,
    },
    {
      label: `E<sub>form</sub>`,
      better: `lower`,
      color_scale: `interpolateBlues`,
      format: `.1f`,
    },
    { label: `Created Date`, description: `ISO date string with automatic date formatting` },
    {
      label: `Calculated At`,
      description: `ISO date-time string with automatic date/time formatting`,
    },
    {
      label: `Last Updated`,
      format_type: `datetime`,
      description: `Millisecond timestamp; click the calendar button to show age since now`,
    },
    {
      label: `Synthesis Time`,
      datetime_format: `time`,
      description: `Date object rendered as time by default`,
    },
  ]
</script>

<HeatmapTable
  {data}
  {columns}
  sort_hint={{ text: `Click column headers to sort`, position: `top`, permanent: true }}
  style="margin: 0 auto"
/>

Periodic Table Elements

All 118 chemical elements with physical and chemical properties. Features column grouping, category/phase filters, row selection with statistics, double-click to open element pages, and radioactive element highlighting:

IdentityStructurePhysicalChemicalThermalDiscovery
Symbol Name Z Mass (u) Category Period Group nval ρ (g/cm³) ratom (Å) rcov (Å) Phase χ EA (kJ/mol) IE1 (eV) Cp Tm (K) Tb (K) Year
H Hydrogen 1 1.01 diatomic nonmetal 1 1 1 0.0000899 0.25 0.31 Gas 2.20 72.8 13.60 14.30 14 20 1766
He Helium 2 4.00 noble gas 1 18 n/a 0.000179 n/a 0.28 Gas n/a −48.0 24.59 5.19 n/a 4 1868
Li Lithium 3 6.94 alkali metal 2 1 1 0.534 1.45 1.28 Solid 0.98 59.6 5.39 3.58 454 1,615 1817
Be Beryllium 4 9.01 alkaline earth metal 2 2 2 1.85 1.05 0.96 Solid 1.57 −48.0 9.32 1.82 1,560 2,742 1798
B Boron 5 10.81 metalloid 2 13 3 2.08 0.85 0.84 Solid 2.04 27.0 8.30 1.03 2,573 4,200 1808
C Carbon 6 12.01 polyatomic nonmetal 2 14 4 1.82 0.70 0.76 Solid 2.55 121.8 11.26 0.71 3,948 4,300 unknown
N Nitrogen 7 14.01 diatomic nonmetal 2 15 5 0.00125 0.65 0.71 Gas 3.04 −6.8 14.53 1.04 63 77 1772
O Oxygen 8 16.00 diatomic nonmetal 2 16 6 0.00143 0.60 0.66 Gas 3.44 141.0 13.62 0.92 51 90 1774
F Fluorine 9 19.00 diatomic nonmetal 2 17 7 0.0017 0.50 0.57 Gas 3.98 328.2 17.42 0.82 54 85 1886
Ne Neon 10 20.18 noble gas 2 18 8 0.0009 n/a 0.58 Gas n/a −116.0 21.56 1.03 25 27 1898
Na Sodium 11 22.99 alkali metal 3 1 1 0.968 1.80 1.66 Solid 0.93 52.9 5.14 1.23 371 1,156 1807
Mg Magnesium 12 24.30 alkaline earth metal 3 2 2 1.74 1.50 1.41 Solid 1.31 −40.0 7.65 1.02 923 1,363 1755
Al Aluminium 13 26.98 post-transition metal 3 13 3 2.7 1.25 1.21 Solid 1.61 41.8 5.99 0.90 933 2,792 1827
Si Silicon 14 28.09 metalloid 3 14 4 2.33 1.10 1.11 Solid 1.90 134.1 8.15 0.70 1,683 3,538 1824
P Phosphorus 15 30.97 polyatomic nonmetal 3 15 5 1.82 1.00 1.07 Solid 2.19 72.0 10.49 0.77 317 553 1669
S Sulfur 16 32.06 polyatomic nonmetal 3 16 6 2.07 1.00 1.05 Solid 2.58 200.4 10.36 0.71 389 718 1809
Cl Chlorine 17 35.45 diatomic nonmetal 3 17 7 0.0032 1.00 1.02 Gas 3.16 348.6 12.97 0.48 172 239 1774
Ar Argon 18 39.95 noble gas 3 18 8 0.00178 0.71 1.06 Gas n/a −96.0 15.76 0.52 84 87 1894
K Potassium 19 39.10 alkali metal 4 1 1 0.862 2.20 2.03 Solid 0.82 48.4 4.34 0.76 337 1,032 1807
Ca Calcium 20 40.08 alkaline earth metal 4 2 2 1.55 1.80 1.76 Solid 1.00 2.4 6.11 0.65 1,112 1,757 1808
Click headers to sort, Shift+click for multi-sort
svelte<script lang="ts">
  import { element_data, HeatmapTable } from 'matterviz'

  // Get unique categories and phases for filters
  const categories = [...new Set(element_data.map((el) => el.category))].sort()
  const phases = [...new Set(element_data.map((el) => el.phase))].sort()

  let category_filter = $state(`all`)
  let phase_filter = $state(`all`)
  let selected_rows = $state([])

  // Transform and filter element data
  let data = $derived(
    element_data
      .filter((el) => category_filter === `all` || el.category === category_filter)
      .filter((el) => phase_filter === `all` || el.phase === phase_filter)
      .map((el) => ({
        Symbol: el.radioactive ? `☢️ ${el.symbol}` : el.symbol,
        Name: el.name,
        Z: el.number,
        'Mass (u)': el.atomic_mass,
        Category: el.category,
        Period: el.period,
        Group: el.column,
        'n<sub>val</sub>': el.n_valence,
        'ρ (g/cm³)': el.density,
        'r<sub>atom</sub> (Å)': el.atomic_radius,
        'r<sub>cov</sub> (Å)': el.covalent_radius,
        χ: el.electronegativity,
        'EA (kJ/mol)': el.electron_affinity,
        'IE<sub>1</sub> (eV)': el.first_ionization,
        'C<sub>p</sub>': el.specific_heat,
        'T<sub>m</sub> (K)': el.melting_point,
        'T<sub>b</sub> (K)': el.boiling_point,
        Phase: el.phase,
        Year: el.year,
        _symbol: el.symbol, // raw symbol for selected row display
      })),
  )

  // Calculate statistics for selected elements
  let stats = $derived.by(() => {
    if (selected_rows.length === 0) return null
    const nums = (key) => selected_rows.map((row) => row[key]).filter((val) => val != null)
    const avg = (arr) =>
      arr.length ? arr.reduce((sum, val) => sum + val, 0) / arr.length : null
    return {
      count: selected_rows.length,
      avg_mass: avg(nums(`Mass (u)`))?.toFixed(2),
      avg_density: avg(nums(`ρ (g/cm³)`))?.toFixed(2),
      avg_electronegativity: avg(nums(`χ`))?.toFixed(2),
    }
  })

  // oxfmt-ignore
  const columns = [
    // Identity
    { label: `Symbol`, sticky: true, style: `min-width: 75px; font-weight: 600;` },
    { label: `Name`, group: `Identity`, style: `min-width: 100px;` },
    { label: `Z`, group: `Identity`, color_scale: `interpolateViridis`, format: `d` },
    { label: `Mass (u)`, group: `Identity`, color_scale: `interpolateBlues`, format: `.2f` },
    { label: `Category`, group: `Identity`, style: `min-width: 160px;` },
    // Structure
    { label: `Period`, group: `Structure`, color_scale: `interpolatePurples`, format: `d` },
    { label: `Group`, group: `Structure`, color_scale: `interpolateGreens`, format: `d` },
    {
      label: `n<sub>val</sub>`,
      group: `Structure`,
      color_scale: `interpolateCool`,
      format: `d`,
      description: `Valence electrons`,
    },
    // Physical
    {
      label: `ρ (g/cm³)`,
      group: `Physical`,
      better: `higher`,
      color_scale: `interpolateOranges`,
      format: `.3~`,
      scale_type: `log`,
      description: `Density`,
    },
    {
      label: `r<sub>atom</sub> (Å)`,
      group: `Physical`,
      color_scale: `interpolatePlasma`,
      format: `.2f`,
      description: `Atomic radius`,
    },
    {
      label: `r<sub>cov</sub> (Å)`,
      group: `Physical`,
      color_scale: `interpolateMagma`,
      format: `.2f`,
      description: `Covalent radius`,
    },
    { label: `Phase`, group: `Physical`, style: `min-width: 60px;` },
    // Chemical
    {
      label: `χ`,
      group: `Chemical`,
      better: `higher`,
      color_scale: `interpolateRdYlBu`,
      format: `.2f`,
      description: `Electronegativity (Pauling)`,
    },
    {
      label: `EA (kJ/mol)`,
      group: `Chemical`,
      color_scale: `interpolateRdYlGn`,
      format: `.1f`,
      description: `Electron affinity`,
    },
    {
      label: `IE<sub>1</sub> (eV)`,
      group: `Chemical`,
      better: `higher`,
      color_scale: `interpolateInferno`,
      format: `.2f`,
      description: `First ionization energy`,
    },
    // Thermal
    {
      label: `C<sub>p</sub>`,
      group: `Thermal`,
      color_scale: `interpolateYlOrRd`,
      format: `.2f`,
      description: `Specific heat (J/g·K)`,
    },
    {
      label: `T<sub>m</sub> (K)`,
      group: `Thermal`,
      color_scale: `interpolateCool`,
      format: `,.0f`,
      description: `Melting point`,
    },
    {
      label: `T<sub>b</sub> (K)`,
      group: `Thermal`,
      color_scale: `interpolateWarm`,
      format: `,.0f`,
      description: `Boiling point`,
    },
    // Discovery
    {
      label: `Year`,
      group: `Discovery`,
      color_scale: `interpolateGreys`,
      format: `d`,
      description: `Year of discovery`,
    },
  ]
</script>

<div style="display: flex; gap: 1em; margin-bottom: 1em; flex-wrap: wrap; align-items: center">
  <label>
    Category:
    <select bind:value={category_filter}>
      <option value="all">All ({element_data.length})</option>
      {#each categories as cat (cat)}
        <option value={cat}>
          {cat} ({element_data.filter((el) => el.category === cat).length})
        </option>
      {/each}
    </select>
  </label>
  <label>
    Phase:
    <select bind:value={phase_filter}>
      <option value="all">All</option>
      {#each phases as phase (phase)}
        <option value={phase}>{phase}</option>
      {/each}
    </select>
  </label>
  {#if stats}
    <span style="font-size: 0.9em; opacity: 0.8; margin-left: auto">
      Selected: <strong>{stats.count}</strong> | Avg mass: <strong>{stats.avg_mass}</strong> u
      | Avg ρ: <strong>{stats.avg_density}</strong> g/cm³ | Avg χ:
      <strong>{stats.avg_electronegativity}</strong>
    </span>
  {/if}
</div>

<HeatmapTable
  {data}
  {columns}
  scroll_style="max-height: 500px;"
  search
  export_data
  show_column_toggle
  show_row_select
  bind:selected_rows
  pagination={{ page_size: 20 }}
  sort_hint="Click headers to sort, Shift+click for multi-sort"
  onrowdblclick={(_, row) => window.open(`/${row.Name.toLowerCase()}`, `_blank`)}
  style="margin: 0 auto"
/>

{#if selected_rows.length > 0}
  <p style="margin-top: 0.5em; font-size: 0.9em; color: var(--text-color-muted)">
    Double-click a row to open element page. Selected: {selected_rows
      .map((row) => row._symbol)
      .join(`, `)}
  </p>
{/if}

Drag-and-Drop Column Reordering

Drag columns to reorder them within a group (handy for side-by-side metrics):

Drag column headers to reorder. Current order: (default)

Structure MAE RMSE Max Error
Perovskite 0.042 0.089 0.94 0.31
Spinel 0.038 0.076 0.96 0.28
Rocksalt 0.051 0.102 0.91 0.45
Wurtzite 0.029 0.058 0.97 0.19
Fluorite 0.044 0.091 0.93 0.33
Pyrite 0.035 0.071 0.95 0.24
Zincblende 0.031 0.063 0.96 0.21
Rutile 0.047 0.095 0.92 0.38
svelte<script lang="ts">
  import { HeatmapTable } from 'matterviz'

  const data = [
    [`Perovskite`, 0.042, 0.089, 0.94, 0.31],
    [`Spinel`, 0.038, 0.076, 0.96, 0.28],
    [`Rocksalt`, 0.051, 0.102, 0.91, 0.45],
    [`Wurtzite`, 0.029, 0.058, 0.97, 0.19],
    [`Fluorite`, 0.044, 0.091, 0.93, 0.33],
    [`Pyrite`, 0.035, 0.071, 0.95, 0.24],
    [`Zincblende`, 0.031, 0.063, 0.96, 0.21],
    [`Rutile`, 0.047, 0.095, 0.92, 0.38],
  ].map(([v1, v2, v3, v4, v5]) => ({
    Structure: v1,
    MAE: v2,
    RMSE: v3,
    '': v4,
    'Max Error': v5,
  }))

  // oxfmt-ignore
  const columns = [
    { label: `Structure` },
    { label: `MAE`, better: `lower`, color_scale: `interpolateRdYlGn`, format: `.3f` },
    { label: `RMSE`, better: `lower`, color_scale: `interpolateRdYlGn`, format: `.3f` },
    { label: ``, better: `higher`, color_scale: `interpolateViridis`, format: `.2f` },
    { label: `Max Error`, better: `lower`, color_scale: `interpolateOranges`, format: `.2f` },
  ]

  let column_order = $state([])
</script>

<p style="color: var(--text-color-muted); margin-bottom: 1em">
  Drag column headers to reorder. Current order: {column_order.join(`, `) || `(default)`}
</p>

<HeatmapTable {data} {columns} bind:column_order style="margin: 0 auto" />

Large Table with Scrolling

ML model benchmark with a sticky first column. Scroll horizontally to compare models across datasets:

↔️ Scroll horizontally to see all datasets  |  ↕️ Scroll vertically for all models  |  Model column stays pinned

Model Published Date Scheduled Time Last Run Queued At MP JARVIS OQMD AFLOW MC3D GNoME WBM COD ICSD Perovskites
MACE-MP-0 2020-01-15 12:00 2026-06-25 12:00 2026-06-25 12:00 0.83 0.91 0.88 0.79 0.92 0.85 0.87 0.90 0.82 0.86
CHGNet 2021-02-15 12:23 2026-06-25 08:30 2026-06-25 11:13 0.79 0.88 0.84 0.76 0.89 0.81 0.83 0.86 0.78 0.82
M3GNet 2022-03-15 12:46 2026-06-25 05:00 2026-06-25 10:26 0.75 0.84 0.80 0.72 0.85 0.77 0.79 0.82 0.74 0.78
ALIGNN 2023-04-15 13:09 2026-06-25 01:30 2026-06-25 09:39 0.81 0.89 0.86 0.77 0.90 0.83 0.85 0.88 0.80 0.84
SchNet 2024-05-15 13:32 2026-06-24 22:00 2026-06-25 08:52 0.68 0.76 0.72 0.65 0.77 0.70 0.72 0.75 0.67 0.71
DimeNet++ 2025-06-15 13:55 2026-06-24 18:30 2026-06-25 08:05 0.77 0.86 0.82 0.74 0.87 0.79 0.81 0.84 0.76 0.80
GemNet-T 2020-07-15 14:18 2026-06-24 15:00 2026-06-25 07:18 0.80 0.88 0.85 0.76 0.89 0.82 0.84 0.87 0.79 0.83
NequIP 2021-08-15 14:41 2026-06-24 11:30 2026-06-25 06:31 0.82 0.90 0.87 0.78 0.91 0.84 0.86 0.89 0.81 0.85
PaiNN 2022-09-15 15:04 2026-06-24 08:00 2026-06-25 05:44 0.76 0.85 0.81 0.73 0.86 0.78 0.80 0.83 0.75 0.79
CGCNN 2023-10-15 15:27 2026-06-24 04:30 2026-06-25 04:57 0.65 0.73 0.69 0.62 0.74 0.67 0.69 0.72 0.64 0.68
MEGNet 2024-11-15 15:50 2026-06-24 01:00 2026-06-25 04:10 0.71 0.79 0.75 0.68 0.80 0.73 0.75 0.78 0.70 0.74
BOWSR 2025-12-15 16:13 2026-06-23 21:30 2026-06-25 03:23 0.58 0.66 0.62 0.55 0.67 0.60 0.62 0.65 0.57 0.61
Wrenformer 2020-01-15 16:36 2026-06-23 18:00 2026-06-25 02:36 0.73 0.81 0.77 0.70 0.82 0.75 0.77 0.80 0.72 0.76
SevenNet 2021-02-15 16:59 2026-06-23 14:30 2026-06-25 01:49 0.84 0.92 0.89 0.80 0.93 0.86 0.88 0.91 0.83 0.87
EquiformerV2 2022-03-15 17:22 2026-06-23 11:00 2026-06-25 01:02 0.85 0.93 0.90 0.81 0.94 0.87 0.89 0.92 0.84 0.88
Graphormer 2023-04-15 17:45 2026-06-23 07:30 2026-06-25 00:15 0.72 0.80 0.76 0.69 0.81 0.74 0.76 0.79 0.71 0.75
TorchMD-NET 2024-05-15 18:08 2026-06-23 04:00 2026-06-24 23:28 0.78 0.87 0.83 0.75 0.88 0.80 0.82 0.85 0.77 0.81
SpookyNet 2025-06-15 18:31 2026-06-23 00:30 2026-06-24 22:41 0.74 0.83 0.79 0.71 0.84 0.76 0.78 0.81 0.73 0.77
ForceNet 2020-07-15 18:54 2026-06-22 21:00 2026-06-24 21:54 0.69 0.77 0.73 0.66 0.78 0.71 0.73 0.76 0.68 0.72
SphereNet 2021-08-15 19:17 2026-06-22 17:30 2026-06-24 21:07 0.75 0.84 0.80 0.72 0.85 0.77 0.79 0.82 0.74 0.78
ComENet 2022-09-15 19:40 2026-06-22 14:00 2026-06-24 20:20 0.70 0.78 0.74 0.67 0.79 0.72 0.74 0.77 0.69 0.73
EGNN 2023-10-15 20:03 2026-06-22 10:30 2026-06-24 19:33 0.66 0.74 0.70 0.63 0.75 0.68 0.70 0.73 0.65 0.69
VisNet 2024-11-15 20:26 2026-06-22 07:00 2026-06-24 18:46 0.77 0.86 0.82 0.74 0.87 0.79 0.81 0.84 0.76 0.80
Allegro 2025-12-15 20:49 2026-06-22 03:30 2026-06-24 17:59 0.81 0.89 0.86 0.77 0.90 0.83 0.85 0.88 0.80 0.84
SO3krates 2020-01-15 21:12 2026-06-22 00:00 2026-06-24 17:12 0.76 0.85 0.81 0.73 0.86 0.78 0.80 0.83 0.75 0.79
MACE-OFF 2021-02-15 21:35 2026-06-21 20:30 2026-06-24 16:25 0.86 0.94 0.91 0.82 0.95 0.88 0.90 0.93 0.85 0.89
Orb 2022-03-15 21:58 2026-06-21 17:00 2026-06-24 15:38 0.79 0.88 0.84 0.76 0.89 0.81 0.83 0.86 0.78 0.82
FAENet 2023-04-15 22:21 2026-06-21 13:30 2026-06-24 14:51 0.67 0.75 0.71 0.64 0.76 0.69 0.71 0.74 0.66 0.70
svelte<script lang="ts">
  import { HeatmapTable } from 'matterviz'

  const models = [
    [`MACE-MP-0`, 0.83, 0.91, 0.88, 0.79, 0.92, 0.85, 0.87, 0.9, 0.82, 0.86],
    [`CHGNet`, 0.79, 0.88, 0.84, 0.76, 0.89, 0.81, 0.83, 0.86, 0.78, 0.82],
    [`M3GNet`, 0.75, 0.84, 0.8, 0.72, 0.85, 0.77, 0.79, 0.82, 0.74, 0.78],
    [`ALIGNN`, 0.81, 0.89, 0.86, 0.77, 0.9, 0.83, 0.85, 0.88, 0.8, 0.84],
    [`SchNet`, 0.68, 0.76, 0.72, 0.65, 0.77, 0.7, 0.72, 0.75, 0.67, 0.71],
    [`DimeNet++`, 0.77, 0.86, 0.82, 0.74, 0.87, 0.79, 0.81, 0.84, 0.76, 0.8],
    [`GemNet-T`, 0.8, 0.88, 0.85, 0.76, 0.89, 0.82, 0.84, 0.87, 0.79, 0.83],
    [`NequIP`, 0.82, 0.9, 0.87, 0.78, 0.91, 0.84, 0.86, 0.89, 0.81, 0.85],
    [`PaiNN`, 0.76, 0.85, 0.81, 0.73, 0.86, 0.78, 0.8, 0.83, 0.75, 0.79],
    [`CGCNN`, 0.65, 0.73, 0.69, 0.62, 0.74, 0.67, 0.69, 0.72, 0.64, 0.68],
    [`MEGNet`, 0.71, 0.79, 0.75, 0.68, 0.8, 0.73, 0.75, 0.78, 0.7, 0.74],
    [`BOWSR`, 0.58, 0.66, 0.62, 0.55, 0.67, 0.6, 0.62, 0.65, 0.57, 0.61],
    [`Wrenformer`, 0.73, 0.81, 0.77, 0.7, 0.82, 0.75, 0.77, 0.8, 0.72, 0.76],
    [`SevenNet`, 0.84, 0.92, 0.89, 0.8, 0.93, 0.86, 0.88, 0.91, 0.83, 0.87],
    [`EquiformerV2`, 0.85, 0.93, 0.9, 0.81, 0.94, 0.87, 0.89, 0.92, 0.84, 0.88],
    [`Graphormer`, 0.72, 0.8, 0.76, 0.69, 0.81, 0.74, 0.76, 0.79, 0.71, 0.75],
    [`TorchMD-NET`, 0.78, 0.87, 0.83, 0.75, 0.88, 0.8, 0.82, 0.85, 0.77, 0.81],
    [`SpookyNet`, 0.74, 0.83, 0.79, 0.71, 0.84, 0.76, 0.78, 0.81, 0.73, 0.77],
    [`ForceNet`, 0.69, 0.77, 0.73, 0.66, 0.78, 0.71, 0.73, 0.76, 0.68, 0.72],
    [`SphereNet`, 0.75, 0.84, 0.8, 0.72, 0.85, 0.77, 0.79, 0.82, 0.74, 0.78],
    [`ComENet`, 0.7, 0.78, 0.74, 0.67, 0.79, 0.72, 0.74, 0.77, 0.69, 0.73],
    [`EGNN`, 0.66, 0.74, 0.7, 0.63, 0.75, 0.68, 0.7, 0.73, 0.65, 0.69],
    [`VisNet`, 0.77, 0.86, 0.82, 0.74, 0.87, 0.79, 0.81, 0.84, 0.76, 0.8],
    [`Allegro`, 0.81, 0.89, 0.86, 0.77, 0.9, 0.83, 0.85, 0.88, 0.8, 0.84],
    [`SO3krates`, 0.76, 0.85, 0.81, 0.73, 0.86, 0.78, 0.8, 0.83, 0.75, 0.79],
    [`MACE-OFF`, 0.86, 0.94, 0.91, 0.82, 0.95, 0.88, 0.9, 0.93, 0.85, 0.89],
    [`Orb`, 0.79, 0.88, 0.84, 0.76, 0.89, 0.81, 0.83, 0.86, 0.78, 0.82],
    [`FAENet`, 0.67, 0.75, 0.71, 0.64, 0.76, 0.69, 0.71, 0.74, 0.66, 0.7],
  ]

  const benchmarks = `MP JARVIS OQMD AFLOW MC3D GNoME WBM COD ICSD Perovskites`.split(` `)

  const run_epoch = Date.parse(`2026-06-25T12:00:00Z`)

  const data = models.map(([name, ...scores], model_idx) => {
    const row = {
      Model: name,
      'Published Date': `202${model_idx % 6}-${String((model_idx % 12) + 1).padStart(2, `0`)}-15`,
      'Scheduled Time': new Date(run_epoch + model_idx * 23 * 60 * 1000),
      'Last Run': run_epoch - model_idx * 3.5 * 60 * 60 * 1000,
      'Queued At': new Date(run_epoch - model_idx * 47 * 60 * 1000).toISOString(),
    }
    benchmarks.forEach((bench, idx) => {
      row[bench] = scores[idx]
    })
    return row
  })

  const columns = [
    { label: `Model`, sticky: true, style: `min-width: 120px; font-weight: 600;` },
    { label: `Published Date`, style: `min-width: 115px;` },
    {
      label: `Scheduled Time`,
      datetime_format: `time`,
      style: `min-width: 115px;`,
      description: `Date object rendered as time by default`,
    },
    {
      label: `Last Run`,
      format_type: `datetime`,
      style: `min-width: 115px;`,
      description: `Numeric timestamp from the latest benchmark run`,
    },
    {
      label: `Queued At`,
      style: `min-width: 125px;`,
      description: `ISO timestamp for the queued benchmark job`,
    },
    ...benchmarks.map((bench) => ({
      label: bench,
      better: `higher`,
      color_scale: `interpolateViridis`,
      format: `.2f`,
      style: `min-width: 95px;`,
    })),
  ]
</script>

<p style="color: var(--text-color-muted); margin-bottom: 0.5em; font-size: 0.9em">
  ↔️ Scroll horizontally to see all datasets &nbsp;|&nbsp; ↕️ Scroll vertically for all models
  &nbsp;|&nbsp; Model column stays pinned
</p>

<HeatmapTable
  {data}
  {columns}
  scroll_style="max-height: 400px; border-inline: none"
  style="margin: 0 auto"
/>

Values with Uncertainties

The table correctly handles numeric strings with uncertainty notation for both sorting and heatmap coloring. Values like 1.23 ± 0.05, 1.23 +- 0.05, and 1.23(5) are parsed to extract the primary number:

Click column headers to sort. Values are sorted by the primary number, ignoring the ± uncertainty

ID Formula Egap (eV) ρ (g/cm³) κ (W/m·K)
mp-149 Si 1.12 ± 0.02 2.329 ± 0.005 148 +- 3
mp-32 GaAs 1.42 ± 0.03 5.317 ± 0.008 55 +- 2
mp-390 TiO₂ 3.20 ± 0.04 4.230 ± 0.007 8.5 +- 0.5
mp-804 SiC 3.26 ± 0.04 3.217 ± 0.005 490 +- 20
mp-35 GaN 3.40 ± 0.05 6.150 ± 0.010 130 +- 8
mp-2133 ZnO 3.44 ± 0.05 5.606 ± 0.012 60 +- 4
mp-66 Diamond 5.47 ± 0.01 3.515 ± 0.002 2200 +- 50
mp-1265 MgO 7.83 ± 0.08 3.583 ± 0.003 60 +- 3
mp-5020 NaCl 8.50 ± 0.12 2.165 ± 0.003 6.5 +- 0.3
mp-1143 Al₂O₃ 8.80 ± 0.10 3.987 ± 0.004 30 +- 2
svelte<script lang="ts">
  import { HeatmapTable } from 'matterviz'

  // Experimental measurements with uncertainties in various formats
  const data = [
    [`mp-149`, `Si`, `1.12 ± 0.02`, `2.329 ± 0.005`, `148 +- 3`],
    [`mp-32`, `GaAs`, `1.42 ± 0.03`, `5.317 ± 0.008`, `55 +- 2`],
    [`mp-2133`, `ZnO`, `3.44 ± 0.05`, `5.606 ± 0.012`, `60 +- 4`],
    [`mp-390`, `TiO₂`, `3.20 ± 0.04`, `4.230 ± 0.007`, `8.5 +- 0.5`],
    [`mp-66`, `Diamond`, `5.47 ± 0.01`, `3.515 ± 0.002`, `2200 +- 50`],
    [`mp-1143`, `Al₂O₃`, `8.80 ± 0.10`, `3.987 ± 0.004`, `30 +- 2`],
    [`mp-1265`, `MgO`, `7.83 ± 0.08`, `3.583 ± 0.003`, `60 +- 3`],
    [`mp-804`, `SiC`, `3.26 ± 0.04`, `3.217 ± 0.005`, `490 +- 20`],
    [`mp-35`, `GaN`, `3.40 ± 0.05`, `6.150 ± 0.010`, `130 +- 8`],
    [`mp-5020`, `NaCl`, `8.50 ± 0.12`, `2.165 ± 0.003`, `6.5 +- 0.3`],
  ].map(([id, formula, gap, density, thermal]) => ({
    ID: id,
    Formula: formula,
    'E<sub>gap</sub> (eV)': gap,
    'ρ (g/cm³)': density,
    'κ (W/m·K)': thermal,
  }))

  // oxfmt-ignore
  const columns = [
    { label: `ID` },
    { label: `Formula` },
    {
      label: `E<sub>gap</sub> (eV)`,
      better: `higher`,
      color_scale: `interpolateViridis`,
      description: `Band gap with measurement uncertainty`,
    },
    {
      label: `ρ (g/cm³)`,
      color_scale: `interpolateBlues`,
      description: `Density with uncertainty`,
    },
    {
      label: `κ (W/m·K)`,
      better: `higher`,
      color_scale: `interpolateOranges`,
      description: `Thermal conductivity with uncertainty`,
    },
  ]
</script>

<p style="color: var(--text-color-muted); margin-bottom: 0.5em; font-size: 0.9em">
  Click column headers to sort. Values are sorted by the primary number, ignoring the ±
  uncertainty
</p>

<HeatmapTable {data} {columns} initial_sort="E<sub>gap</sub> (eV)" style="margin: 0 auto" />

Color Scales

Try D3 color scales, linear vs log scale types, and the better prop (which end of the scale counts as “good”). Log scale suits properties spanning many orders of magnitude, e.g. electrical conductivity:

Material Egap (eV) σ (S/m) κ (W/m·K)
Si 1.12 1.56e-3 150
Copper 0.00 5.96e+7 401
Diamond 5.47 1.00e-13 2,200
GaAs 1.42 1.00e-8 55
ZnO 3.44 1.00e-6 60
SiC 3.26 1.00e-6 490
GaN 3.40 1.00e-10 130
Al₂O₃ 8.80 1.00e-14 30
MgO 7.80 1.00e-15 60
TiO₂ 3.20 1.00e-12 9
svelte<script lang="ts">
  import { HeatmapTable } from 'matterviz'

  const color_scales = [
    `interpolateViridis`,
    `interpolatePlasma`,
    `interpolateInferno`,
    `interpolateMagma`,
    `interpolateCividis`,
    `interpolateCool`,
    `interpolateWarm`,
    `interpolateRdYlBu`,
    `interpolateRdYlGn`,
    `interpolateSpectral`,
    `interpolatePurples`,
    `interpolateBlues`,
    `interpolateGreens`,
    `interpolateOranges`,
    `interpolateReds`,
  ]

  let selected_scale = $state(`interpolateViridis`)
  let better = $state(`higher`)
  let scale_type = $state(`linear`)
  let heatmap_opacity = $state(1)

  // Materials with properties spanning different orders of magnitude
  const data = [
    [`Si`, 1.12, 1.56e-3, 150],
    [`Copper`, 0, 5.96e7, 401],
    [`Diamond`, 5.47, 1e-13, 2200],
    [`GaAs`, 1.42, 1e-8, 55],
    [`ZnO`, 3.44, 1e-6, 60],
    [`SiC`, 3.26, 1e-6, 490],
    [`GaN`, 3.4, 1e-10, 130],
    [`Al₂O₃`, 8.8, 1e-14, 30],
    [`MgO`, 7.8, 1e-15, 60],
    [`TiO₂`, 3.2, 1e-12, 8.5],
  ].map(([v1, v2, v3, v4]) => ({
    Material: v1,
    'E<sub>gap</sub> (eV)': v2,
    'σ (S/m)': v3,
    'κ (W/m·K)': v4,
  }))

  let columns = $derived([
    { label: `Material` },
    {
      label: `E<sub>gap</sub> (eV)`,
      better,
      color_scale: selected_scale,
      format: `.2f`,
      description: `Band gap`,
    },
    {
      label: `σ (S/m)`,
      better,
      color_scale: selected_scale,
      scale_type,
      format: `.2e`,
      description: `Electrical conductivity (try log scale!)`,
    },
    {
      label: `κ (W/m·K)`,
      better,
      color_scale: selected_scale,
      format: `,.0f`,
      description: `Thermal conductivity`,
    },
  ])
</script>

<div style="display: flex; gap: 2em; margin-bottom: 1em; flex-wrap: wrap">
  <label>
    Color Scale:
    <select bind:value={selected_scale}>
      {#each color_scales as scale (scale)}
        <option value={scale}>{scale.replace(`interpolate`, ``)}</option>
      {/each}
    </select>
  </label>
  <label>
    Better:
    <select bind:value={better}>
      <option value="higher">Higher</option>
      <option value="lower">Lower</option>
    </select>
  </label>
  <label>
    Scale Type:
    <select bind:value={scale_type}>
      <option value="linear">Linear</option>
      <option value="log">Logarithmic</option>
    </select>
  </label>
  <label>
    Opacity: {Math.round(heatmap_opacity * 100)}%
    <input type="range" min="0" max="1" step="0.01" bind:value={heatmap_opacity} />
  </label>
</div>

<HeatmapTable {data} {columns} bind:heatmap_opacity style="margin: 0 auto" />

Filters, Summaries and Normalization

Combines per-column filters, filtered summaries, diverging normalization, quantile clipping, shared domains, data bars, and best-value rings.

Material E_form MAE RMSE sigma Tier
Fe2O3 −8.5 0.042 0.089 1.2e-6 oxide
TiO2 −9.8 0.038 0.076 4.5e-7 oxide
ZnO −3.6 0.051 0.102 2.1e-5 oxide
Cu2O 1.7 0.029 0.058 8.9e-3 oxide
NaCl −4.1 0.044 0.091 1.1e-9 halide
KBr 2.4 0.035 0.071 3.3e-9 halide
LiF −6.2 0.031 0.063 6.0e+3 halide
mean−4.00.0390.0798.6e+2
min−9.80.0290.0581.1e-9
max2.40.0510.1026.0e+3

Click a funnel to filter a column — the summary row follows. Click a header three times to clear the sort. Focus a cell and use arrows (Shift extends the selection, Alt moves the column). Export now offers Markdown and LaTeX alongside CSV/JSON.

svelte<script lang="ts">
  import { HeatmapTable } from 'matterviz'

  const rows = [
    { Material: `Fe2O3`, E_form: -8.5, MAE: 0.042, RMSE: 0.089, sigma: 1.2e-6, Tier: `oxide` },
    { Material: `TiO2`, E_form: -9.8, MAE: 0.038, RMSE: 0.076, sigma: 4.5e-7, Tier: `oxide` },
    { Material: `ZnO`, E_form: -3.6, MAE: 0.051, RMSE: 0.102, sigma: 2.1e-5, Tier: `oxide` },
    { Material: `Cu2O`, E_form: 1.7, MAE: 0.029, RMSE: 0.058, sigma: 8.9e-3, Tier: `oxide` },
    { Material: `NaCl`, E_form: -4.1, MAE: 0.044, RMSE: 0.091, sigma: 1.1e-9, Tier: `halide` },
    { Material: `KBr`, E_form: 2.4, MAE: 0.035, RMSE: 0.071, sigma: 3.3e-9, Tier: `halide` },
    { Material: `LiF`, E_form: -6.2, MAE: 0.031, RMSE: 0.063, sigma: 6_000, Tier: `halide` },
  ]

  let density = $state(`cosy`)

  const columns = [
    { label: `Material`, sticky: true },
    // diverging: zero sits at the scale's midpoint, so sign reads at a glance
    { label: `E_form`, color_scale: `interpolateRdBu`, normalize: `diverging`, format: `.1f` },
    // one shared domain, so MAE and RMSE cells are comparable to each other
    {
      label: `MAE`,
      better: `lower`,
      domain_group: `error`,
      highlight_best: true,
      format: `.3f`,
    },
    { label: `RMSE`, better: `lower`, domain_group: `error`, format: `.3f` },
    // spans 12 orders of magnitude; quantile clipping keeps the middle legible
    { label: `sigma`, normalize: `quantile`, render_as: `bar`, format: `.1e` },
    { label: `Tier` },
  ]
</script>

<label>
  Density:
  <select bind:value={density}>
    {#each [`compact`, `cosy`, `comfortable`] as option (option)}
      <option value={option}>{option}</option>
    {/each}
  </select>
</label>

<HeatmapTable
  data={rows}
  {columns}
  {density}
  show_filters
  summary={[`mean`, `min`, `max`]}
  export_data
  keyboard_cells
  style="margin-top: 1em"
/>

<p style="font-size: 0.85em; opacity: 0.75">
  Click a funnel to filter a column — the summary row follows. Click a header three times to
  clear the sort. Focus a cell and use arrows (Shift extends the selection, Alt moves the
  column). Export now offers Markdown and LaTeX alongside CSV/JSON.
</p>