1 GitHub star = 1 new component.

Star the repo
Charts & Stats chartsvgzero-dependencyheatmapmatrixsequentialgrid

Heatmap Chart

Matrix heatmap on a single-hue sequential ramp with values, scale legend and keyboard-focusable cells.

Live Preview

Error rate by service

Per weekday, % of requests

MonTueWedThuFriSatSun
auth 0.2 0.3 0.2 0.4 0.6 0.1 0.1
billing 0.8 1.2 0.9 1.6 2.4 0.4 0.3
search 0.4 0.5 0.7 0.6 0.9 0.3 0.2
uploads 1.1 1.4 1.9 2.8 3.2 0.9 0.6
webhooks 0.3 0.2 0.4 0.5 0.4 0.2 0.1
View data
MonTueWedThuFriSatSun
auth0.2%0.3%0.2%0.4%0.6%0.1%0.1%
billing0.8%1.2%0.9%1.6%2.4%0.4%0.3%
search0.4%0.5%0.7%0.6%0.9%0.3%0.2%
uploads1.1%1.4%1.9%2.8%3.2%0.9%0.6%
webhooks0.3%0.2%0.4%0.5%0.4%0.2%0.1%

Usage

Copy and paste this code into your Astro page:

astro
---
import { HeatmapChart } from 'astro-component-kit/charts';
---

<HeatmapChart
  rows={['auth', 'billing']}
  columns={['Mon', 'Tue', 'Wed']}
  values={[[0.2, 0.3, 0.2], [0.8, 1.2, 0.9]]}
  suffix="%"
/>
--- import { HeatmapChart } from 'astro-component-kit/charts'; --- <HeatmapChart rows={['auth', 'billing']} columns={['Mon', 'Tue', 'Wed']} values={[[0.2, 0.3, 0.2], [0.8, 1.2, 0.9]]} suffix="%" />

Manual Installation

If you are not using the npm package, create a new file and paste the following code: src/components/lib/HeatmapChart.astro:

astro
---
/**
 * HeatmapChart — Matrix of cells colored on a single-hue sequential ramp
 * (light = low, saturated = high), with a scale legend.
 *
 * @param {string[]} rows - Row labels.
 * @param {string[]} columns - Column labels.
 * @param {number[][]} values - values[row][col].
 * @param {boolean} showValues - Print values inside cells.
 * @param {string} suffix - Value suffix.
 * @param {string} color - Ramp hue.
 * @param {string} variant - Card style (glass|solid|ghost).
 * @param {string} size - Density (sm|md|lg).
 */
import ChartFrame from './ChartFrame.astro';

interface Props {
  title?: string;
  subtitle?: string;
  rows?: string[];
  columns?: string[];
  values?: number[][];
  showValues?: boolean;
  suffix?: string;
  color?: string;
  variant?: 'glass' | 'solid' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

const {
  title = 'Error rate by service',
  subtitle = 'Per weekday, % of requests',
  rows = ['auth', 'billing', 'search', 'uploads', 'webhooks'],
  columns = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
  values = [
    [0.2, 0.3, 0.2, 0.4, 0.6, 0.1, 0.1],
    [0.8, 1.2, 0.9, 1.6, 2.4, 0.4, 0.3],
    [0.4, 0.5, 0.7, 0.6, 0.9, 0.3, 0.2],
    [1.1, 1.4, 1.9, 2.8, 3.2, 0.9, 0.6],
    [0.3, 0.2, 0.4, 0.5, 0.4, 0.2, 0.1],
  ],
  showValues = true,
  suffix = '%',
  color = 'var(--chart-1, #3987e5)',
  variant = 'glass',
  size = 'md',
  className = '',
} = Astro.props;

const flat = values.flat();
const min = Math.min(...flat);
const max = Math.max(...flat);
const t = (v: number) => (max === min ? 1 : (v - min) / (max - min));
const fill = (v: number) => `color-mix(in srgb, ${color} ${Math.round(12 + t(v) * 88)}%, #141416)`;
---

<ChartFrame
  title={title}
  subtitle={subtitle}
  variant={variant}
  size={size}
  className={className}
  table={{ headers: ['', ...columns], rows: rows.map((r, ri) => [r, ...values[ri].map((v) => `${v}${suffix}`)]) }}
>
  <div class="hm" style={`--hm-cols:${columns.length}`}>
    <div class="hm__grid" role="grid" aria-label={title}>
      <div class="hm__row" role="row">
        <span class="hm__corner" role="columnheader"></span>
        {columns.map((c) => <span class="hm__col" role="columnheader">{c}</span>)}
      </div>
      {rows.map((r, ri) => (
        <div class="hm__row" role="row">
          <span class="hm__rowlabel" role="rowheader">{r}</span>
          {columns.map((c, ci) => {
            const v = values[ri]?.[ci] ?? 0;
            return (
              <span class="hm__cell cf-mark" role="gridcell" tabindex="0" style={`background:${fill(v)}`}
                aria-label={`${r}, ${c}: ${v}${suffix}`} data-tip-title={`${r} · ${c}`} data-tip={`${v}${suffix}`}>
                {showValues && <span class={t(v) > 0.55 ? 'hm__v hm__v--strong' : 'hm__v'}>{v}</span>}
              </span>
            );
          })}
        </div>
      ))}
    </div>
    <div class="hm__scale" aria-hidden="true">
      <span>{min}{suffix}</span>
      <span class="hm__ramp" style={`background:linear-gradient(90deg, ${fill(min)}, ${fill(max)})`}></span>
      <span>{max}{suffix}</span>
    </div>
  </div>
</ChartFrame>

<style>
  .hm { overflow-x: auto; }
  .hm__grid { display: grid; gap: 3px; min-width: calc(5rem + var(--hm-cols) * 2.4rem); }
  .hm__row {
    display: grid;
    grid-template-columns: 5rem repeat(var(--hm-cols), minmax(2.2rem, 1fr));
    gap: 3px;
  }
  .hm__col { text-align: center; font-size: 0.6875rem; color: var(--c-text-3, #8a8a8a); padding-bottom: 0.2rem; }
  .hm__rowlabel {
    display: flex;
    align-items: center;
    font-size: 0.75rem;
    color: var(--c-text-2, #a3a3a3);
    font-family: var(--font-mono, monospace);
  }
  .hm__cell {
    display: grid;
    place-items: center;
    min-height: 34px;
    border-radius: 4px;
    transition: transform 0.15s ease;
  }
  .hm__cell:hover { transform: scale(1.06); }
  .hm__cell:focus-visible { outline: 2px solid var(--c-text-1, #fff); outline-offset: 1px; }
  .hm__v { font-size: 0.6875rem; color: rgb(var(--ink-rgb, 255 255 255) / 0.7); font-variant-numeric: tabular-nums; }
  .hm__v--strong { color: #fff; font-weight: 650; }
  .hm__scale {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin-top: 0.9rem;
    font-size: 0.6875rem;
    color: var(--c-text-3, #8a8a8a);
  }
  .hm__ramp { flex: 0 1 160px; height: 8px; border-radius: 4px; }
</style>
--- /** * HeatmapChart — Matrix of cells colored on a single-hue sequential ramp * (light = low, saturated = high), with a scale legend. * * @param {string[]} rows - Row labels. * @param {string[]} columns - Column labels. * @param {number[][]} values - values[row][col]. * @param {boolean} showValues - Print values inside cells. * @param {string} suffix - Value suffix. * @param {string} color - Ramp hue. * @param {string} variant - Card style (glass|solid|ghost). * @param {string} size - Density (sm|md|lg). */ import ChartFrame from './ChartFrame.astro'; interface Props { title?: string; subtitle?: string; rows?: string[]; columns?: string[]; values?: number[][]; showValues?: boolean; suffix?: string; color?: string; variant?: 'glass' | 'solid' | 'ghost'; size?: 'sm' | 'md' | 'lg'; className?: string; } const { title = 'Error rate by service', subtitle = 'Per weekday, % of requests', rows = ['auth', 'billing', 'search', 'uploads', 'webhooks'], columns = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], values = [ [0.2, 0.3, 0.2, 0.4, 0.6, 0.1, 0.1], [0.8, 1.2, 0.9, 1.6, 2.4, 0.4, 0.3], [0.4, 0.5, 0.7, 0.6, 0.9, 0.3, 0.2], [1.1, 1.4, 1.9, 2.8, 3.2, 0.9, 0.6], [0.3, 0.2, 0.4, 0.5, 0.4, 0.2, 0.1], ], showValues = true, suffix = '%', color = 'var(--chart-1, #3987e5)', variant = 'glass', size = 'md', className = '', } = Astro.props; const flat = values.flat(); const min = Math.min(...flat); const max = Math.max(...flat); const t = (v: number) => (max === min ? 1 : (v - min) / (max - min)); const fill = (v: number) => `color-mix(in srgb, ${color} ${Math.round(12 + t(v) * 88)}%, #141416)`; --- <ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className} table={{ headers: ['', ...columns], rows: rows.map((r, ri) => [r, ...values[ri].map((v) => `${v}${suffix}`)]) }} > <div class="hm" style={`--hm-cols:${columns.length}`}> <div class="hm__grid" role="grid" aria-label={title}> <div class="hm__row" role="row"> <span class="hm__corner" role="columnheader"></span> {columns.map((c) => <span class="hm__col" role="columnheader">{c}</span>)} </div> {rows.map((r, ri) => ( <div class="hm__row" role="row"> <span class="hm__rowlabel" role="rowheader">{r}</span> {columns.map((c, ci) => { const v = values[ri]?.[ci] ?? 0; return ( <span class="hm__cell cf-mark" role="gridcell" tabindex="0" style={`background:${fill(v)}`} aria-label={`${r}, ${c}: ${v}${suffix}`} data-tip-title={`${r} · ${c}`} data-tip={`${v}${suffix}`}> {showValues && <span class={t(v) > 0.55 ? 'hm__v hm__v--strong' : 'hm__v'}>{v}</span>} </span> ); })} </div> ))} </div> <div class="hm__scale" aria-hidden="true"> <span>{min}{suffix}</span> <span class="hm__ramp" style={`background:linear-gradient(90deg, ${fill(min)}, ${fill(max)})`}></span> <span>{max}{suffix}</span> </div> </div> </ChartFrame> <style> .hm { overflow-x: auto; } .hm__grid { display: grid; gap: 3px; min-width: calc(5rem + var(--hm-cols) * 2.4rem); } .hm__row { display: grid; grid-template-columns: 5rem repeat(var(--hm-cols), minmax(2.2rem, 1fr)); gap: 3px; } .hm__col { text-align: center; font-size: 0.6875rem; color: var(--c-text-3, #8a8a8a); padding-bottom: 0.2rem; } .hm__rowlabel { display: flex; align-items: center; font-size: 0.75rem; color: var(--c-text-2, #a3a3a3); font-family: var(--font-mono, monospace); } .hm__cell { display: grid; place-items: center; min-height: 34px; border-radius: 4px; transition: transform 0.15s ease; } .hm__cell:hover { transform: scale(1.06); } .hm__cell:focus-visible { outline: 2px solid var(--c-text-1, #fff); outline-offset: 1px; } .hm__v { font-size: 0.6875rem; color: rgb(var(--ink-rgb, 255 255 255) / 0.7); font-variant-numeric: tabular-nums; } .hm__v--strong { color: #fff; font-weight: 650; } .hm__scale { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.9rem; font-size: 0.6875rem; color: var(--c-text-3, #8a8a8a); } .hm__ramp { flex: 0 1 160px; height: 8px; border-radius: 4px; } </style>

Quick Info

Category
Charts & Stats
Filename
HeatmapChart.astro
Dependencies
apexcharts (peer)
Tags
chartsvgzero-dependencyheatmapmatrixsequentialgrid