plot
plot copied to clipboard
legend outside the plot area (return legend as pict?)
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:
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.
In order to get to the legend entries early, the (2D-)Render-Proc's will need to be called early.
We can do this
- by calling the
Render-Procwith a fake plot area: this will mean that for expensive functions a lot of time will be invested in drawing something that we are going to discard - by calling the
Render-Procwith bounds(Rect (ivl #f #f) (ivl #f #f)): all the render procs will need to be updated to accept this as a valid region and only send back the legend entry. Currently most functions either error or send back an empty list as legend entry in this case. (This option does not mean that we accept this as sensible bounds, this is still checked elsewhere in theget-bounds-rectfunction) - Changing the signature for
Render-Procinto a struct or similar of(-> Area Void)and(-> (Treeof legend-entry))
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.
TODO list
- [x] Separate legend and renderer :
renderer2dand 3d have an extra struct fieldlabel#70 - [x] Separate the legend size calculation from the drawing :
plot-device%has an extra methodcalculate-legend-rect#71 - [x] Define new legend anchor types :
Legend-Anchoris(U #f Anchor (List (U 'inside 'outside) Anchor))#72 - [x] Inform plot-area% about the legend early on :
plot-area%takes an extra argumentlegend#72 - [x] Plot and legend in seperate picts #73
- [x] Make room for the legend when it is outside : outside currently places the legend as lowest element outside the plot-area, but other elements will be drawn on top of it, #f will not draw the legend at all
- [x] Update docs
- [x] Add new tests
Thinks to do after completing this task:
- [x] Document the
'autoentry foranchor/c, see discussion on #72 -- this is already documented in contracts.scrbl, but in a peculiar way: the Anchor TR type contains'auto, but theanchor/ccontract does not. This means that auto cannot be used as a legend anchor (as this is protected by the contract), but can be used forpoint-label, whose#:labelparameter is protected by a contract generated from TR (TR = Typed Racket) - [x] add a horizontal layout mode for the plot legend
- [x] Avoid starting the plot render when there are no entries, see discussion on #72 -- this will require updating some test data.
- [x] center plot3d-snip overlay messages on the plot area
- [ ] ~~add a test for
plot-legend-as-pict, this requires extending the test framework for picts, but the same technique can be used.~~
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).

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.
Hi @alex-hhh,
- Alignment with plot boundaries: I think I know what to change, and it should be fairly straightforward.
- A vs L: my current implementation chooses based on the width/height of the legend. Trying to maximize plot area
- Overflow: Current clipping is at the width/height of the dc( minus title) I think. Do you prefer clipping at plot area width/height? With large ticks the area can become very small, so I prefer to leave it like it is.
Also, I think this only applies to 2D, no?
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.
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
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:
- the x-min, x-max, y-min, y-max would have to be provided to the plot legend (but only sometimes), even though no plot is actually drawn -- this can be confusing for plot users
- you have to provide the entire renderer tree to the legend pict function -- in a multi-plot scenario, such as your paper, you would have to supply only one of the plots and make sure that the remaining plots use the same line style and colors for their functions, and the plot package would not be able to detect and flag the inconsistency.
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.