tmbb

tmbb

Quartz - Data Visualization Library (aimed at publication, not interactive display)

Playfair (named after: William Playfair) is a data visualization/plotting library with the goal of being able to produce publication-quality figures without the use of any other tools. Because producion publication-quality figures always entails some manual adjustments, Playfair aims to be very customizable. When picking between convenience/terseness or customizability, Playfair will often choose customizability. Playfair doesn’t draw anything directly. Instead, it uses the typst typesetting system as a backend, through the interface provided by ExTypst.

Currently it only supports boxplots (and the boxplots don’t even show the outliers outside the whiskers because I haven’t implemented that part yet). I’ve decided to implement boxplots before more basic plots such as scatter plots becuase boxplots are actually quite complex.

The library is under very active development and APIs might changes without warning. Don’t use this for anything serious yet.

Some example code:

XYPlot.new()
# Data is generated somehwere else in the code
|> BoxPlot.plot("x", "y", data())
|> XYPlot.put_title(Typst.raw("strong()[A. Reaction time according to group]"))
|> XYPlot.put_axis_label("y", Typst.raw("strong()[Reaction time (ms)]"))
|> XYPlot.put_axis_label("x", Typst.raw("strong()[Populations]"))
# Drop the spines for the X2 and Y2 axes (you can also remove those axes instead)
|> XYPlot.drop_axes_spines(["x2", "y2"])
|> XYPlot.render_to_pdf_file!("examples/box-plot-example-without-spines.pdf")

Generated figure (converted from PDF to PNG because ExTypst can’t generate PNG or SVGs directly, althougn I believe typst itself can):

The code is inspired by Python’t Matplotlib but with a more functional style. The goal is to provide all the plot types and general functionality provivded by matplotlib except for the interactive parts. I’m open to support animatinos by generating multiple frames and then gluing them together, but it’s definitely not a priority.

Issues (so far!)

Unlike Matplotlib, it’s not yet possible to have more than one plot in the same figure (Playfair doesn’t even have the notion of a figure), but that’s actually quite simple to implement using typst as a backend (we can just reuse the native typst layouts to compose plots together).

The main issue with Playfair right now is that it isn’t easy to draw arbitrary content in the labels area, for example (you might want that if you’re doing some survival analysis with Kaplan-Meier curves and want to draw a table with the “at risk” counts below the x-axis ticks). In all fairness, this is something with which even Matplotlib strugles a lot. Most of the interesting functionality requires some kind of constraint solver (even a linear solver employing something like the simplex method would probably be enough for most cases), and it’s hard to provide an interface which makes it natural to draw something AND which is compatible with the way constraint solvers like to work.

Another problem with trying solve constraints intelligently is that we actually need the backend (i.e. typst) to evaluate the object sizes (the Elixir part has no idea how text is rendered, or even worse, how mathematical formulas are rendered!), and this means that a lot of the constraint solving must happen inside typst, instead of calling an optimized constraint solver.

Most Liked

tmbb

tmbb

For anyone following this, it turns out I’m a total idiot… Elixir is handling tens of thousands of elements just fine, as I have just discovered with better benchmarking. Even if each element contains a number of totally not optimised polynomials implemented as structs.

It turns out that unlike what I previously reported, the problem is with typst. Problems start to happen when I try to compile typst files of > 20.000 lines with a large number of graphical objects. To be fair to typst, these files are probably much larger than anything anyone has tried to use typst on (I don’t know whether the critical part is the patching or the rendering, but I’m not that interested in finding out).

Because of this, I have decided to keep the core constraint-based architecture the same and drop typst. I’ll be try to use resvg (the rust svg rendering library, which also has Elixir bindings) for text rendering and measuring and to render SVGs into PNGs. I assume that resvg is probably better optimised than typst for rendering larger numbers of shapes.

The only thing I lose is math typesetting, but it turns out plots don’t use that much math typesetting. One can get by with unicode symbols, superscripts, subscripts and not much else.

tmbb

tmbb

Quartz (this library is no longer called playfair) can now draw line plots (by very inefficiently drawing line segments individually instead of using a normal path because I haven’t iomplemented paths yet).

Example here:

Code:

defmodule Quartz.Benchmarks.LinePlot do
  use Dantzig.Polynomial.Operators
  require Quartz.Figure, as: Figure
  alias Quartz.Plot2D
  alias Quartz.Length

  def build_plot() do
    figure =
      Figure.new([width: Length.cm(8), height: Length.cm(6), debug: false], fn _fig ->
        [[bounds]] =
          Figure.bounds_for_plots_in_grid(
            nr_of_rows: 1,
            nr_of_columns: 1,
            padding: Length.pt(16)
          )

      x = for i <- 1..100, do: 0.01 * i
      y = for x_i <- x, do: x_i * 0.3 + (0.05 * :rand.uniform())

      data = %{x: x, y: y}

      _plot =
        Plot2D.new(id: "plot_A")
        |> Plot2D.set_bounds(bounds)
        |> Plot2D.line_plot("x", "y", data)
        # Use typst to explicitly style the title and labels ――――――――――――――――――――――――――――――――
        |> Plot2D.put_title("A. Line plot")
        |> Plot2D.put_axis_label("y", "Prediction: $f(x)$", text: [escape: false])
        |> Plot2D.put_axis_label("x", "Predictor: $x$", text: [escape: false])
        |> Plot2D.finalize()
      end)

    path = Path.join([__DIR__, "line_plot", "example.pdf"])
    Figure.render_to_pdf_file!(figure, path)
  end
end
tmbb

tmbb

Some more examples, this time for distribution plots.

KDE smoothing of the posterior distributions for a NUTS bayesian simulation, showing all 4 chains in the same plot:

The same data plotted using boxplots (boxplots are not very configurable yet, but that is just a question of adding more options):

Finally, the contour plot I’ve shown before, but with a much higher resolution now that we don’t depend on typst and can use the more performant (for this use case) resvg renderer (note that we can fake powers of integers with superscript unicode characters, although for more advanced things one really needs proper math typesetting):

And no, Quartz still can’t rotate text, which messes up the case where the labels of the y-axis are longer, but it works fine for these short labels.

tmbb

tmbb

Big landmark! Quartz can now accurately rotate text, which means it can render actual real-world labels for the Y-axis of plots:

Some questions for whoever may be following this:

  1. What do you think of the choice of using the Sans-Serif Ubuntu font as a default? The license is quite permissive, and it’s certainly prettier than other options such as DejaVu, used by Matplotlib. However, I’ve been thinking of making the default a Serif font, such as Linux Libertine (which I actually use to typeset math characters)

  2. Now that I can support rotation of text and legends, I think that it might be the right time to think about releasing a 0.1 version. What do you think would be the “essential” plot types for an actual release on Hex? I already support the histogram, the KDE for continuous distributions, line plots, scatterplots and “fill-between” plots. For things like survival analysis, plotting a step function is useful (survival functions are usually represented as Kaplan-Meier plots, which are pretty much always displayed as step functions)

  3. Anyone here is interested in diving into the constraint-solving part of things? Currently it works very well, and allow for very useful dynamic layouts in which things “fit into place” magically. The problem is that failures aren’t really debuggable. Is anyone here experienced with Linear programming solvers or other kinds of constraint solvers? When the linear solver fails, the mains problem is that I just raise an error and can’t give the user any feedback on which constraints are causing problems or why. Is there any literature on this? Should I just randomly remove constraints from the problem (with something like a binary search) and try to detect which of them cause problems? The way things are implemented now, there won’t be any constraint-solving problems unless the user creates “raw” constraints using the Figure.assert/1 macro, but I wonder if one can enhance debuggability

  4. For actual real-life plots, how important would it be to support proper mathematical formulas beyond math symbols, subscripts and superscripts? I figure I can probably integrate with Typst to get proper math typesetting, but that would split text handling into two. Or maybe I could integrate with Typst for all text and leave the shape renfering to Quartz

tmbb

tmbb

Version 0.8.1

With this new version, I was finally able to update the docs to hexdocs (API Reference — Quartz v0.8.1) by moving all images to a new website: https://tmbb.github.io/quartz/. This is currently hosted on GitHub pages, but when I have the time, I’ll host it in its own domain. These changes allow me to remain within the size limits of hexdocs.

Demo for some of the supported plots here: https://tmbb.github.io/quartz/plot_types.html

Plotting API

The plotting API is inspired by Matplotlib. Despite being quite old, Matplotlib is very functional for my needs and makes it very easy to customize the plots, which is important for publication-quality figures.

I really don’t like frameworks which encourage any kind of “grammar of graphics”, which always seem very artificial to me and obscure what would usually be simple plotting commands.

Contributing

I will have a bit more free time in April, and I would like to refactor the code to stabilize the interface and make it easier for external contributors. I think this package could be a good alternative to generate plots for scientific paper or other static media (like Matplotlib). Together with my Ulam package, one can already create, fit and plot Bayesian models, and honestly I prefer to manage Elixir projects as opposed to Python projects (packaging is python projects is rather problematic).

I don’t know if people are still trying to develop an ecosystem for scientific programming in Elixir, but if so, Quartz would be an interesting part of such ecosystem.

Where Next?

Popular in Announcing Top

Gigitsu
Hi everyone! I’d like to share a small library I’ve recently extracted from a project I’m working on: ginject. ginject provides a minim...
New
shahryarjb
The Chelekom project is a library of Phoenix and LiveView components generated via Mix tasks to fit developer needs seamlessly. One of i...
New
webofbits
Helix is a visual workflow designer for AI agents and multi-agent systems, built with Phoenix and React Flow. It provides an intuitive dr...
#ai
New
type1fool
WebAuthnLiveComponent WebAuthnComponents See this post about renaming the package. Passwordless authentication for Phoenix LiveView app...
New
jallum
Turbopuffer - Elixir client for vector and full-text search I’m excited to share Turbopuffer, a new Elixir client library for the Turbop...
New
anuaralfetahe
Hello Published a new library - ProcessHub! ProcessHub is a library designed to manage process distribution within the Elixir cluster. ...
New
fhunleth
Elixir Circuits is a set of libraries for interacting with hardware. We previously announced Circuits.UART, and now we’re ready to announ...
New
corka149
A JSON patch is a way to define a sequence of manipulating operations on a JavaScript object. The IETF published the RFC 6902 - found he...
New
taro
I took lessons from the last discussion and cobbled together an example as a proof-of-concept. Mar demonstrates a Flask-like web dev int...
New
volcov
Hello, How is everyone? I hope you’re well =) I worked on a project with some legacy code that existed before Oban’s arrival. This leg...
New

Other popular topics Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement