holoviz / hvplot

A high-level plotting API for pandas, dask, xarray, and networkx built on HoloViews
https://hvplot.holoviz.org
BSD 3-Clause "New" or "Revised" License
1.13k stars 109 forks source link

Cannot fix aspect ratio and remove padding simultaneously #931

Open TomNicholas opened 2 years ago

TomNicholas commented 2 years ago

I am trying to plot an image using the xarray accessor, but can't seem to be able to specify the aspect ratio and also remove padding simultaneously.

Demonstration:

import xarray as xr
import hvplot.xarray  # noqa

air_ds = xr.tutorial.open_dataset('air_temperature').load()
air2d = air_ds.air.sel(time='2013-06-01 12:00')

default behaviour

air2d.hvplot.image()

bokeh_plot

air2d.hvplot.image().opts(data_aspect=True)

bokeh_plot (1)

air2d.hvplot.image(padding=0)

bokeh_plot (2)

air2d.hvplot.image(padding=0).opts(data_aspect=True)

bokeh_plot (3)

I would have expected the last figure not to have any padding at the sides.

This shows that I can't currently choose my axis to have the same aspect ratio as the data, and also remove all padding simultaneously. I don't know whether this is a bokeh problem or a hvplot problem, but I thought I would raise it here first.

I also tried setting xlim but that did not remove the padding either.

jbednar commented 2 years ago

In the last figure are you expecting the figure to get larger vertically? I have some dim recollection of hvplot having set height and width explicitly that then override such expansion, making it an hvplot rather than bokeh issue.

TomNicholas commented 2 years ago

Hi Jim. No, what I expected / wanted was for the horizontal axis not to extend past the region of data. In this reproducible example the added whitespace is small, but in the actual plots I'm trying to make the same issue is creating huge amounts of whitespace.

bokeh_plot (4)

I'm making a dashboard with 3 plots like this side-by-side - if I can't remove all this whitespace there will be as much whitespace as plot!

hoxbro commented 2 years ago

Two things that might work:

1) Run air2d.hvplot.image(data_aspect=True) 2) Manipulate the frame_width / frame_height to match the ratio of data_aspect.

frame_height = 300
data_aspect = 1

ratio = (air2d.lon.max() - air2d.lon.min()).values /  (air2d.lat.max() - air2d.lat.min()).values
frame_width = int(ratio * frame_height / data_aspect)
air2d.hvplot.image().opts(data_aspect=data_aspect, frame_width=frame_width, frame_height=frame_height)

image

jbednar commented 2 years ago

I guess I'm not quite getting which dimension you want to control everything. E.g. for that divergence plot, it sounds like you want the height of the plot to remain the same, while the width changes dynamically. For other plots (e.g. with a different data aspect ratio), you might want the width to remain the same, but the height to change dynamically. I'm not sure we could make that choice in any automatic way. It certainly shouldn't be as difficult as in @Hoxbro 's approach, though!

TomNicholas commented 2 years ago

I guess I'm not quite getting which dimension you want to control everything.

That's a fair point.

It certainly shouldn't be as difficult as in @Hoxbro 's approach, though!

Yeah, this issue would be resolved if there was a simple way to match the frame width to the data width (which I what I expected padding=0 to do).

jbednar commented 2 years ago

padding=x simply adds extra space around whatever autoranging came up with; it doesn't (currently) have any other effect. So the current behavior is maybe extra_padding more than exactly_this_padding.

Here's a proposal to consider: When data_aspect=True and padding=0, we treat the supplied width and height as the maximum plot size. The actual size along one of the dimensions will then be shorter than one of those values unless the data_aspect precisely matches the width/height ratio. I.e., in that particular case we treat the width and height as a box in which the plot must fit, but still make the actual viewport cover only the area where there is data. I think I'd be happy with that behavior, personally; seems like the right way for dense data like images and rasters to behave, and padding is zero by default for those element types.

For sparsely drawn element types like Curve and Points where padding is not zero by default, adding additional padding as illustrated above is normally appropriate, since then the data aspect will only affect how much padding there is, not whether there is any.

I can't decide whether this proposal is too tricky, doing too much magic, but it does seem like it would give reasonable and mostly unsurprising behavior in the cases I've considered.

hoxbro commented 2 years ago

It certainly shouldn't be as difficult as in @Hoxbro 's approach, though!

I agree. My second approach is too complicated. But isn't the first proposal pretty straightforward, e.g. adding data_aspect directly in df.hvplot.image and not using opts?

jbednar commented 2 years ago

Oh! I didn't spot that in your response! Ah, this is an interaction between hvPlot and HoloViews, then. I think hvPlot sets the height and width when it hands things over to HoloViews, and once that's happened, HoloViews sees data_aspect, height, and width as all having been specified by the user, and thus tries to achieve all three instructions. When data_aspect is given to hvPlot directly, it can take effect before height and width are given values in hvPlot.

So it's great that there is a reasonable way to achieve the desired behavior, but it's not at all a good situation, because having data_aspect behave so differently depending on whether it's passed to hvPlot or HoloViews is very tricky and subtle. I think I've noticed similar issues with responsive and sizing_mode, i.e. things where it matters whether a height or width has been set explicitly by the user. Definitely a topic for someone who knows that interface between hvPlot and HoloViews better than I do to address!