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.

First Post!

tmbb

tmbb

Some questions (especially for @viniciusmuller, but anyone else feel free to answer):

  1. Is it possible to have ExTypst generate something other than PDFs, namely SVG and PNG? SVG and PNG are useful either to deal with publishers who don’t want PDFs submitted as images, or to embed such images places like word documents

  2. Currently, typsts isn’t thaaaat slow to compile, and to my great surprise I managed to install a rust toolchain quite quickly. However, it can still be a stumbling block for some. Would it be possible to use rust_precompiled to make the installation process even easier?

  3. Would there be any way of querying typst for sizes of some of the content? I’m thinking in particular of text and math formulas, but I guess what I want is the ability to query the sizes of arbitrary boxes. This could be useful in order to have some interplay between the Elixir frontend (which decides things such as how many ticks there are in an axis) and Typst (which renders the tick labels, and as such can determine whether labels overlap or not); in case the labels do overlap, it would be interesting to have a way of feeding that information to Elixir so that Elixir could pick a smaller number of ticks. I’m not asking for bidirectional communication between typst and Elixir, but would it be possible for Elixir to query the sizes of at least some of the generated boxes so that it would know the optimal number of ticks?

There should be a way to configure plots in order to decide on things such as line width, default font size, etc. Although all of this can be passed manually into functions, or using raw typst script, I think it would be easier if there was a way of saying stuff like “axis labels should be 9pt and bold”. Matplotlib (which is my main inspiration), already provides a way of doing this using a “global” configure, which can be set in the current context. The idiom for doing that in Python is:

with matplotlib.rc_context({key: value, another_key: another_value, ...})
    # code that plots stuff using this style

# the style defined above is no longer valid here

The most direct correspondence to this is to use the process dictionary to define the “global config” in a way that it doesn’t need to be passed down into each function. This could be encapsulated in a function, so we could actually have something like this:

Playfair.with_config(%{my: "new", config: "here"}, fn ->
  # Code that plots things and takes the config from the process dictionary
end)

The main issue with using the process dictionary for configuration is that it stops working if we start spawning processes to draw our stuff. I don’t think this will be very common, though… Although data analysis can be parallelized (and often is!), data vizualization is often very “serial” (and not parallel) in nature. I wonder what more experienced people think about this. The Elixir formatter, for example, uses (used to use? I haven’t looked at the source for some time) the process dictionary to store some configuration options to avoid having to pass them around through all the functions.

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.

Last Post!

tmbb

tmbb

Big update - Integration with SciEx

I have finally released a SciEx on hex.pm, which means that Quartz can now depend on SciEx, which provides some rust-based goodies which are very intersting, like support for proper contours and isobands (not completely implemented yet) and better KDE algorithms (not implemented in Quartz yet).

Not that the code below uses the SciEx.Float64.Array2 data type from SciEx, which is actually the first good implementation of a 2D array I’ve seen in Elixir (it uses Rust’s ndarray as a backend and it can be made to support matrix multiplications, FFT, etc.)

    height_map = Array2.from_image("test/data/heightmaps/image.png")

    snapshot = Path.join(@out_dir, "countour_1_snapshot.png")

    figure =
      Figure.new([width: Length.cm(8), height: Length.cm(6)], fn _fig ->
        _plot =
          Plot2D.new()
          |> Plot2D.put_title("A. From heightmap")
          |> Plot2D.put_axis_label("x", "Horizontal")
          |> Plot2D.put_axis_label("y", "Vertical")
          |> Plot2D.draw_contour_plot(height_map, [0.2, 0.4, 0.6, 0.8])
          |> Plot2D.finalize()
      end)

    Figure.render_to_png_file(figure, snapshot)

Which renders the following contour map (future versions might support smoothing).

Where Next?

Popular in Announcing Top

kelvinst
Hey everyone! Well, we made this lib a while ago and now we decided to finally go out and public with it! It’s a tool for creating and m...
New
treble37
Just looking for a little feedback on a tiny helper library I built - Sometimes I find the need to convert maps with atom keys to maps w...
New
kip
ex_cldr provides localisation and internationalisation support based upon the data from the Unicode CLDR project. Unicode released CLDR ...
407 13366 120
New
dominicletz
Hi, I thought I had posted my library before but seems I hadn’t. The project is still in early stages but it’s growing and so I think it...
New
trisolaran
Hi! :waving_hand: I would like to present LiveSelect, a little library that I wrote to easily add a dynamic selection input to your LV f...
198 11283 107
New
woylie
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries. offset-based pagination with...
New
restlessronin
The repo is at GitHub - cyberchitta/openai_ex: Community maintained Elixir library for OpenAI API · GitHub. Docs are at OpenaiEx User Gu...
152 10796 134
New

Other popular topics Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement