gnuplotの等高線でヒートマップを描きたい

二次元データを可視化する方法に、標高線を描くという方法があります。

f:id:lpha_z:20200823223749p:plain
図1: 標高線を使った可視化

また、ヒートマップを使う方法もあります。

f:id:lpha_z:20200823223248p:plain
図2: ヒートマップを使った可視化

標高線通りにヒートマップを作ってほしいのですが、gnuplotが標高線を描画するアルゴリズムとヒートマップを作る時の補間アルゴリズムは異なっているため、ずれが発生します。

f:id:lpha_z:20200823224526p:plain
図3: 標高線とヒートマップを同時に使った可視化

これを一貫するようにしてみたいと思います。

戦略

標高線は、図1や図3だと直線で補間していますが、スプライン曲線で補間することもできます。 しかし、スプライン曲線で補間しても、ヒートマップとはずれが生じます。

そこで、gnuplotに標高線データをsvgで出力してもらって、それをスクリプトで加工することを考えます。

実装

まずはこんな感じでgnuplotに標高線データをsvgで出力してもらって、

gnuplot -e "set terminal svg; set output 'contour.svg'; set view map; unset surface; set contour base; set xrange [0:4]; set yrange [0:4]; set xtics 1; set ytics 1; set ticscale 0; set cntrparam bspline; set cntrparam levels incremental 0,2,16; splot 'data' u 1:2:3 w l notitle"

次に以下のRubyスクリプトで加工します。

require 'rexml/document'

palette = ["rgb(96,1,199)", "rgb(136,6,249)", "rgb(167,20,111)", "rgb(193,48,0)", "rgb(216,93,0)", "rgb(236,161,0)", "rgb(255,255,0)"]

xml = REXML::Document.new(File.new("contour.svg"))

contour = xml.root.get_elements('g/g/g/path').map { |e| e.attribute("d") }.reverse

xml.root.elements.each_with_index('g/g/g/path') { |e, i|
    e.add_attribute("d", contour[i])
    e.add_attribute("fill", palette[i])
    e.add_attribute("stroke", palette[i])
}

File.write("contour.svg", xml)

reverseでひっくり返しているのは、描画順を入れ替えるためです。 gnuplotは標高線データを値の大きいほうから順番に出力してきます。

この方法でうまくいくためには、少なくとも以下の条件が要求されます。

  • 等高線に谷のような部分がなく山だけがある(複数の頂上があるのは大丈夫)
    • 標高の低い部分の塗りつぶしから順にキャンバスに書くことで、間が塗りつぶされているように見せているため
  • 一周しない等高線の始点と終点は同じ辺に属している
    • もともと線だったものを面に変換しているので、一周しない線の場合は始点と終点が結ばれる
    • 標高線が切れる部分が同じ辺にある場合は始点と終点が結ばれて問題なくなる
    • 標高線が切れる部分が違う辺にある場合は斜めに結ばれて正しくなくなる

結果

f:id:lpha_z:20200823231713p:plain
図4: 標高線と一貫したヒートマップによる可視化

作図に用いたコマンド

元データ

0 0 1
0 1 7
0 2 7
0 3 7
0 4 1

1 0 1
1 1 15
1 2 13
1 3 15
1 4 1

2 0 1
2 1 3
2 2 11
2 3 11
2 4 1

3 0 1
3 1 3
3 2 11
3 3 3
3 4 1

4 0 1
4 1 1
4 2 1
4 3 1
4 4 1

使ったgnuplotコマンド

図1

gnuplot -e "set terminal png; set output 'contour.png'; set view map; unset surface; set xrange [0:4]; set yrange [0:4]; set xtics 1; set ytics 1; set ticscale 0; set pm3d at b; set pm3d interpolate 100,100; set cbrange [0:16]; set palette maxcolors 8; splot 'data' u 1:2:3 w l notitle"

図2

gnuplot -e "set terminal png; set output 'contour.png'; set view map; set contour base; unset surface; set xrange [0:4]; set yrange [0:4]; set xtics 1; set ytics 1; set ticscale 0; set cntrparam linear; set cntrparam levels incremental 0,2,16; splot 'data' u 1:2:3 w l notitle"

図3

gnuplot -e "set terminal png; set output 'contour.png'; set view map; set contour base; set xrange [0:4]; set yrange [0:4]; set xtics 1; set ytics 1; set ticscale 0; set cntrparam linear; set cntrparam levels incremental 0,2,16; set pm3d at b; set pm3d interpolate 100,100; set cbrange [0:16]; set palette maxcolors 8; splot 'data' u 1:2:3 w l notitle"