CSSだけで、data属性の数値に基づいたヒートマップを実装する方法

# はじめに
Google Chromeのバージョン133以降、cssの`attr()`関数で出来ることが一気に増えました。

https://developer.chrome.com/blog/advanced-attr?hl=ja

リリースノートを読んで、「cssだけでヒートマップが実装できるんじゃないか？」と思って試してみました。

# 作ったもの
data属性の数値に応じてサーモグラフィー風に色がつくヒートマップ。

![スクリーンショット 2025-04-06 18.03.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2885419/8940fb1c-9671-4793-b440-98fd766b3a33.png)

# 実装
:::note warn
2025年4月現在、Google Chrome133以降のブラウザのみで正しく表示されます。
:::

```heatmap.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8" />
        <title>css heatmap</title>
        <link rel="stylesheet" href="./heatmap.css" type="text/css" />
    </head>
    <body>
        <div class="grid" id="grid"></div>
        <script>
            const grid = document.getElementById("grid");
            for (let i = 0; i < 25; i++) {
                const cell = document.createElement("div");
                cell.classList.add("cell");
                cell.setAttribute("data-val", i + 1);
                grid.appendChild(cell);
            }
        </script>
    </body>
</html>
```

```heatmap.css
.grid {
    display: grid;
    grid-template-columns: repeat(25, 25px);
    grid-template-rows: repeat(1, 75px);
    padding: 20px;
}

.cell {
    --max: 25;
    --min: 1;
    --val: attr(data-val type(<number>));
    --range: calc(var(--max) - var(--min));
    --actual-val: calc(var(--val) - var(--min));
    --normalized-val: calc(var(--actual-val) / var(--range));

    --r: calc(4 * 255 * var(--normalized-val) - 510);
    --g: calc(
        255 - 4 * 255 * (var(--normalized-val) - 0.5) *
            (var(--normalized-val) - 0.5)
    );
    --b: calc(510 - 4 * 255 * var(--normalized-val));

    background-color: rgb(var(--r), var(--g), var(--b));
}
```

# 解説
### 1. 最大値、最小値の設定
```
    --max: 25;
    --min: 1;
```
CSSカスタムプロパティ（変数）を使ってヒートマップで表現する最大値と最小値を設定します。
扱うデータに応じて設定してください。

### 2. data属性から値を取得

```
    --val: attr(data-val type(<number>));
```
`attr()`関数を利用して`data-val`属性の値を取得します。
ヒートマップの色の指定には`rgb()`を使っていて、`rgb()`の各パラメーターは`<number>`として指定するため、取得時に型を指定しておきます。**この型指定が無いと正しく表示できません。**

### 3. 取得した値を扱いやすいように正規化

```
    --range: calc(var(--max) - var(--min));
    --actual-val: calc(var(--val) - var(--min));
    --normalized-val: calc(var(--actual-val) / var(--range));
```
データがとる範囲`--range`と、取得したデータとデータが取りうる最小値の差`--actual-val`を用いてデータを正規化します。
正規化された値`--normalized-val`は、`attr()`で取得した値を0(最小値)~1(最大値)の間に収まるよう変換した値なので、今後の計算が楽になります。

### 4. 赤色の強さを計算

```
    --r: calc(4 * 255 * var(--normalized-val) - 510);
```
光の三原色である、赤・緑・青のうち赤色をどの程度混ぜるかを計算します。

`rgb()`のパラメーターとして、**0より小さい値を設定した場合は0として扱われ、255より大きい値を渡した場合は255として扱われる**ことを利用して下記のように設定しています。

|正規化された値|Redの設定値|
|-|-|
|0~0.5|0固定|
|0.5~0.75|0から255まで正規化された値に比例して増やす|
|0.75~1|255固定|

データを最小値から最大値まで変化させて、赤色だけを表示すると下記の画像のようになります。

![スクリーンショット 2025-04-06 19.24.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2885419/da563402-fb1f-4ad1-a89c-4dec744d2278.png)

### 5. 緑色の強さを計算
```
    --g: calc(
        255 - 4 * 255 * (var(--normalized-val) - 0.5) *
            (var(--normalized-val) - 0.5)
    );
```
緑色は、**値が最小の時と最大の時に弱く、値が中くらいの時に強く表示したい**です。
そのため、二次関数を利用して実装しました。
データを最小値から最大値まで変化させて、緑色だけを表示すると下記の画像のようになります。

![スクリーンショット 2025-04-06 19.48.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2885419/3bd6b815-e818-42a9-9672-537694143e3a.png)

### 6. 青色の強さを計算
```
    --b: calc(510 - 4 * 255 * var(--normalized-val));
```
赤色の計算の符号を逆にした版です。
データを最小値から最大値まで変化させて、青色だけを表示すると下記の画像のようになります。

![スクリーンショット 2025-04-06 19.48.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2885419/856ac842-e093-4c40-af33-2e661ca9d626.png)

### 7. 計算した光の三原色の設定値を背景色に設定
```
    background-color: rgb(var(--r), var(--g), var(--b));
```
完成！
![スクリーンショット 2025-04-06 18.03.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2885419/8940fb1c-9671-4793-b440-98fd766b3a33.png)

# おわりに
2025年4月現在、Google Chrome以外のブラウザでは使用できないのであまり実用的ではないですが、cssでできる計算のみでうまく実装する方法を考えるのは楽しかったです。