Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
bennn opened this issue Nov 9, 2018 · 9 comments
Open

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

bennn opened this issue Nov 9, 2018 · 9 comments

Comments

@bennn
Copy link
Contributor

bennn commented Nov 9, 2018

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
Copy link
Collaborator

alex-hhh commented 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-anchor parameter, which is of type Anchor, defined here:

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

(define/public (draw-legend legend-entries rect)

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

(let-values ([(left-val right-val top-val bottom-val)

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

(define (plot-area area renderer-list)

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:

(define (plot/dc renderer-tree dc x y width height

(make-object 2d-plot-area%

(make-object 2d-plot-area%

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
Copy link
Contributor

bdeket commented 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

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

@bdeket
Copy link
Contributor

bdeket commented Sep 10, 2020

TODO list

alex-hhh pushed a commit that referenced this issue Sep 13, 2020
The various plot renderer procedures for 2D and 3D plots used to render their
data than return a legend entry to be used for constructing the legend.  This
means that the plot library can only know about the list of legend entries
after the data is drawn on the plot.

The `renderer2d` and `renderer3d` structures have been updated to add a slot
for the legend entry and all renders have been updated so the legend entry is
available without rendering the actual data.

This was done so that legend entries can be drawn outside of the plot area,
see #49 for all the commits related to that functionality.
alex-hhh pushed a commit that referenced this issue Sep 14, 2020
The code which draws the plot legend used to calculate the legend dimensions
and draw the legend in a single method of the `plot-device%` class.  This has
been separated in two: a method to calculate the legend dimensions and one to
actually draw the legend.

This is done in order to be able to place the plot legend outside the plot
area, as part of #49
@alex-hhh
Copy link
Collaborator

alex-hhh commented Sep 15, 2020

Thinks to do after completing this task:

  • 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)
  • add a horizontal layout mode for the plot legend
  • Avoid starting the plot render when there are no entries, see discussion on [Legend pt3] Make legend available to area at start #72 -- this will require updating some test data.
  • 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.

alex-hhh pushed a commit that referenced this issue Sep 15, 2020
In order to be able to calculate the space for legend entries when the legend
is outside the plot area, the plot area object (for 2D and 3D plots) needs to
have a access to the legend entries.

This commit also introduces a new tyle `legend-anchor/c` and `LegendAnchor`
for the legend anchor values.

Anchoring the legend outside the plot is possible, but that functionality is
not yet complete.
This was referenced Sep 16, 2020
@alex-hhh
Copy link
Collaborator

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
Copy link
Contributor

bdeket commented Sep 16, 2020

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?

alex-hhh pushed a commit that referenced this issue Sep 21, 2020
New legend plot locations for placing the legend outside the plot area, plus a
`'no-legend` option for not drawing the legend at all.
alex-hhh pushed a commit that referenced this issue Sep 22, 2020
Document the new locations where the plot legend can be placed.
@alex-hhh alex-hhh changed the title return legend as pict? legend outside the plot area (return legend as pict?) Sep 23, 2020
@alex-hhh
Copy link
Collaborator

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
Copy link
Contributor Author

bennn commented 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

@alex-hhh
Copy link
Collaborator

alex-hhh commented 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:

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

alex-hhh pushed a commit that referenced this issue Sep 24, 2020
When the legend is placed outside the plot, the message used to be in the center of the entire 
plot image, which didn't look correct.
alex-hhh added a commit to alex-hhh/plot that referenced this issue Sep 25, 2020
The 2d-plot-area% for the overlays is only created if there are any overlays
present and no unit tests were exercising that code path, so a unit test was
also added to ensure that this plot area is not forgotten about.
alex-hhh added a commit to alex-hhh/plot that referenced this issue Sep 25, 2020
Add test cases for all possible cases for outside legend placement for both 2D
and 3D plots.
alex-hhh pushed a commit that referenced this issue Sep 30, 2020
A new parameter, `plot-legend-layout`, allows placing legend entries into
multiple rows or columns.  This allows, for example, using a horizontal layout
when the legend is placed at the top or bottom of the plot.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants