Open bennn opened 5 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.
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.
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.
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.
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):
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:
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
Render-Proc
with 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 discardRender-Proc
with 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 the get-bounds-rect
function)Render-Proc
into 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
renderer2d
and 3d have an extra struct field label
#70 plot-device%
has an extra method calculate-legend-rect
#71 Legend-Anchor
is (U #f Anchor (List (U 'inside 'outside) Anchor))
#72 plot-area%
takes an extra argument legend
#72 Thinks to do after completing this task:
'auto
entry for anchor/c
, see discussion on #72 -- this is already documented in contracts.scrbl, but in a peculiar way: the Anchor TR type contains 'auto
, but the anchor/c
contract does not. This means that auto cannot be used as a legend anchor (as this is protected by the contract), but can be used for point-label
, whose #:label
parameter is protected by a contract generated from TR (TR = Typed Racket)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).
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.
The other question is what to name these locations, because (outside top-left)
could be both positions A and L.
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,
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:
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.
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: