racket / plot

Other
39 stars 37 forks source link

legend outside the plot area (return legend as pict?) #49

Open bennn opened 5 years ago

bennn commented 5 years ago

Currently, plot draws legends on the plot area.

It would be nice if plotting functions could return the plot and legend as two picts that could be combined using a pict combiner or progressive pict.

(edit) see also:

alex-hhh commented 4 years ago

These are some notes for adding support to draw the plot legend outside the plot area. Doing this is surprisingly difficult, as the notes below show.

Define new legend anchor types

The location of the plot legend is controlled by the plot-legend-anchor parameter, which is of type Anchor, defined here: https://github.com/racket/plot/blob/4df34897acba74f689954188183e367c160911b8/plot-lib/plot/private/common/types.rkt#L15

We need to add 'outer-top-left, 'outer-left, 'outer-bottom-left, 'outer-top-right, 'outer-right and 'outer-bottom-right cases to this type.

NOTE unfortunately, this type is also used as an anchor to the labels on the plot and the new types would be invalid values for use as plot labels. Also the anchor type already contains 'auto, which is used by the plot labels (meaning to place the label such that it fits inside the plot), but has no meaning for legend entries. Perhaps we should separate the types and use Anchor for labels and a new type LegendAnchor for plot legends.

Separate the legend size calculation from the drawing

https://github.com/racket/plot/blob/4df34897acba74f689954188183e367c160911b8/plot-lib/plot/private/common/plot-device.rkt#L613

The legend is actually drawn in plot-device%/draw-legend, this function both calculates the size of the legend and draws it. It receives the legend entries, plus the plot area in plot coordinates (since, as of now, the legend is drawn inside the plot area).

We need at least a function which calculates the legend-x-size and legend-y-size separately.

Also this function will need to be updated to recognize the new legend anchors and adjust the position of the legend accordingly.

Make room for the legend when it is outside

When the legend is outside the plot, space will need to be reserved for it. This is done here (there is an equivalent place for 3D plots): https://github.com/racket/plot/blob/4df34897acba74f689954188183e367c160911b8/plot-lib/plot/private/plot2d/plot-area.rkt#L554

The get-param-vs/set-view->dc! will need to be updated to add an entry for the legend, if the legend is outside. The function returns a list of entries for margin-fixpoint to use. Each entry is in the format:

(list label (vector x y) Anchor Angle)

The x, y are the location of the label in DC coordinates, label is currently either a string or a picture (something on which margin-fixpoint can calculate width and height), Anchor represents the position of the label relative to the (x,y) point and Angle is the direction of the label. For examples on how these entries are constructed, see get-{x,y,z}-label-params in the same file.

We will need to construct such an entry for the legend, if the legend is to be rendered outside the plot area. Unfortunately, at this stage, we don't know about the legend yet!

Note that the Anchor passed here would be a different anchor than the legend anchor supplied by the user: it refers to the position on the legend where the x,y point will be.

For example: for a 'outer-top-left legend anchor: x, y will be the position of the top-left top area (view->dc 0 1), minus a gap, minus the space for the plot ticks, minus the space for the y axis label. The anchor will than be 'top-right, as this is the point on which the legend will be attached to this point.

Inform 2d-plot-area% about the legend early on.

The plot-area% object does not know about the legend, until it is asked to draw it. The plot-area function (see link below) will receive a 2d-plot-area% instance (which already has all its sizes calculated), draw the renderers on it, than draw the legend as the last step (this is when the plot area first sees the legend):

https://github.com/racket/plot/blob/8dcfd7745e2595b8d517fd8cd7c59510efab84a9/plot-lib/plot/private/no-gui/plot2d-utils.rkt#L68

The plot-area function already receives a 2d-plot-area% object will all the layout calculated, so it is already too late to inform it about the legend. The 2d-plot-area% object is created in several places, and the legend will need to be passed here, so it can be used for size calculations:

https://github.com/racket/plot/blob/18d8ecad480776ded740a170df06862f0e39e7c8/plot-lib/plot/private/no-gui/plot2d.rkt#L40

https://github.com/racket/plot/blob/18d8ecad480776ded740a170df06862f0e39e7c8/plot-gui-lib/plot/private/gui/plot2d.rkt#L116

https://github.com/racket/plot/blob/58b5d7e44a387d1d612b2450e6dc999380653cd5/plot-gui-lib/plot/private/gui/snip2d.rkt#L258

Same changes for 3D plots

While the drawing of the plot legend is handled in common, there is a different, 3d-plot-area% object and it will need to be updated in the same way as the 2d-plot-area% object.

bdeket commented 4 years ago

In order to get to the legend entries early, the (2D-)Render-Proc's will need to be called early. We can do this

personally I prefer option 3 since that makes it clear that two separate functionalities need to be provided. If we go with 2 we need to make sure that new render functions don't forget to send the label for the empty bounds.

bdeket commented 3 years ago

TODO list

alex-hhh commented 3 years ago

Thinks to do after completing this task:

alex-hhh commented 3 years ago

Hi @bdeket, I had a brief look at one of the images from the plot tests and didn't look at the code changes yet, but I would like to make sure we both have the same understanding of where to place the legend outside the plot area. Basically, the test image pr70-2.png is not what I had in mind for the "outside bottom right" location.

The way I view it, there are 12 reasonable location for placing this legend outside the plot area (see image below). Originally, I thought to only provide configuration via LegendAnchor to positions A, B, C, G, H and I, but after discussing this with you, I believe that at least some positions at the top and bottom of the area make sense (especially if we implement horizontal legend layout).

plot-area

Alignment

The main difference is that I think the legends should be aligned with the plot area even when they are outside. For example, for position A, the top of the legend is aligned with the top of the plot area (and not with the top of the draw area) and position G is aligned with the bottom of the plot area.

Naming

The other question is what to name these locations, because (outside top-left) could be both positions A and L.

Overflow handling

For the inside of the plot, when there are more legend entries than would fit in the plot area, they are clipped -- we want to make sure that the same things happen when the legend is outside. For example, when calculating the required space for the legend in positions A, B, C, G, H, I, only the width should be considered, not the height and I am not sure about the top and bottom locations.

bdeket commented 3 years ago

Hi @alex-hhh,

Also, I think this only applies to 2D, no?

alex-hhh commented 3 years ago

I updated the title of the issue to reflect the changes that were done. The original requests on the racket-users group were for placing the legend outside the plot area, and this is what was implemented. The "return legend as a pict" was only a possible solution for that problem, and one which did not take interactive plot snips into account.

While returning the legend as a pict is possible (see #73), I am not convinced that it would be a useful functionality, and will only consider it if someone provides a reasonable use case for it.

bennn commented 3 years ago

If I'm putting similar plots on the same page, I like to have one legend off to the side. With a pict, I get a default legend that can be put anywhere.

p7 here has a hand-made solution https://www2.ccs.neu.edu/racket/pubs/popl16-tfgnvf.pdf

alex-hhh commented 3 years ago

I don't see how returning the legend as a pict would have helped you since the legend produced by the plot package is not in the format you used for the plots in the linked paper.

Yes, there are valid reasons to have the legend separate from the actual plot, but such a legend can already be created with the pict package as you have demonstrated in that paper and I have also shown in the linked google groups post. The pict package offers additional flexibility as well.


On a technical note, there are some interface problems with #73, which would make such a function difficult to use in most of the scenarios where it would actually be needed:

Perhaps when we implement multiple "linked" plots as part of #7, we might be able to provide a legend which is consistent across several plots, and provide an interface which produces several plots which line up plus a legend. Until than, users can create their own legends using the pict package, if they need the legend separate from the actual plot.