Comparison of plotting packages

https://makie.org gives you some overview to figure out if its worth it :wink:
The docs are alright, although they’re certainly not perfect.
We do have a few plans to improve them in the next months: [Roadmap] Documentation · Issue #3105 · MakieOrg/Makie.jl · GitHub

5 Likes

I gives some overview in respect to capabilities, not in respect to ease of use and ease to switch (from Plots).

OK, there is Basic Tutorial in the docs.

If you say you don’t need interactivity and animation and speed of plotting larger data like images, it’s less important for you which lib you will use. In my use-case, interactivity was an absolute must, like I click on something on the image/plot and then run an algorithm over the selected column/data and display other subplots based on the result. Only Makie (i use GLMakie) can do this.

I you have time, yes, at least give it a try.
Another thing to consider: the more users a library has, the more resources this lib will get, so I’m strongly in favor of any Julia based plotting lib. Therefore I think we shall prefer Makie over Plots as I think it’s best to develop Makie and not PyPlot(matplotlib) for instance.

1 Like

When I started using Julia, I also tried out Plots.jl first. I was a matplotlib / Matlab user before and these were / are my reasons for not sticking with Plots.jl, and starting to contribute to Makie instead:

  • It gets you 80% of the way quickly, but I’m a bit perfectionist and need as much control as I can have. So I got into fiddling with the backends quickly, which was annoying. Things I wanted to control were mostly fonts, spacings, avoiding overlaps, choosing exact positions of colorbars and legends.
  • My kind of work/data didn’t really benefit from the “apply plot to some object” workflow, I could have used something ggplot-like but both Gadfly and StatsPlots fell way short in that regard for me.
  • I often got confused with its behavior where you pass row vectors for multiple series, and you have to coordinate that with the layout keyword to make facet plots etc. I wanted more explicit control over the layout (hence MakieLayout.jl).
  • I didn’t see much benefit in having multiple backends if they all look kind of different and make me learn their internals so I can tweak things. GR was not as far developed at the time if I remember correctly, I couldn’t choose arbitrary fonts for example. The matplotlib backend came with reproducibility / python env problems. Plotly wasn’t good for making animations.
  • The backends aren’t written in Julia, so they all have their own specific issues at the boundary between languages which can matter for large datasets.
  • Limited or no custom interactivity
  • It was more fun to get involved with Makie, a young project that wasn’t set in its ways yet.

Some of these may have changed over the years, but now it’s too late for me :wink:

15 Likes

About switching, I recently updated some code from Gadfly to Plots, and while it was relatively easy, it was still a lot of work for little benefit. So to switch you probably want a real need or at least start fresh with a new project.

2 Likes

This doesn’t seem like a fair characterization.

This interactivity of Vega-Lite is really cool, but how can I replicate this with VegaLite.jl?

Deneb seems to have a more thorough gallery of examples, including a section on interactive examples: Gallery · Deneb.jl

4 Likes

Take a look at Tutorial · VegaLite.jl, that explains how one can generally translate any vega-lite spec into the equivalent VegaLite.jl version. Using that, I get the following, which works fine:

@vlplot(
   data = {
      url = URI("https://vega.github.io/vega-lite/examples/data/stocks.csv"),
      format = {parse = {date = "date"}}
   },
   height = 300,
   width = 650,
   layer = [
      {
         encoding = {
            x = {
               axis = nothing,
               field = "date",
               type = "temporal"
            },
            opacity = {
               value = 0
            }
         },
         params = [
            {
            name = "index",
            select = {
               nearest = true,
               on = "mouseover",
               encodings = [
                  "x"
               ],
               type = "point"
            },
            value = [
               {
               x = {
                  month = 1,
                  year = 2005,
                  date = 1
               }
            }
            ]
         }
         ],
         mark = "point"
      },
      {
         encoding = {
            x = {
               axis = nothing,
               field = "date",
               type = "temporal"
            },
            color = {
               field = "symbol",
               type = "nominal"
            },
            y = {
               axis = {
                  format = "%"
               },
               field = "indexed_price",
               type = "quantitative"
            }
         },
         mark = "line",
         transform = [
            {
               from = {
                  key = "symbol",
                  param = "index"
               },
               lookup = "symbol"
            },
            {
               calculate = "datum.index && datum.index.price > 0 ? (datum.price - datum.index.price)/datum.index.price : 0",
               as = "indexed_price"
            }
         ]
      },
      {
         layer = [
            {
               mark = {
                  strokeWidth = 0.5,
                  type = "rule"
               }
            },
            {
               encoding = {
                  text = {
                     timeUnit = "yearmonth",
                     field = "date"
                  },
                  y = {
                     value = 310
                  }
               },
               mark = {
                  fontWeight = 100,
                  type = "text",
                  align = "center"
               }
            }
         ],
         encoding = {
            x = {
               axis = nothing,
               field = "date",
               type = "temporal"
            },
            color = {
               value = "firebrick"
            }
         },
         transform = [
            {
            filter = {
               param = "index"
            }
         }
         ]
      }
   ]
)
3 Likes

Thanks David, I will give it another try.

EDIT: It worked. Not out of the box, but that’s probably because of corporate firewall issues on my part to obtain the flat file. When modifying the example to separately load the data via VegaDatasets and piping the data in (as in other VegaLite.jl examples) - i got the beautiful interactivity, both in Pluto and in VSCode. I fully agree with David that this interactivity is where VegaLite.jl really shines compared to other plotting packages.

1 Like

@klwlevy I just added that example in Deneb.jl’s gallery: Interactive index chart · Deneb.jl

This is how I would do it using Deneb.jl’s api:

using Deneb

set_theme!(:default_no_tooltip)

stocks_url = "https://vega.github.io/vega-datasets/data/stocks.csv"

base = Data(url=stocks_url) * Encoding(x=field("date:T", axis=nothing))

points = Mark(:point, opacity=0) * select_point(
    :index,
    encodings=:x,
    on=:mouseover,
    nearest=true,
    value=(;x=(;year=2005)),
)

lines = Mark(:line) * Encoding(
    y=field("indexed_price:Q", axis=(;format="%")),
    color="symbol:N"
) * transform_lookup(
    :symbol, (param=:index, key=:symbol),
) * transform_calculate(
    indexed_price="datum.index && datum.index.price > 0 ? (datum.price - datum.index.price)/datum.index.price : 0",
)

rule = Mark(:rule, color=:firebrick, strokeWidth=0.5)

label = Mark(:text, color=:firebrick, align=:center, fontWeight=100, y=310) * Encoding(
    text="yearmonth(date)",
)

label_rule = transform_filter(param(:index)) * (rule + label)

chart = base * (points + lines + label_rule) * vlspec(
    width=650, height=300
)

Of course, it can also be done by directly translating the Vega-Lite specification as explained in here. This would be the code:

vlspec(
    data = (
        url = "https://vega.github.io/vega-datasets/data/stocks.csv",
        format = (; parse = (; date = "date"))
    ),
    width = 650,
    height = 300,
    layer = [
        (
            params = [(
                name = "index",
                value = [(; x = (year = 2005, month = 1, date = 1))],
                select = (
                    type = "point",
                    encodings = ["x"],
                    on = "mouseover",
                    nearest = true
                )
            )],
            mark = "point",
            encoding = (
                x = (field = "date", type = "temporal", axis = nothing),
                opacity = (; value = 0)
            )
        ),
        (
            transform = [
                (
                    lookup = "symbol",
                    from = (param = "index", key = "symbol")
                ),
                (
                    calculate = "datum.index && datum.index.price > 0 ? (datum.price - datum.index.price)/datum.index.price : 0",
                    as = "indexed_price"
                )
            ],
            mark = "line",
            encoding = (
                x = (field = "date", type = "temporal", axis = nothing),
                y = (
                    field = "indexed_price", type = "quantitative",
                    axis = (; format = "%")
                ),
                color = (field = "symbol", type = "nominal")
            )
        ),
        (
        transform = [(; filter = (;param = "index"))],
        encoding = (
            x = (field = "date", type = "temporal", axis = nothing),
            color = (; value = "firebrick")
        ),
        layer = [
            (; mark = (type = "rule", strokeWidth = 0.5)),
            (
                mark = (type = "text", align = "center", fontWeight = 100),
                encoding = (
                    text = (field = "date", timeUnit = "yearmonth"),
                    y = (; value = 310)
                )
            )
        ]
        )
    ]
)
3 Likes

Here’s a similar interactive plot using Makie:

using WGLMakie
using Downloads, CSV, DataFrames

file = Downloads.download("https://vega.github.io/vega-datasets/data/stocks.csv")
df = CSV.read(file, DataFrame)
wide = unstack(df, :date, :symbol, :price)
symbols = names(wide, r"[A-Z]")
data = Array(wide[!, symbols])
cursor = Observable(1)
stock_levels = @lift permutedims(data) ./ data[$cursor, :]
fig, ax, _ = series(stock_levels, labels=symbols, color=:Set1, linewidth=2.5)
vlines!(cursor, color=:red)
text!(cursor, 0, text=@lift(string(" ", $cursor)), color=:red)
axislegend()

on(events(ax).mouseposition) do ev
    x, y = round.(Int, mouseposition(ax))
    if x != cursor[] && 1 ≤ x ≤ nrow(wide)
        cursor[] = x
    end
    autolimits!(ax)
end
fig

(I’m just moving the mouse around, unfortunately the mouse pointer is not visible in the recording.)

Some remarks:

  • It feels quite sluggish.
  • Support for dates is still poor in Makie so here I just show the row numbers to keep the focus on the mechanics of interaction.
  • I also couldn’t find how to show the red label under the red line (outside of the axis). Does anyone have a solution for that?
  • WGLMakie is still experimental and I think it shows: I found it fiddly while developing, for example sometimes I got errors with Observables or things were not updating as expected until restarted the kernel and reloaded the IJulia page. Just use GLMakie instead of WGLMakie and it should be more robust.

Finally, a great thing with VegaLite (as I understand) is that the results can be shared/deployed easily as static web pages. So here’s a variation of the above example using JSServe to make a static export:

using WGLMakie, JSServe
using Downloads, CSV, DataFrames

file = Downloads.download("https://vega.github.io/vega-datasets/data/stocks.csv")
df = CSV.read(file, DataFrame)
wide = unstack(df, :date, :symbol, :price)

symbols = names(wide, r"[A-Z]")
data = Array(wide[!, symbols])

app = App() do session::Session
    cursor = Slider(1:nrow(wide))
    
    stock_levels = @lift permutedims(data) ./ data[$cursor, :]
    with_margins(x) = x .+ (x[2]-x[1]) .* (-0.1, 0.1)
    limits = @lift (nothing, with_margins(extrema(skipmissing($stock_levels))))

    fig = series(stock_levels, labels=symbols, color=:Set1, linewidth=2.5, axis=(; limits))
    axislegend()
    
    vlines!(cursor, color=:red)
    text!(cursor, 0, text=@lift(string(" ", $cursor)), color=:red)
    
    return JSServe.record_states(session, DOM.div(cursor, fig))
end

Page(exportable=true, offline=true)
open("stocks.html", "w") do io
    println(io, "<html><head></head><body>")
    show(io, MIME"text/html"(), app)
    println(io, "</body></html>")
end

You can try the result here.

Remarks:

  • This uses a slider because I don’t think JSServe supports mouse events currently (without custom JavaScript).
  • I couldn’t get the axis limits to update in the static export (but it works when running the App in the notebook).
  • Again lot’s of restart/reload needed during development.
  • The static export is not sluggish at all :slight_smile:
2 Likes

Possibly because autolimits! also updates the tick sizes, which updates the layout. You could call tight_ticklabel_spacing!(ax) and see whether that makes a difference when interacting? I don’t think autolimits! should be that slow with this amount of data. Sometimes that’s a bottleneck for quick updates, unnecessary limit calculations.

1 Like

It’s true, that WGLMakie is currently not as polished and more sluggish than Vegalite (because of round tripping to julia), making it more of a stopgap solution for people who have already bought into the Makie ecosystem.
We do have a roadmap to move more things to JS, to make it better at competing with Vegalite.
The next breaking release will already improve WGLMakie interactivity quite a bit, since I moved more code to JS for the line primitive.
Better static exports will also be a consequence of moving more interactions to JS, so it hopefully wont be like that anymore in the not so distant future.
This should also substantially help with time to first plot, so hopefully WGLMakie will be much nicer for notebooks and web apps soon :slight_smile:

10 Likes

@jules yes it’s much better with tight_ticklabel_spacing!(ax) !

@sdanisch wow that roadmap is nice. It’s exciting to follow Makie’s development, I’m impressed how you keep setting ambitious goals.

Maybe my post gave the wrong impression… I made sure to mention all the negative points (to compensate for my partiality towards Makie), but actually I think it shows that Makie is great for this kind of interactive plots, or will be soon. I like how it’s all simple, usual Julia code, and rather elegant I think. I prefer that to VegaLite where you have to learn another language and cannot combine it with Julia pieces.

3 Likes

No worries, it’s maybe a reflex to answer with workarounds or fixes to negative points :slight_smile: In the end, there are always choices and tradeoffs, so some things will be better in other packages no matter how hard we work on them.

VegaLite is really cool I think, declarative plotting frameworks tend to be quite elegant. I personally like the drawing-board model more where I can just put things wherever and however I want. The drawback is that some simple things for VegaLite are more difficult in Makie.

2 Likes