JavaScriptでカラーバーを描画する [d3.js]

# はじめに

何の需要があるかわかりませんが, JavaScriptでカラーバー(だけ)を描画したかったので, 色々模索したときのログを残しておきます.

# 作れるもの

こんな感じです.

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264254/dd23c06d-5432-d901-81b6-ed421c94f86a.png)


# d3.js と d3fc を使う

d3.jsとd3fcを利用しました. ちなみにバージョンはそれぞれd3.js(v5), d3fc(15.0.8)でした.
それぞれローカルにダウンロードするか, CDNとかでリンク貼ってください. そこら辺の説明は割愛します.

## とりあえず作る

### スクリプトを書く

とりあえず, テキトーにhtmlとcssを書いちゃいます.
今回は`#colorbar-container`にカラーバーを入れていきましょう.
CSSは左下にカラーバー置きたいな〜ってだけなので, 無くてもいいです.

```html:index.html
<!DOCTYPE html>
<head>
</head>
<body>
    <div id="colorbar-container"></div>
</body>
<style>

html, body {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
}

#colorbar-container {
    position: absolute;
    bottom: 3rem;
    left: 2vw;
    
    background-color: rgba(0, 0, 0, 0.1);
    
    padding: 2vh 0 2vh 2vw;
    border-radius: 2rem;
}

</style>
<script>
let min=-100, max=100;
func = function (x) {
    return d3.interpolateViridis(norm(x, min, max))
};
makeColorbar(min, max, func);
</script>
```

JSの方を書いていきましょう.
htmlの一番下に記述している`norm`と`makeColorbar`の中身を書いていきます.
リンクをはるか, htmlに追記してください.

```javascript:norm.js
// 値を正規化(0~1の範囲に直す)
function norm(x, min, max) {
    if (x > max) return 1;
    if (x < min) return 0;
    return (x - min) / (max - min);
};
```

```javascript:makeColorbar.js
// カラーバーの作成&配置
// minValue : カラーバーの下限値
// maxValue : カラーバーの上限値
// interpolattionFunc : 入力値をRGBの値に変換する関数
function makeColorbar(minValue, maxValue, interpolationFunc) {
    const container = "#colorbar-container";
    const addId = "colorbar-svg";
    const domain = [minValue, maxValue]; // range for colorbar

    const height = 500;
    const width = 200;

    const viewWidth = 130;
    const viewHeight = height;

    // pad colorbar
    const paddedDomain = fc.extentLinear()
        .pad([0.1, 0.1])
        .padUnit("percent")(domain);
    const [min, max] = paddedDomain;
    const expandedDomain = d3.range(min, max, (max - min) / height);

    // Band scale for x-axis
    const xScale = d3
        .scaleBand()
        .domain([0, 1])
        .range([0, width]);

    // Linear scale for y-axis
    const yScale = d3
        .scaleLinear()
        .domain(paddedDomain)
        .range([height, 0]);

    const svgBar = fc
        .autoBandwidth(fc.seriesSvgBar())
        .xScale(xScale)
        .yScale(yScale)
        .crossValue(0)
        .baseValue((_, i) => (i > 0 ? expandedDomain[i - 1] : 0))
        .mainValue(d => d)
        .decorate(selection => {
            selection.selectAll("path").style("fill", d => interpolationFunc(d, ...domain));
        });

    // Drawing the legend bar
    const legendSvg = d3.select(container).append("svg")
        .attr("height", viewHeight)
        .attr("width", viewWidth)
        .attr("viewBox", `0, 0, ${viewWidth}, ${viewHeight}`)
        .attr("id", addId);
    const legendBar = legendSvg
        .append("g")
        .datum(expandedDomain)
        .call(svgBar);

    tickValues = [...domain,
    domain[0] + (domain[1] - domain[0]) / 5,
    domain[0] + 2 * (domain[1] - domain[0]) / 5,
    domain[0] + 3 * (domain[1] - domain[0]) / 5,
    domain[0] + 4 * (domain[1] - domain[0]) / 5]

    // Removing the outer ticks
    const axisLabel = fc
        .axisRight(yScale)
        .tickValues(tickValues)
        .tickSizeOuter(0)
        .decorate((s) =>
            s.enter()
                .select("text")
                .style("font-size", "20"));

    // Drawing and translating the label
    const barWidth = Math.abs(legendBar.node().getBBox().x);
    legendSvg.append("g")
        .attr("transform", `translate(${barWidth})`)
        .datum(expandedDomain)
        .call(axisLabel);

    // Hiding the vertical line
    legendSvg.append("g")
        .attr("transform", `translate(${barWidth})`)
        .datum(expandedDomain)
        .call(axisLabel)
        .select(".domain")
        .attr("visibility", "hidden");
};
```

### 表示例

CSSで左下の方に追いやってるので, 以下のような感じになります.
また, テーマは`Viridis`を利用してます.

![cbar_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264254/1510c27d-0766-f3e7-cb6f-b7da3b71c96f.png)


### 簡単な説明

まず, html内の`<script>`タグ内ですが, 値→RGB値の変換関数を作成して, `makeColorbar`に渡しています.
d3の`d3.interpolate~~系`を用いると, 0〜1の範囲の値をRGBへ変換することができます.
今回はテーマに`viridis`を用いてるので, `d3.interpolateViridis`を用いて, RGBへと変換しています.

![interpolateVirids.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264254/bc669987-d786-419a-760d-ddba3f3febab.png)

ここで, 例えば-70〜200のレンジでカラーバーを作成したい場合, この値を0〜1の範囲に直す必要がでてくるので, 今回は`norm`という関数を用意して, 正規化を行いました.

肝心のカラーバー作製を行っている関数`makeColorbar`ですが, [こちら](https://blog.scottlogic.com/2019/03/13/how-to-create-a-continuous-colour-range-legend-using-d3-and-d3fc.html)を参考に作成しました.
冒頭の変数をいじれば大きさとか入れる場所などを指定できるので, 変更する場合はそこら辺を直してみてください.

## カラーバーのテーマを変更する/カラーバーの色の向きを反転する

### スクリプトを書く

まず, htmlの下部の`<script>`タグ内部を以下のように変更します.

```html:index.html
<script>
let min=-100, max=100;
func = getInterpolationFunc(min, max, "red-blue", false);
makeColorbar(min, max, func);
</script>
```

JSの方を書いていきます.
htmlの追記した`getInterpolationFunc`の中身を書いていきます.
こちらもリンクをはるか, htmlに追記してください.

```javascript:getInterpolationFunc.js
// テーマ名を入れると対応するテーマにおけるのRGB変換関数を返す
// minValue : カラーバーの下限値
// maxValue : カラーバーの上限値
// interpolateTheme : 利用したいテーマの名前を入れる
// reverseHeatmap : カラーバーの色の向きを逆向きにする (trueで逆向き)
function getInterpolationFunc(minValue, maxValue, interpolateTheme, reverseHeatmap) {
    let f = function () { };
    switch (interpolateTheme) {
        case "red-blue":
            f = d3.interpolateRdBu;
            break;

        case "red":
            f = d3.interpolateReds;
            break;

        case "blue":
            f = d3.interpolateBlues;
            break;

        case "spectral":
            f = d3.interpolateSpectral;
            break;

        case "viridis":
            f = d3.interpolateViridis;
            break;

        case "inferno":
            f = d3.interpolateInferno;
            break;

        default:
            f = d3.interpolateSpectral;
    }

    if (reverseHeatmap) {
        return function (x) { return f(1 - norm(x, minValue, maxValue)) };
    } else {
        return function (x) { return f(norm(x, minValue, maxValue)) };
    }
};
```

### 簡単な説明

`getInterpolationFunc`では, 色変換の関数を取ってきます.
色変換の関数はd3にいくつか用意されているので, それらに対してswitch文で対応しているだけです.
ちなみにd3のカラーバーのテーマは[こちら](https://wizardace.com/d3v5-scale-chromatic/)に見本と一緒に載ってます.
他に好きなカラーテーマがあれば, switch文に追加すれば大丈夫です.

`getInterpolationFunc`の下の方で, 色方向の反転処理を書いてます. `0~1`の範囲のものを`1~0`にしてあげればいいだけです.

(jetはこう)

## 動的にする

### カラーバーの各パラメータを動的に変化させる

上限値/下限値, テーマ, 色の方向を動的に変化できるようにします.
JQueryを使って動的に描画していきます. JQueryもCDNなりなんなりでいれます.

値の入力とそれを受け取ることができるようにするため, htmlを以下のように変更します.
ちなみにフォームの部分はだいぶ適当です.


```html:index.html
<!DOCTYPE html>
<head>
</head>
<body>
    <div id="colorbar-container"></div>
    <div id="input-container">
        <input type="number" value="-100" id="min-input" />
        <input type="number" value="100" id="max-input" />
        <select class="have-ul" id="theme-select">
            <option value="red-blue" selected>Red → Blue</option>
            <option value="red">Red</option>
            <option value="blue">Blue</option>
            <option value="spectral">spectral</option>
            <option value="viridis">viridis</option>
            <option value="inferno">inferno</option>
        </select>
        <input type="checkbox" id="reverse-input"/>
        <input type="button" id="decide-button" />
    </div>
</body>
<style>

html, body {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
}


#colorbar-container {
    position: absolute;
    bottom: 3rem;
    left: 2vw;
    
    background-color: rgba(0, 0, 0, 0.1);
    
    padding: 2vh 0 2vh 2vw;
    border-radius: 2rem;
}


#input-container {
    position: absolute;
    top: 3rem;
    right: 2vw;
    
    background-color: rgba(0, 0, 0, 0.1);
}

#input-container input,
#input-container select {
    display: block;
    width: 100%;
}

</style>
<script>
$(function(){
    $("#decide-button").on("click", function() {
        
        // 各値の取得
        const min = parseInt($("#min-input").val());
        const max = parseInt($("#max-input").val());
        const theme = $("#theme-select").val();
        const reverse = $("#reverse-input").prop("checked");

        // カラーバーを消す
        $("#colorbar-container").empty();

        func = getInterpolationFunc(min, max, theme, reverse);
        makeColorbar(min, max, func);
    });
});
</script>
```

### 動作確認

こんな感じになります.

![cbar.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264254/15c92ea3-8602-f876-da70-1cc6eec54984.gif)


### 大きさの変更

このままだと, ウィンドウサイズを変えたときに大きさが不変なので, そこら辺を直していきましょう.
`makeColorbar.js`の関数に少し付け足していきます.

```javascript:makeColorbar.js
// カラーバーの作成&配置
// minValue : カラーバーの下限値
// maxValue : カラーバーの上限値
// interpolattionFunc : 入力値をRGBの値に変換する関数
function makeColorbar(minValue, maxValue, interpolationFunc) {
    const container = "#colorbar-container";
    const addId = "colorbar-svg";
    const domain = [minValue, maxValue]; // range for colorbar
    
    // 高さを動的に変化させる 今回はウィドウサイズの38%の高さにする.
    const height = Math.floor(38 * $(window).height() / 100);
    const width = 200;

    const viewWidth = 130;
    const viewHeight = height;

    // ~~ 中略 ~~ //
    
    // Resizing colorbar when window size is resized
    $(window).off('resize');
    $(window).on('resize', function () {
        legendSvg.attr("height", Math.floor(38 * $(window).height() / 100));
    });
};
```

# 余談 : Jetについて

d3にJetのカラーテーマのがないっぽいので, 自作してみました.
以下のように`interpolateJet`関数を用意して, `makeColorbar(-100, 100, interpolateJet)`のように渡してあげれば, 描画できます. (もしくは`getInterpolationFunc`のswitch文に追加してあげてください)

```javascript:interpolateJet.js
//The steps in the jet colorscale
const jet_data_lin = [
    [0,0,0.5],
    [0,0,1],
    [0,0.5,1],
    [0,1,1],
    [0.5,1,0.5],
    [1,1,0],
    [1,0.5,0],
    [1,0,0],
    [0.5,0,0]
]

const jet_rgb = jet_data_lin.map(x => {
    return d3.rgb.apply(null, x.map(y=>y*255))
})

jetInterpList = new Array(jet_rgb.length-1);
for (let i=0;i<jet_rgb.length-1;i++) {
    jetInterpList[i] = d3.interpolateRgb(jet_rgb[i], jet_rgb[i+1])
}

function interpolateJet(pow) {
    if (!(0 <= pow && pow <= 1)) return "rgb(0, 0, 0)";

    const n = jet_rgb.length-1;

    var i = Math.max(0, Math.min(n - 1, Math.floor(pow *= n)));
    return jetInterpList[i](pow - i);
};
```

こんな感じになります.

![jet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264254/ef966ba5-1868-8601-4244-149f44826517.png)



# 参考

[How to create a continuous colour range legend using D3 and d3fc](https://blog.scottlogic.com/2019/03/13/how-to-create-a-continuous-colour-range-legend-using-d3-and-d3fc.html)