Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set plot area #7

Open
bennn opened this issue Jul 9, 2015 · 15 comments
Open

Set plot area #7

bennn opened this issue Jul 9, 2015 · 15 comments

Comments

@bennn
Copy link
Contributor

bennn commented Jul 9, 2015

It's hard to align plots with different-sized axis labels. See below -- these are equal-sized figures vl-append-ed together (code is far below)

out

Suggestions

  • Add parameters to set plot area, rather than overall plot+axis labels area
  • Add option to pad labels to a fixed width

Code

#lang racket

(require plot/pict pict racket/class)

(define (make-ticks #:multiplier [mul 1])
  (ticks (lambda (lo hi)
           (for/list ([i (in-range lo hi)])
             (pre-tick i #t)))
         (lambda (lo hi pre)
           (for/list ([pt (in-list pre)]
                      [i (in-naturals)])
             (number->string (* mul i))))))

(define (make-plot m)
  (parameterize ([plot-y-ticks (make-ticks #:multiplier m)])
  (plot-pict (function (lambda (x) x))
    #:x-min 0
    #:x-max 5
    #:y-min 0
    #:y-max 5
    #:width 90
    #:height 90)))

(define plt (vl-append 0 (make-plot 1) (make-plot 10000)))

(send (pict->bitmap plt) save-file "maligned.png" 'png)
@gus-massa
Copy link
Contributor

Seconded.

I have a problem with the x axis, so I "solved" it using the alignment to hide the difference.

Perhaps I'd prefer to be able to set the four margins between the plot area and the image area.

@spdegabrielle
Copy link
Member

is this still an issue? Please consider adding the label good first issue so it is findable:

Issues labeled good first issue in in Racket GitHub repositories

@gus-massa
Copy link
Contributor

Is this a good first issue? I didn't look too much under the hood, but my guess is that there are too many moving parts that interact to place all the parts of the image.

(IIRC this is still a problem, and I still think it will be a very useful addition.)

@spdegabrielle
Copy link
Member

@gus-massa it was part of a list of 'good first bugs' at https://github.com/racket/racket/wiki/Racketeer-Office-Hours-2017-Task-Ideas

@alex-hhh
Copy link
Collaborator

alex-hhh commented Jun 25, 2020

The margins for the plot area are calculated here

(let-values ([(left-val right-val top-val bottom-val)

but this is an iterative process looking at the size of the decorations (labels, ticks, etc) and making sure they have enough space to be drawn along the plot area. If we just override these values with user provided values, the user will now have to fiddle with these values, and the labels can enter the plot area or be clipped if the supplied margins are wrong.

The alternative would be to separate this calculation and have get-param-vs/set-view->dc! look at all the labels and ticks across several plot calls (that is, get-all-label-params and get-all-tick-params should look at other plots internal data):

(: get-param-vs/set-view->dc! (-> Real Real Real Real (Listof (Vectorof Real))))

Currently, each plot invocation is completely separate and even the plot parameters are captured in structures and used inside a plot call. To implement this functionality we would need to think about how to create a shared plot state across several plots.

This would be a useful feature indeed, but it would require a significant redesign of the internals of the plot library. However, if someone has other ideas, I would be happy to help out with the implementation.

@rfindler
Copy link
Member

Being able to get a set of plots that all have the same size plot area (and, indeed, even to have the decorations drawn outside the bounding box in "pict" mode) would really be great for using plots in figures in papers! It would make layout so much easier/nicer.

@alex-hhh
Copy link
Collaborator

OK, lets look at the other end of this problem. How should the interface look like?

The current situation

Currently each plot call produces a single plot, and "packaging" plots is done outside of the plot library. In the example for this issue, two separate picts were produced using plot-pict and they are "stitched together" using vl-append from the pict library.

There is a lot of flexibility when it comes to stitching plots together, but all this is outside of the plot library, keeping the plot library somewhat simpler.

How would this work for aligning the areas

If we are to align the plot areas, each plot invocation needs to know about the other plots. These are the current entry points into creating plots:

  • plot and plot-snip -- these produce snips which can be viewed in the DrRacket REPL or added to GUI applications. the produced snips can end up in completely different editor-canvases% -- this would be my use case, where I would like to align the plot areas in plot snips which are quite "far away" in terms of referencing objects (i.e. in a different editor-canvas%)
  • plot-pict -- produces pict objects, which can than be combined with other picts (either produced by plot-pict or other pict constructors)
  • plot-bitmap -- produces image files on disk
  • plot-dc -- this is the basic plot function which draws a plot onto a subset of a dc<%> surface.

I can see two broad approaches to this: sub-plots and plot grouping.

Sub-plots

Pythons mathplotlib seems to use the concept of subplot, where there is a single plot area, which can be split into sub-areas, one for each plot. If we take this approach, each of the plot function would need to have a "subplot" interface, defining how several plots are aligned.

This shows the things that mathplotlib can do:
https://matplotlib.org/devdocs/gallery/subplots_axes_and_figures/subplots_demo.html

Implementing this means we need to add layout functionality to plot, but also means that we can implement things like sharing axes (see mathplotlib examples)

Plot Groups

Another option is to simply "group" plots together somehow. This would work as follows: All plot functions, except for plot-snip go through plot/dc, but plot-snip works the same. plot/dc does the following:

  • processes the arguments to plot to obtain the renderers and the plot bounds
  • creates a plot area
  • calls plot-area on the newly created plot area to render the plot

With plot groups the steps would be:

  • each plot inside a group processes their arguments
  • each plot inside the group creates their plot area
  • the plot group grabs all plot areas and syncs them (e.g. sets their margin so they are equal)
  • each plot inside the group calls plot-area on its own area with its own renderers to produce a plot

Implementing this would be simpler (although I am not sure how the API would look), but it would not allow things like sharing axes.

@mfelleisen
Copy link

From very far, the Python approach strikes me as imperative (you get an area to play) while the Plot Group one feels compositional. I have often used an approach like this to figure out the size of picts before I compose them (you create preliminary versions of all the pieces, measure, and then the force puts them all into the best size as determined by the overall thing).

@soegaard
Copy link
Member

soegaard commented Jun 26, 2020 via email

@rfindler
Copy link
Member

Looking at the python pictures, it seems to me that if I could build a plot by specifying a width and height for the core plot area (and then the decorations sized themselves to that) then I could use the pict library and the existing control of plots to get all those pictures.

For example, if I wanted a 2x2 group of plots that shared axies everywhere, I would pict a fixed width and height and then I'd make a plot for the upper-left corner with no axies and a legend, a plot for the lower left with both axes and no legend, and plots for the other two corners with one axis each and then I could use vl-append and ht-append (or probably better, pict's table function) to put them together. (I have done similar things in papers in the past that were all slightly wrong because I couldn't get the control I needed -- for example, check out figure 15 on page 24 of this pdf.)

That said, the plot snips do more than just draw, they allow interaction, so I'm not sure of the best way of working that in. Perhaps it is fine to not be able to combine these two pieces of functionality, however. That is, if I want created a plot to be able to interact with it, maybe I'm okay not being able to lay it out super carefully? Or maybe we should really be looking at bulking up things at the editor<%> layer to support that (that is, put the plots into a pasteboard% and use its controls to align them nicely -- then we'd still have the ability to interact).

@rfindler
Copy link
Member

(And I realize I might not have been completely clear -- the resulting picts would need to have their bounding boxes be set to the plot area only -- so some of the decorations would draw outside the bounding boxes, so the bounding boxes could be used for alignment.)

@gus-massa
Copy link
Contributor

In my case, I wanted a few plots of very similar functions. Something like:

Text
[Plot]
More text
[Plot]
And more text

And I really wanted that all the plots use the exact same scale. But I didn't want all of them to be in a single block, because I wanted to put some text between them.

I can imagine that in a similar situation I'd like to have the exact same horizontal scale, but I would not care about the vertical scale.

@alex-hhh
Copy link
Collaborator

@soegaard , the plot library uses three coordinate systems, and using the terminology that plot uses, they are:

  • The first one is the plot coordinates system, these are the coordinates of the functions being plotted. For example, if you plot the sine function from -5 to 5, the plot bounding box will be (-5, 5, -1, 1)
  • The last one is the device context coordinates system, which uses the coordinates on the drawing surface. For example, if you want a picture of 500x500 pixels, the device context coordinates might be (0, 400, 0, 400), to account for the plot decorations
  • In the middle sits the view coordinate and it is the tricky one. It is the one which adjusts the plot coordinates according to various axis transforms. for example, if an axis uses a logarithmic transform, it will be applied to a plot coordinate before calculating its device context coordinates. The view coordinate system is not defined completely by its bounding box. This is because plot supports axis transforms for a subset of the axis (see stretch-transform and collapse-transform)

As such, for a general case, it is not sufficient for the plot to just return a bounding box (or two of them), but it would need to give you access to the drawing area object which provides functions for converting between the three coordinate systems.

I am not sure how metapict works, but it is a lot easier to do things the opposite direction: plot already has a point-pict function which allows placing a pict on the plot area, and we could extent the #:label argument for renderers to accept a pict as well, to handle complex legend rendering, as I suggested in #58


Than, there is this statement (emphasis is mine):

Plot returns a pict without further information.

The plot library can produce picts, bitmap files, draw onto device context and provide interactive snips. There is also the 3D variant. Any new functionality should be available for all these interfaces. In a different message @rfindler mentioned that:

That is, if I want created a plot to be able to interact with it, maybe I'm okay not being able to lay it out super carefully?

I disagree: I use interactive plots, and I would like to be able to align them, this is why I participate in this discussion :-). Here is an example where all plots are interactive and also stretch transforms are used to "zoom in" into a section of the plot, all highlighted sections should be aligned:

img 243

@soegaard
Copy link
Member

soegaard commented Jun 27, 2020 via email

@ralsei
Copy link
Contributor

ralsei commented May 23, 2021

While working on faceting for Graphite, I came across this work-around for setting the plot area:

#lang racket
(require plot/pict pict)

(define (plot-extras-size plotpict)
  (match-define (vector (vector x-min x-max)
                        (vector y-min y-max))
    (plot-pict-bounds plotpict))
  (match-define (vector x-left y-bottom)
    ((plot-pict-plot->dc plotpict) (vector x-min y-min)))
  (match-define (vector x-right y-top)
    ((plot-pict-plot->dc plotpict) (vector x-max y-max)))

  (define inner-width (- x-right x-left))
  (define inner-height (- y-bottom y-top))

  (values (- (pict-width plotpict) inner-width)
          (- (pict-height plotpict) inner-height)))

(define (plot-with-area plot-thunk area-width area-height)
  (match-define-values ((app inexact->exact y-extras)
                        (app inexact->exact x-extras))
    (plot-extras-size (plot-thunk)))

  (parameterize ([plot-width (+ area-width y-extras)]
                 [plot-height (+ area-height x-extras)])
    (plot-thunk)))

(plot-with-area (thunk (plot (function sin -2 2)))
                1024 768)

which works by calculating the size of the non-plot area (as it's presumably invariant and not dependent on plot-width/plot-height), and then adding that to the intended plot area.

Unfortunately, this results in rendering the plot twice, which is probably inefficient. However, it's still likely useful to someone. This can likely be adapted to interactive plots -- but I haven't used them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants