plot icon indicating copy to clipboard operation
plot copied to clipboard

Set plot area

Open bennn opened this issue 10 years ago • 15 comments

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)

bennn avatar Jul 09 '15 00:07 bennn

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.

gus-massa avatar Sep 27 '19 02:09 gus-massa

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

spdegabrielle avatar Jun 18 '20 09:06 spdegabrielle

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.)

gus-massa avatar Jun 18 '20 11:06 gus-massa

@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

spdegabrielle avatar Jun 19 '20 15:06 spdegabrielle

The margins for the plot area are calculated here

https://github.com/racket/plot/blob/aacfb3739cf9f5246944b612e17e8c9b7db91929/plot-lib/plot/private/plot2d/plot-area.rkt#L537

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):

https://github.com/racket/plot/blob/aacfb3739cf9f5246944b612e17e8c9b7db91929/plot-lib/plot/private/plot2d/plot-area.rkt#L515

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.

alex-hhh avatar Jun 25 '20 22:06 alex-hhh

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.

rfindler avatar Jun 25 '20 23:06 rfindler

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.

alex-hhh avatar Jun 26 '20 02:06 alex-hhh

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).

mfelleisen avatar Jun 26 '20 12:06 mfelleisen

Den fre. 26. jun. 2020 kl. 04.49 skrev Alex Harsányi < [email protected]>:

OK, lets look at the other end of this problem. How should the interface look like? 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.

An additional, related issue: Plot returns a pict without further information.

If I want to place additional points, labels, arrows etc using MetaPict or latex-pict there is no way of knowing, how pict coordinates maps to the logical coordinates that plot used to place the plotted objects. Due margins, legends and axes there is no way of figuring out how the logical coordinate system is places on the pict.

A way of getting the placement of the logical coordinate system relative to the pict, would be a great feature.

/Jens Axel

soegaard avatar Jun 26 '20 13:06 soegaard

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 avatar Jun 26 '20 13:06 rfindler

(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.)

rfindler avatar Jun 26 '20 13:06 rfindler

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.

gus-massa avatar Jun 26 '20 14:06 gus-massa

@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

alex-hhh avatar Jun 26 '20 23:06 alex-hhh

Den lør. 27. jun. 2020 kl. 01.07 skrev Alex Harsányi < [email protected]>:

@soegaard https://github.com/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 https://github.com/racket/plot/issues/58

Thanks for clarifying.

Overview: The overall goal (for me) is to make decorations/annotations without using plot. The decoration is rendered as a pict, and then superimposed on the pict returned from plot.

To make things (hopefully) clearer, let me give an example without metapict and let's assume the mapping from view coordinates to device coordinates is affine.

Let's say I have a plot p returned from plot as a pict. Now I want to draw on top of that pict (i.e. decorate it) with the standard tools. In particular I want to draw a shape that starts in one point and ends in another.

The way I want to do this is: 1. make a new pict d with a certain size 2. draw the shape 3. use a variant of super-impose to place d on top of p.

Thinking aloud, the details are:

ad 1) The sizes of p and d do not need to be the same, but the scaling from plot coordinates into device coordinates need to be the same.

Useful information: xmin, xmax, ymin, ymax in device coordinates of the plotting area used by plot xmin, xmax, ymin, ymax in plot coordinates

Given these I can determine how large the decoration pict d needs to be and get the scaling right.

ad 2) I can use the information from 1) to set up a dc transformation that maps plot coordinates into device coordinates of d. Then the shape can be drawn.

ad 3) Aligning both start and end point of the shape needs a mapping from plot coordinates to device coordinates of p. The information from 1) is enough. Note: The existing point-pict is not enough; it can only align one of the points.

In summary: A way to get the plot window in both device and plot coordinates is enough for external tools to draw on top of plots produced with plot.

One way to extend plot in a compatible way would be to add a keyword signaling that the windows are needed and then let plot return some extra values.

If non-linear axes are used, then the view transformation becomes important. I need to think some more about handling that. Maybe returning functions mapping to and from view coordinates is enough?

/Jens Axel

soegaard avatar Jun 27 '20 09:06 soegaard

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.

ralsei avatar May 23 '21 22:05 ralsei