You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
legend outside the plot area (return legend as pict?) #49
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
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):
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):
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
by calling the 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 discard
by calling the Render-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)
Changing the signature for 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.
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
Document the 'auto entry for anchor/c, see discussion on [Legend pt3] Make legend available to area at start #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)
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.
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.
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.
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.
Activity
alex-hhh commentedon Sep 4, 2020
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-anchorparameter, which is of typeAnchor, defined here:plot/plot-lib/plot/private/common/types.rkt
Line 15 in 4df3489
We need to add
'outer-top-left,'outer-left,'outer-bottom-left,'outer-top-right,'outer-rightand'outer-bottom-rightcases 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 useAnchorfor labels and a new typeLegendAnchorfor plot legends.Separate the legend size calculation from the drawing
plot/plot-lib/plot/private/common/plot-device.rkt
Line 613 in 4df3489
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-sizeandlegend-y-sizeseparately.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):
plot/plot-lib/plot/private/plot2d/plot-area.rkt
Line 554 in 4df3489
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 formargin-fixpointto use. Each entry is in the format:The
x,yare the location of the label in DC coordinates,labelis currently either a string or a picture (something on whichmargin-fixpointcan calculate width and height),Anchorrepresents 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, seeget-{x,y,z}-label-paramsin 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
Anchorpassed 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-leftlegend 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. Theplot-areafunction (see link below) will receive a2d-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):plot/plot-lib/plot/private/no-gui/plot2d-utils.rkt
Line 68 in 8dcfd77
The
plot-areafunction already receives a2d-plot-area%object will all the layout calculated, so it is already too late to inform it about the legend. The2d-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:plot/plot-lib/plot/private/no-gui/plot2d.rkt
Line 40 in 18d8eca
plot/plot-gui-lib/plot/private/gui/plot2d.rkt
Line 116 in 18d8eca
plot/plot-gui-lib/plot/private/gui/snip2d.rkt
Line 258 in 58b5d7e
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 the2d-plot-area%object.bdeket commentedon Sep 8, 2020
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-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 discardRender-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)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.
bdeket commentedon Sep 10, 2020
TODO list
renderer2dand 3d have an extra struct fieldlabel[legend pt1] legend outside plot area #70plot-device%has an extra methodcalculate-legend-rect[legend pt2] separate legend size calculation and drawing functionality #71Legend-Anchoris(U #f Anchor (List (U 'inside 'outside) Anchor))[Legend pt3] Make legend available to area at start #72plot-area%takes an extra argumentlegend[Legend pt3] Make legend available to area at start #72Separate legend entry creation from plot rendering (#70, #49)
Separate size calculations from drawing the plot legend (#71, #49)
alex-hhh commentedon Sep 15, 2020
Thinks to do after completing this task:
'autoentry foranchor/c, see discussion on [Legend pt3] Make legend available to area at start #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)add a test forplot-legend-as-pict, this requires extending the test framework for picts, but the same technique can be used.Pass the legend entries to the plot area object constructor (#72, #49)
alex-hhh commentedon Sep 16, 2020
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
LegendAnchorto 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.
bdeket commentedon Sep 16, 2020
Hi @alex-hhh,
Also, I think this only applies to 2D, no?
New legend placement locations (#74, #49)
Update scribble documentation for legend-anchor/c (#77, #49)
[-]return legend as pict?[/-][+]legend outside the plot area (return legend as pict?)[/+]alex-hhh commentedon Sep 23, 2020
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 commentedon Sep 23, 2020
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
Don't call `start-renderer` when there are no renderers (#79, #49)
alex-hhh commentedon Sep 24, 2020
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.
3dsnip: center the interactive message on the plot area (#80, #49)
Fix initialization of 2d-plot-area% when drawing overlays (racket#49)
Add automated tests for outside legend placement (racket#49)
Multi-line, multi-column legend layout (#81, #49)