Open41

Makieなんもわからん.jl

hnakano863hnakano863

公式サイトのトップページを見ると「つべこべ言わずにTutorialをやってExampleをやれと言っているように見えるので、Basic Tutorialから読む。

hnakano863hnakano863

バックエンドごとに、CairoMakie.jlGLMakie.jlWGLMakieに分かれている。
CairoMakieは非インタラクティブなプロットしかできない。一方で、GLMakieWGLMakieでならインタラクティブなプロットを作れるらしい。GLMakieは別のウィンドウを立ち上げるっぽい?対してWGLMakieはhtmlをレンダリングできる環境に直接グラフを描画してくれるっぽい?Plutoでグリグリ動かせるグラフがほしければWGLMakieが良さそう。

hnakano863hnakano863

using CairoMakieでパッケージを使う。

折れ線グラフはlinesで散布図はscatter

begin 
    x = range(0, 10, length = 100)
    y = sin.(x)
end
# 折れ線グラフ
lines(x, y)
# 散布図
scatter(x, y)

この辺はpyplotとあまり変わらなさそう。

hnakano863hnakano863

scatterlinesのような関数をMakieのドキュメントではplotting functionというっぽい。

Makieのplotting functionは全部!がつくバージョンがあるらしい。!がない方は新しくグラフを描画するのに対して、!がある方はすでに存在するグラフを書き換える。Plots.jlplotplot!の関係に似てる気がする。

y2 = cos.(x)
begin
    # 1本目
    lines(x, y)

    # 2本目
    lines!(x, y2)

    # グラフの表示
    current_figure()
end
hnakano863hnakano863

自動でlegendを表示してくれるような優しさはなさそう

hnakano863hnakano863

linesの返り値の型を確認してみたところ、FigureAxisPlotだったが、lines!の返り値の型はLines{Tuple{Vector{Point{2, Float32}}}}だった。グラフとして描画されるのはFigure, FigureAxisPlot, Sceneなどといった型だけらしいので、lines!のあとにグラフを描画するにはcurrent_figure()が必要。ちなみにcurrent_figureの返り値の型はFigureだった。

hnakano863hnakano863

どのplotting functionも、キーワード引数で指定できる属性がある。
plotting function によって使えるキーワード引数は異なる。

lines(x, y, color = :red)
# `markersize`は`lines`にはない
scatter(x, y, color = :red, markersize = 5)

scatter!lines!などのようにPlotオブジェクトを返す関数の場合、plot.attr = valのようにして後から属性の値を変えることができる。

let
    lines(x, y, color = :red)
    l = lines!(x, y2, color = :blue)
    l.color = :green
    current_figure()
end
hnakano863hnakano863

ところで、どこを見ればプロットオブジェクトの属性を調べることができるのだろう?API referenceには書かれていないようなので困る。

hnakano863hnakano863

colormarkersizeなどの一部の属性は配列で渡せる。

scatter(x, y,
    markersize = range(5, 15, length = 100),
    color = repeat([:crimson, :dodgeblue, :slateblue1, :sienna1, :orchid1], 20),
)

colormapと組み合わせたときは、colorの値は0から1までの実数をとれる

lines(x, y, color = range(0, 1, length = 100), colormap = :thermal)

ちなみに配列で属性を指定するときは、データ点の数と属性配列の要素数が一致しないとAssertionErrorが出る。

hnakano863hnakano863

:red:blueのような名前つきの色は、Colors.jlで定義されている。したがって、使える色が知りたいときは、Colors.jlのドキュメントのNamed Colorsのページを参照するとよい。

colormapに指定できる値とそのときどんな色になるかの一覧はMakieの公式サイトのColormapsの項で説明されている。

hnakano863hnakano863

疑問点

  • colormapがカテゴリカルな場合でも、colorは0から1でないといけないのか?
  • colormapのほとんどがColorScheme.jlで定義されているように見えるけど、逆にMakieのドキュメントに載っていないcolormapでもColorSchemes.jlで定義されていれば使えるのか?
hnakano863hnakano863

そもそもcolorの引数は数値の配列か色に変換できるデータでないといけない。
colormaptab10などのようなカテゴリカルなものでもそれは同じ

colorの配列は0から1でなくてもよくて、最大値と最小値がcolormapの端点に自動的にマッピングされる。マッピングの対応関係を変更するには、colorrangeを指定する。

hnakano863hnakano863

Makieのドキュメントに載っていないcolormapも使えた。そういうわけで、colormapはColorSchemes.jlのドキュメントのCatalogue of ColorSchemesのページを参照したほうが良さそう

hnakano863hnakano863

label属性を指定すれば、axislegend関数を使ってlegendを表示できる

begin
    lines(x, y, label = "sin")
    lines!(x, y2, label = "cos")
    axislegend()
    current_figure()
end

axislegend()の返り値はLegend型なので、current_figure()が必要

hnakano863hnakano863

subplotを作るにはFigureオブジェクトを作る必要がある。

Figureはいわゆるキャンバスみたいなもので、多分matplotlibでのpyplot.figureにあたるんだろうなぁと思う。

fig = Figure()とすると、plot_func(fig[i,j], x, y)figij列にsubplotを描画できる。

let
    fig = Figure()
    lines(fig[1, 1], x, y)
    lines(fig[1,2], x, y)
    lines(fig[2,1:2], x, y)
    current_figure()
end

fig[1, 1:2]みたいにインデックスにrangeが使えるのがとても便利。

ちなみにFigure()Figure型、fig[i,j]GridPosition型、lines(fig[i,j],x,y)AxisPlot型になる。

hnakano863hnakano863

それぞれのsubplotが描画される領域のことをAxisと呼んでいるっぽい。これもちょっとmatplotlibと似ててわかりやすい。

Axis(fig[i,j])とするとFigureij列にAxisを追加できるみたい。こうして作ったAxisにプロットを追加するにはplot_func!(ax, x, y)とする。!がつくことに注意。

let
    fig = Figure()
    ax = Axis(fig[1,1])
    lines!(ax, x, y)
    fig
end

Axis()の返り値はAxisだし、lines!(ax, x, y)の返り値はlines!(x, y)の返り値と変わらないっぽい。

hnakano863hnakano863

lines(x, y)だけでなく、lines(x, func)でもプロットが描画できる。
xにはIntervalSets.jlの区間を使うこともできる。

begin
    lines(x, y)
    lines!(x, cos)
    lines!(0..10, sqrt)
    current_figure()
end
hnakano863hnakano863

AxisLegendLayoutableの具体型らしい。Layoutableなオブジェクトは基本的にFigure上の任意の位置に配置することができる。

supertype(Legend) == supertype(Axis) == Makie.MakieLayout.Layoutable # => true

したがって、LegendAxisと同様に作成して配置することができる。

let
    fig = Figure()

    ax1 = Axis(fig[1,1])
    l1 = lines!(ax1, 0..10, sin)

    ax2 = Axis(fig[2,1])
    l2 = lines!(ax2, 0..10, cos)

    Legend(fig[1:2,2], [l1,l2], ["sin", "cos"])
    fig
end
hnakano863hnakano863

ColorbarLayoutableの一つなので、Axisと同じようにFigure上に配置できる。

let
    fig = Figure()
    ax = Axis(fig[1,1])
    hm = heatmap!(ax, randn(20,20))
    Colorbar(fig[1,2], hm)
    fig
end
hnakano863hnakano863

FigureAxisPlotAxisPlotにはiterateメソッドが実装されているので、destructできる。

FigureAxisPlotFigure, Axis, Plotオブジェクトの3つにdestructできる。

let
    # heatmapの返り値はFigureAxisPlot
    fig, _, hm = heatmap(randn(20, 20), colormap = :thermal)
    Colorbar(fig[1,2], hm)
    fig
end

AxisPlotAxisPlotにdestructできる。

let
    fig = Figure()
    # linesの返り値はAxisPlot
    _, l1 = lines(fig[1,1], 0..10, sin, color = :red)
    ax, l2 = lines(fig[2,1], 0..10, cos, color = :blue)
    l3 = lines!(ax, 0..10, sqrt, color = :green)
    Legend(fig[1:2, 2], [l1, l2, l3], ["sin", "cos", "sqrt"])
    fig
end
hnakano863hnakano863

しかし、Figureのキーワード引数については、内部のデータ構造に次々に渡されていくため、どこか1箇所にまとまっているということはなく、結果的に何を指定できるかは全くわからない。バージョンアップを待とう。

hnakano863hnakano863

FigureLayoutableの属性はこんな感じで指定できる。

let
    fig = Figure(backgroundcolor = :ivory1)
    ax = Axis(fig[1,1], aspect = 1, xlabel = "x axis", ylabel = "y axis")
    hm = heatmap!(ax, randn(20, 20))
    Colorbar(fig[1,2], hm, label = "colorbar")
    fig
end

plotfuncAxisPlotを返す場合は、キーワード引数にaxisが含まれる。FigureAxisPlotのときはfigureも含まれる。

let
    fig, _, hm = heatmap(randn(20, 20);
        figure = (backgroundcolor = :ivory1,),
        axis = (aspect = 1, xlabel = "x axis", ylabel = "y axis"))
    Colorbar(fig[1,2], hm, label = "colorbar")
    fig
end

figureaxisの引数はNamedTupleであることに注意。特に、属性が1つだけのときは、(backgroundcolor = :ivory1,)のように、後ろにカンマを入れるか、(; backgroundcolor = :ivory1)のように、頭にセミコロンを入れるかしないといけない。

hnakano863hnakano863

Layout Tutorialの主な目的は、以下のグラフの作成を通じて、Makieで複雑なレイアウトのグラフを作成する方法を習得することにありそう。

hnakano863hnakano863

まずは、全部のグラフを配置するおおもとのFigureを作成する

using CairoMakie

# font
noto_sans = assetpath("fonts", "NotoSans-Regular.ttf")
noto_sans_bold = assetpath("fonts", "NotoSans-Bold.ttf")

f = Figure(;
    backgroundcolor = RGBf(.98, .98, .98),
    resolution = (1000, 700),
    font = noto_sans,
)

hnakano863hnakano863

RGBfについては大したドキュメントがないけれど、多分ColorTypes.jlRGB{Float32}のエイリアスだと思う。Point2fGeometryBasics.Point{2, Float32}なので、その類推で、MakieでfがつけばFloat32なのだろうと考えられる。

基本的な使い方はRGBf(r,g,b)なのだが、r,g,bはそれぞれ0から1までの実数でなければならない(なおかつ、多分Float32にキャストできないといけない)

引数はマイナスでもエラーにはならないが、0として扱われる。同様に、1を超える値は1として扱われる

hnakano863hnakano863

Figureを4つのパネルに分割する。GridLayoutというのを使うらしい。
この場合は、Figurefの1列目に、ga(上段)とgb(下段)を割り当て、2列目にはgcdを割り当てている。gcgdgcdの中にネストしている。

Tableauでダッシュボード作ったことがあるので、こういうネストしたレイアウトが作れるのは同じ発想が生かせてうれしい。

begin
    ga = f[1,1] = GridLayout()
    gb = f[2,1] = GridLayout()
    gcd = f[1:2, 2] = GridLayout()
    gc = gcd[1,1] = GridLayout()
    gd = gcd[2,1] = GridLayout()
end
hnakano863hnakano863

Panel Aからつくる。Axisを配置してまずはグラフを配置する。

axtop = Axis(ga[1,1])
axmain = Axis(ga[2,1], xlabel = "before", ylabel = "after")
axright = Axis(ga[2,2])
	
labels = ["treatment", "placebo", "control"]
data = randn(3, 100, 2) .+ [1, 3, 5]

for (label, col) in zip(labels, eachslice(data, dims = 1))
	scatter!(axmain, col, label = label)
	density!(axtop, col[:, 1])
	density!(axright, col[:, 2], direction = :y)
end

f

hnakano863hnakano863

dataは3次元の多次元配列で、(3×100×2)の形状になっている。
eachslice(data, dims = 1)によって、行ごとのイテレータができる。このイテレータは、(100×2)の行列をイテレーションごとに返すと考えていい。

hnakano863hnakano863

scatter(mat)の形でプロットするとき、基本的には、matの各 が点であると認識される。例えば、(3×2)の行列だったら、2次元の点が3個ではなく、3次元の点が2個として認識される。

しかし、(n×2)や(n×3)で、nが4以上であれば、自動的に各 が点として認識されるように切り替わる。したがって、本来はscatter!(axmain, col')と書く必要があるところを、colが100行あるので、scatter(axmain, col)のように書くことができる。

hnakano863hnakano863

density, density!はカーネル密度推定をしたプロットをつくる。

えっ、外部パッケージなしでカーネル密度推定を!?
できらぁ!!

実際、Plots.jlではStatsPlotsが必要だし、matplotlibではseabornが必要になるカーネル密度推定を、Makieなら単一パッケージでできるのでチョットだけうれしい。