Heatmap Table
Columns use a stable id, a display label, and an optional row-property key (defaults to id). Changing the label or group preserves sorting, visibility, order, and preferences. Omit columns to discover them from the data. Put a custom renderer on the column’s cell snippet; it receives { row, col, val } and overrides the table-wide cell snippet.
For row selection, supply row_key as a property name or a function returning a unique string or number, and bind selected_ids. Selection survives replacing row objects; derive selected rows from the current data when needed. JSON exports use column IDs as keys; CSV and other display formats use labels.
Bind visible_rows to read the rows the table currently shows: every row left after search and filters, in the active sort order and across all pages. Use it for custom exports or top-N picks instead of re-implementing the table’s sort and filter logic.
For large tables (thousands of rows), hold data in $state.raw rather than $state. A deep $state proxy wraps every row and cell in a reactive signal the table then reads through, which made a 20,000-row, 13-column table mount about 6x slower and refresh about 14x slower in our measurements. Replace the array to update it (rows = [...rows, new_row]) instead of mutating rows in place.
Basic Usage
Interactive example
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(
([vector_1, vector_2, vector_3, vector_4, vector_5, vector_6, vector_7, vector_8]) => ({
Formula: vector_1,
'E<sub>above hull</sub>': vector_2,
'E<sub>gap</sub>': vector_3,
'E<sub>form</sub>': vector_4,
'Created Date': vector_5,
'Calculated At': vector_6,
'Last Updated': vector_7,
'Synthesis Time': vector_8,
}),
)
// oxfmt-ignore
const columns = [
{ id: `Formula`, label: `Formula` },
{
id: `E<sub>above hull</sub>`,
label: `E<sub>above hull</sub>`,
better: `lower`,
color_scale: `interpolateRdYlGn`,
format: `.2f`,
},
{
id: `E<sub>gap</sub>`,
label: `E<sub>gap</sub>`,
better: `higher`,
color_scale: `interpolateViridis`,
format: `.1f`,
},
{
id: `E<sub>form</sub>`,
label: `E<sub>form</sub>`,
better: `lower`,
color_scale: `interpolateBlues`,
format: `.1f`,
},
{
id: `Created Date`,
label: `Created Date`,
description: `ISO date string with automatic date formatting`,
},
{
id: `Calculated At`,
label: `Calculated At`,
description: `ISO date-time string with automatic date/time formatting`,
},
{
id: `Last Updated`,
label: `Last Updated`,
datetime_format: `datetime`,
description: `Millisecond timestamp; click the calendar button to show age since now`,
},
{
id: `Synthesis Time`,
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:
Interactive example
svelte<script lang="ts">
import { element_data, format_num, HeatmapTable } from 'matterviz'
// Get unique categories and phases for filters
const categories = [...new Set(element_data.map((element) => element.category))].sort()
const phases = [...new Set(element_data.map((element) => element.phase))].sort()
let category_filter = $state(`all`)
let phase_filter = $state(`all`)
let selected_ids = $state<number[]>([])
let selected_rows = $derived(data.filter((row) => selected_ids.includes(row.atomic_number)))
// Transform and filter element data
let data = $derived(
element_data
.filter((element) => category_filter === `all` || element.category === category_filter)
.filter((element) => phase_filter === `all` || element.phase === phase_filter)
.map((element) => ({
symbol: element.radioactive ? `☢️ ${element.symbol}` : element.symbol,
name: element.name,
atomic_number: element.number,
atomic_mass: element.atomic_mass,
category: element.category,
period: element.period,
group: element.column,
n_valence: element.n_valence,
density: element.density,
atomic_radius: element.atomic_radius,
covalent_radius: element.covalent_radius,
electronegativity: element.electronegativity,
electron_affinity: element.electron_affinity,
first_ionization: element.first_ionization,
specific_heat: element.specific_heat,
melting_point: element.melting_point,
boiling_point: element.boiling_point,
phase: element.phase,
year: element.year,
element_symbol: element.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: format_num(avg(nums(`atomic_mass`)), `.2f`),
avg_density: format_num(avg(nums(`density`)), `.2f`),
avg_electronegativity: format_num(avg(nums(`electronegativity`)), `.2f`),
}
})
// IDs access data fields; labels and groups can change independently.
// oxfmt-ignore
const columns = [
// Identity
{
id: `symbol`,
label: `Symbol`,
sticky: true,
style: `min-width: 75px; font-weight: 600;`,
},
{
id: `name`,
label: `Name`,
group: `Identity`,
style: `min-width: 100px;`,
},
{
id: `atomic_number`,
label: `Z`,
group: `Identity`,
color_scale: `interpolateViridis`,
format: `d`,
},
{
id: `atomic_mass`,
label: `Mass (u)`,
group: `Identity`,
color_scale: `interpolateBlues`,
format: `.2f`,
},
{
id: `category`,
label: `Category`,
group: `Identity`,
style: `min-width: 160px;`,
},
// Structure
{
id: `period`,
label: `Period`,
group: `Structure`,
color_scale: `interpolatePurples`,
format: `d`,
},
{
id: `group`,
label: `Group`,
group: `Structure`,
color_scale: `interpolateGreens`,
format: `d`,
},
{
id: `n_valence`,
label: `n<sub>val</sub>`,
group: `Structure`,
color_scale: `interpolateCool`,
format: `d`,
description: `Valence electrons`,
},
// Physical
{
id: `density`,
label: `ρ (g/cm³)`,
group: `Physical`,
better: `higher`,
color_scale: `interpolateOranges`,
format: `.3~`,
scale_type: `log`,
description: `Density`,
},
{
id: `atomic_radius`,
label: `r<sub>atom</sub> (Å)`,
group: `Physical`,
color_scale: `interpolatePlasma`,
format: `.2f`,
description: `Atomic radius`,
},
{
id: `covalent_radius`,
label: `r<sub>cov</sub> (Å)`,
group: `Physical`,
color_scale: `interpolateMagma`,
format: `.2f`,
description: `Covalent radius`,
},
{
id: `phase`,
label: `Phase`,
group: `Physical`,
style: `min-width: 60px;`,
},
// Chemical
{
id: `electronegativity`,
label: `χ`,
group: `Chemical`,
better: `higher`,
color_scale: `interpolateRdYlBu`,
format: `.2f`,
description: `Electronegativity (Pauling)`,
},
{
id: `electron_affinity`,
label: `EA (kJ/mol)`,
group: `Chemical`,
color_scale: `interpolateRdYlGn`,
format: `.1f`,
description: `Electron affinity`,
},
{
id: `first_ionization`,
label: `IE<sub>1</sub> (eV)`,
group: `Chemical`,
better: `higher`,
color_scale: `interpolateInferno`,
format: `.2f`,
description: `First ionization energy`,
},
// Thermal
{
id: `specific_heat`,
label: `C<sub>p</sub>`,
group: `Thermal`,
color_scale: `interpolateYlOrRd`,
format: `.2f`,
description: `Specific heat (J/g·K)`,
},
{
id: `melting_point`,
label: `T<sub>m</sub> (K)`,
group: `Thermal`,
color_scale: `interpolateCool`,
format: `,.0f`,
description: `Melting point`,
},
{
id: `boiling_point`,
label: `T<sub>b</sub> (K)`,
group: `Thermal`,
color_scale: `interpolateWarm`,
format: `,.0f`,
description: `Boiling point`,
},
// Discovery
{
id: `year`,
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((element) => element.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
row_key="atomic_number"
bind:selected_ids
pagination={{ page_size: 20 }}
sort_hint="Click headers to sort, Shift+click for multi-sort"
on_row_double_click={(_, 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.element_symbol)
.join(`, `)}
</p>
{/if}Drag-and-Drop Column Reordering
Drag columns to reorder them within a group (handy for side-by-side metrics):
Interactive example
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(([vector_1, vector_2, vector_3, vector_4, vector_5]) => ({
Structure: vector_1,
MAE: vector_2,
RMSE: vector_3,
'R²': vector_4,
'Max Error': vector_5,
}))
// oxfmt-ignore
const columns = [
{ id: `Structure`, label: `Structure` },
{
id: `MAE`,
label: `MAE`,
better: `lower`,
color_scale: `interpolateRdYlGn`,
format: `.3f`,
},
{
id: `RMSE`,
label: `RMSE`,
better: `lower`,
color_scale: `interpolateRdYlGn`,
format: `.3f`,
},
{
id: `R²`,
label: `R²`,
better: `higher`,
color_scale: `interpolateViridis`,
format: `.2f`,
},
{
id: `Max Error`,
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:
Interactive example
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 = [
{
id: `Model`,
label: `Model`,
sticky: true,
style: `min-width: 120px; font-weight: 600;`,
},
{ id: `Published Date`, label: `Published Date`, style: `min-width: 115px;` },
{
id: `Scheduled Time`,
label: `Scheduled Time`,
datetime_format: `time`,
style: `min-width: 115px;`,
description: `Date object rendered as time by default`,
},
{
id: `Last Run`,
label: `Last Run`,
datetime_format: `datetime`,
style: `min-width: 115px;`,
description: `Numeric timestamp from the latest benchmark run`,
},
{
id: `Queued At`,
label: `Queued At`,
style: `min-width: 125px;`,
description: `ISO timestamp for the queued benchmark job`,
},
...benchmarks.map((bench) => ({
id: 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 | ↕️ Scroll vertically for all models
| 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:
Interactive example
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(([identifier, formula, gap, density, thermal]) => ({
ID: identifier,
Formula: formula,
'E<sub>gap</sub> (eV)': gap,
'ρ (g/cm³)': density,
'κ (W/m·K)': thermal,
}))
// oxfmt-ignore
const columns = [
{ id: `ID`, label: `ID` },
{ id: `Formula`, label: `Formula` },
{
id: `E<sub>gap</sub> (eV)`,
label: `E<sub>gap</sub> (eV)`,
better: `higher`,
color_scale: `interpolateViridis`,
description: `Band gap with measurement uncertainty`,
},
{
id: `ρ (g/cm³)`,
label: `ρ (g/cm³)`,
color_scale: `interpolateBlues`,
description: `Density with uncertainty`,
},
{
id: `κ (W/m·K)`,
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:
Interactive example
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(([vector_1, vector_2, vector_3, vector_4]) => ({
Material: vector_1,
'E<sub>gap</sub> (eV)': vector_2,
'σ (S/m)': vector_3,
'κ (W/m·K)': vector_4,
}))
let columns = $derived([
{ id: `Material`, label: `Material` },
{
id: `E<sub>gap</sub> (eV)`,
label: `E<sub>gap</sub> (eV)`,
better,
color_scale: selected_scale,
format: `.2f`,
description: `Band gap`,
},
{
id: `σ (S/m)`,
label: `σ (S/m)`,
better,
color_scale: selected_scale,
scale_type,
format: `.2e`,
description: `Electrical conductivity (try log scale!)`,
},
{
id: `κ (W/m·K)`,
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.
Interactive example
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 = [
{ id: `Material`, label: `Material`, sticky: true },
// diverging: zero sits at the scale's midpoint, so sign reads at a glance
{
id: `E_form`,
label: `E_form`,
color_scale: `interpolateRdBu`,
normalize: `diverging`,
format: `.1f`,
},
// one shared domain, so MAE and RMSE cells are comparable to each other
{
id: `MAE`,
label: `MAE`,
better: `lower`,
domain_group: `error`,
highlight_best: true,
format: `.3f`,
},
{ id: `RMSE`, label: `RMSE`, better: `lower`, domain_group: `error`, format: `.3f` },
// spans 12 orders of magnitude; quantile clipping keeps the middle legible
{ id: `sigma`, label: `sigma`, normalize: `quantile`, render_as: `bar`, format: `.1e` },
{ id: `Tier`, 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>