Julia & graphic enthusiast. Interested in high performance computing, GPUs and machine learning.

Makie

About Me

Simon Danisch

  • studied Cognitive Science
  • worked with the MIT Julia-Lab
  • Now Julia expert at Nextjournal

Overview

  • How does Makie work
  • Recipes & Extensions
  • Backends
  • Makie's interactivity
  • State of Makie

Thanks to:

Makie, It's all primitives

Makie currently just a combination of ~8 primitives:

Could be cut down to:

hbox(vbox(c, f), vbox(a, e))

Mesh Primitive:

Line Primitive:

What's missing

  • refactor to make it easy to cut down primitives
  • make it easier to overload specific plots for optimizations
  • overload for better support (e.g. text in PDF/SVG)

Backends

  • CairoMakie (Print, SVG/PDF, 70% features)
  • WGLMakie (Web, fast drawing, 80% features)
  • GLMakie (Desktop, high performance, 100% features)
using CairoMakie
CairoMakie.activate!(type = "png")
line_primitives

What's missing

CairoMakie:

  • better text support
  • interactivity
  • pdf backend
  • better 3d mesh support (2d mesh works fairly well)

WGLMakie:

  • performance
  • proper line shader
  • resizing buffers and small other things don't update yet
  • picking

Scene Graph: putting things together

  • every scene has a transformation (scale, rotation, translation)
  • every child applies the parents transformation to its own transformation
WGLMakie.activate!()
earth_col = load("earth.png")
moon_col = load("moon.jpg")
universe = Scene(
  limits = FRect3D(Vec3f0(-5), Vec3f0(10)), show_axis = false,
  resolution = (650, 300), center = false
)
sun = mesh!(universe, Sphere(Point3f0(0), 0.5f0), color = :yellow)
earth = mesh!(Scene(sun), Sphere(Point3f0(0), 1.5f0), color = earth_col)
moon = mesh!(Scene(earth), Sphere(Point3f0(0), 0.5f0), color = moon_col)
translate!(earth, 0.0, 6.0, 0.0)
rotate!(moon, Vec3f0(0, 0, 1), 0.5pi)
translate!(moon, 0f0, 3f0, 0f0)
update_cam!(universe, Vec3f0(-15, 0, 10), Vec3f0(0.0))
universe
for i in LinRange(0, 2pi, 360)
    rotate!(sun, Vec3f0(0, 0, 1), i)
    rotate!(earth, Vec3f0(0, 0, 1), i)
    WGLMakie.redraw!(getscreen(universe)) # TODO make it update on itself
    sleep(0.01)
end

Layouting

Windows are FREEE :)

set_theme!(resolution = (600, 300))
p = Scene()
a = scatter!(Scene(p, Rect(0, 0, 300, 300)), rand(4))
b = scatter!(Scene(p, Rect(300, 0, 300, 300)), rand(4), color = :red)
p
resize!(a, Rect(60, 50, 100, 100)); update!(a);

Vbox & Hbox

using MakieGallery
using AbstractPlotting: hbox, vbox
set_theme!(resolution = (650, 500))
p1 = run_example("Tutorial adding to a scene")
p2 = run_example("Contour Function")
p3 = run_example("Arrows on Sphere")
p4 = run_example("Surface")
p5 = run_example("Customize Axes")
pscene = vbox(
    hbox(p1, p2),
    p3,
    hbox(p4, p5, sizes = [0.7, 0.3]),
    sizes = [0.2, 0.6, 0.2]
)
  • with that we can implement arbitrary layouts
  • ... Just have to do the math and figure out nice APIs
  • better axis support needed
  • Julius Krumbiegel ported a Layout constraint solver:

Recipes

  • The act of combining primitives: Recipes
struct MyType end
function AbstractPlotting.plot!(p::Plot(MyType))
  mytype = p[1] # first argument to plot(...)
  plot!(p, ...)
end
Julia
2.6s
Run (Julia)
using AbstractPlotting
import AbstractPlotting: Plot, default_theme, plot!, to_value
set_theme!(resolution = (650, 400)); WGLMakie.activate!()
struct Simulation
    grid::Vector{Point3f0}
end
function default_theme(scene::SceneLike, ::Type{<: Plot(Simulation)})
    Theme(
        advance = 0,
        molecule_sizes = [0.08, 0.04, 0.04],
        molecule_colors = [:maroon, :deepskyblue2, :deepskyblue2]
    )
end
function AbstractPlotting.plot!(p::Plot(Simulation))
  sim = p[1][] # first argument is the SimulationResult
  mpos = lift(p.advance) do i
    sim.grid .+ rand(Point3f0, length(sim.grid)) .* 0.01f0
  end
  N = lift(length, mpos)
	r3(x) = repeat(x, outer = N[] ÷ 3)
  sizes = lift(r3, p.molecule_sizes)
  colors = lift(r3, p.molecule_colors)
  meshscatter!(p, mpos, markersize = sizes, color = colors)
  segs = lift(p.plots[end][1]) do x
    indices = Int[]
    for i in 1:3:length(x)
      push!(indices, i, i + 1, i, i + 2)
    end
    view(x, indices)
  end
  linesegments!(p, segs)
end

function viz()
    n = 5
    r = LinRange(-1, 1, n)
    grid = Point3f0.(r, reshape(r, (1, n, 1)), reshape(r, (1, 1, n)))
    molecules = map(1:(n^3) * 3) do i
        i3 = ((i - 1) ÷ 3) + 1
        xy = 0.1; z = 0.08
        i % 3 == 1 && return grid[i3]
        i % 3 == 2 && return grid[i3] + Point3f0(xy, xy, z)
        i % 3 == 0 && return grid[i3] + Point3f0(-xy, xy, z)
    end
    plot(Simulation(molecules))
end
scene = viz()
simplot = scene[end]
scene
for i in 1:100
    simplot.advance = i
    sleep(1/30)
end

What's missing

  • multiple scenes from one recipe ( and layouting them)
  • more consistent API
  • improved Node handling
  • ingesting Plots.jl recipes
  • documentation

StatsMakie

iris = RDatasets.dataset("datasets", "iris")
scatter(Data(iris), Group(:Species), :SepalLength, :SepalWidth)
using CSV, RDatasets, MakieThemes, StatsMakie, CairoMakie, AbstractPlotting
for dataset  (:www, :drivers, :mtcars, :diamonds)
  @eval $(dataset) = CSV.read(
    dirname(pathof(MakieThemes))*"/../data/"*$(string(dataset))*".tsv"
  )
end
set_theme!(ggthemr(:fresh))
p1 = lines(
  Data(www), :Minute, :Users,
  Group(color = :Measure, marker = :Measure),
)
p2 = plot(density, Data(mtcars), :mpg, Group(color = :cyl));
p3 = plot(Position.stack, histogram, Data(diamonds), :price, Group(color = :cut));
p4 = boxplot(Data(drivers), :Year, :Deaths);
vbox(hbox(p3, p1), hbox(p4, p2))

What's missing

  • legends
  • unit support
  • log/log10/logn/polar/flipping with axis support
  • more Plots.jl convenient functions

Interaction

Everything is a Node (a.k.a Observable, a.k.a Signal)

WGLMakie.activate!()
set_theme!(resolution = (600, 300))
data = Node(rand(4))
color = Node(:red)
scene = scatter(data, color = color)
lines!(Rect(1, 0, 4, 1))
data[] = rand(4)
color[] = :green;
scene = lines(Rect(1, 0, 4, 1))
scatter!(rand(4))
scene[end].color = rand(RGBf0)
scene[end][1] = rand(4);
for k in propertynames(scene.events)
  println(k, ": ", getfield(scene.events, k)[])
end

Interact is also build on Observables!

build by @shashi & @piever

using Interact
a = Interact.slider(0f0:50f0, label = "a", value = 13)
b = Interact.slider(-20f0:20f0, label = "b", value = 10)
c = Interact.slider(0f0:20f0, label = "c", value = 2)
d = Interact.slider(LinRange(0.0, 0.02, 100), label = "d", value = 0.01)
scales = Interact.slider(LinRange(0.01, 0.5, 100), label = "scale", value = 0.1)
colorsw = Interact.widget(colorant"red")
sliders = Interact.vbox(a, b, c, d, scales, colorsw)
using Colors, Interact, AbstractPlotting, WGLMakie
using AbstractPlotting: Node
function lorenz(array::Vector, a = 5.0 ,b = 2.0, c = 6.0, d = 0.01)
  t0 = Point3f0(0.1, 0, 0)
  for i = eachindex(array)
    t0 = Point3f0(
      t0[1] + d * a * (t0[2] - t0[1]),
      t0[2] + d * (t0[1] * (b - t0[3]) - t0[2]),
      t0[3] + d * (t0[1] * t0[2] - c * t0[3]),
    )
    array[i] = t0
  end
  array
end
n1, n2 = 18, 30
N = n1*n2
args_n = (a, b, c, d)
v0 = lorenz(zeros(Point3f0, N), getindex.(args_n)...)
positions = map(lorenz, Node(v0), args_n...)
mesh_scene = meshscatter(positions,markersize = scales, color = colorsw)
Interact.hbox(
    sliders,
    center!(mesh_scene)
)
img = rand(100, 100)
scene = Scene(resolution = (500, 500))
heatmap!(scene, img, scale_plot = false)
clicks = Node(zeros(Point2f0, 100))
colors = Node(zeros(RGBAf0, 100))
last_idx = Ref(0)
on(scene.events.mousebuttons) do buttons
  pos = mouseposition(scene)
  last_idx[] += 1
  clicks[][last_idx[]] = pos
  colors[][last_idx[]] = RGBAf0(1, 0, 0, 1)
  clicks[] = clicks[]; colors[] = colors[]
  return
end
scatter!(scene, clicks, color = colors, marker = '+', markersize = 10);
scene

What's missing?

  • unified API between Interact & Makie
  • picking and complex interactions in WebGL + Cairo

Showcases