SciNim / nim-plotly

plotly wrapper for nim-lang
https://scinim.github.io/nim-plotly/
MIT License
179 stars 15 forks source link

Add subplot macro for basic subplots #34

Closed Vindaar closed 5 years ago

Vindaar commented 5 years ago

This adds basic subplot support via the new subplots macro.

A basic implementation for that was quite easy, but then I realized that for real subplots it might be justified to have Plot[T] objects as a basis with different T. Therefore I introduced a PlotJson object, which is basically the Plot[T] object albeit with the two fields already converted to JsonNodes. This way we can merge the two Plot[T] of potentially different data types together. The main part of the creation of subplots is then the (currently pretty static and hacky) combine proc. Since the signature of combine isn't very nice and in my opinion the user doesn't need to know what's needed to create a subplot, I started to write a macro. In the end it's a lot of lines for a single output line of the macro, but well. It's there now. :P Usage looks like this (assuming we have a plt1: Plot[T], plt2: Plot[U], layout: Layout):

let subplt = subplots:
  baseLayout: layout
  plot:
    plt1
    left: 0.0
    bottom: 0.0
    width: 0.45
    height: 1.0
    # alternatively use right, top instead of width, height
    # single letters also supported, e.g. l == left
  plot:
    plt2
    # or just write a concise tuple, here the
    (0.55, 0.0, 0.45, 1.0)

So it consists of a layout object to be used as the base layout for the whole plot and then several plot blocks. Each plot block then just takes a Plot[T] as the first line and then a description of the plot location. Either as a nameless tuple of the type: (left, bottom, width, height) in relative coordinates of the canvas in [0, 1]. Or one can specifically set each of the fields, one per line. That way one may also use right, top instead of width, height (for good measure instead of :, = is also supported, and so are just first letters for each location, i.e. l, b, ...).

Useful sideffect of PlotJson

As a side effect, since PlotJson is now a valid type for show and saveImage one can thus easily extend plotly "on the fly" if a feature isn't available. So for instance if we have some

let plt: Plot[T] = getSomePlot()

and we wish to use a feature that's not in nim-plotly: for example to set the length of the ticks. In plotly this is done via ticklen (https://plot.ly/javascript/reference/#layout-yaxis-ticklen). Now we can just do:

let pltJson = plt.toPlotJson
pltJson["xaxis"]["ticklen"] = 10 # twice as long as default
pltJson.show()

and all should work. In theory one can also just copy paste some plotly.js code and create a JsonNode via the %* macro and with little change one can even create a plot that way. If this https://github.com/nim-lang/Nim/pull/10037 PR would be merged that would become almost trivial. Not very useful in general, but for quick prototyping of unavailable features might come in handy.

brentp commented 5 years ago

this is really neat! Looks good to me after documenting DomainAlt.

Vindaar commented 5 years ago

Will add a line about DomainAlt, replace the explicit tuple... in convertDomain and add a few lines of explanations for the macro usage in the example tomorrow. :)

brentp commented 5 years ago

cool! if you specify subplots without explicit domains, does plotly give you a reasonable default (multi) plot?

Vindaar commented 5 years ago

To be honest, I have no idea as I haven't tried it. But I do know that an alternative to explicit domain setting is the grid field (as in example 1 here: https://plot.ly/javascript/subplots/). If plotly doesn't use a good default, a grid might be a good alternative to use from our end as a default. I'll give it a try.

Vindaar commented 5 years ago

Ok, all done now. Add the ability to set a grid aside from manually setting domains (though mixing isn't supported. No clue if plotly even itself supports that). This is done via a grid block in the macro:

grid:
  rows: 2
  columns: 1

where either is optional. If only one is set, the other is assumed to be 1. If domains nor a grid definition is given, we now default to a grid, which fits all plots, favoring more columns than rows.

brentp commented 5 years ago

thanks! I actually have been needing this.