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

Make Bokeh objects always appear in overlay order #1968

Open
jbednar opened this issue Oct 5, 2017 · 46 comments
Open

Make Bokeh objects always appear in overlay order #1968

jbednar opened this issue Oct 5, 2017 · 46 comments

Comments

@jbednar
Copy link
Member

jbednar commented Oct 5, 2017

The HoloViews * syntax for constructing overlays has a clear left-to-right interpretation. In an expression like e1 * e2 * e3, e1 is normally drawn first, then overlaid with e2, then with e3 on top.

However, at least with the Bokeh backend, this order is not always respected. E.g. GeoViews map tiles always appear underneath any plotted data, which sometimes is what you want (for a tile layer showing geographic context), but is often not what you want (for a tile layer showing geographic place names, which should not usually be obscured by data points).

I propose that we eliminate any cases where displayable items appear at any order other than that specified in the overlay expression, unless the user has explicitly manipulated a special parameter (e.g. modifying the "level" parameter of a Bokeh object to bump it up or down in draw order). Specifically, from what @philippjfr has described, it seems like the WMTS element in GeoViews should be declared to appear at glyph level by default, to match other HoloViews objects so that sorting will be determined by overlay order. But I've raised the issue here rather than in GeoViews because it applies to all displayable items in both HoloViews and GeoViews; in my opinion if any of them (even Annotations) appear at some different order than specifed in the overlay, it's a bug in HoloViews according to the semantics of Overlay.

Does that sound correct to everyone?

@jlstevens
Copy link
Contributor

I agree the order of overlay should be respected everywhere if possible though there are some issues to think about:

  • Is there any valid reason for annotations not to always appear on the top?
  • I believe Bokeh doesn't have a completely straightforward z-ordering model like matplotlib does: as I understand it 'renderers' get ordered.

Other than these two caveats, I agree that the overlay ordering should be the z-ordering whenever possible.

@jbednar
Copy link
Member Author

jbednar commented Oct 5, 2017

Is there any valid reason for annotations not to always appear on the top?

Sure. Just because we call them annotations doesn't mean that they will always be used as such. E.g. someone could use splines, circles, etc. to draw some crazy grid lines:

image

and want their data to go on top of that, as it's background information, not plot elements. I don't think it's up to us to decide whether that's useful; it's much simpler just to layer things in order by default.

@jlstevens
Copy link
Contributor

I agree the utility of having annotations underneath other elements does vary. For instance, while I can imagine wanting a box/ellipse in the background, I can't think of any reason you would you ever want something to render over a text or arrow annotation...

@philippjfr
Copy link
Member

I'd suggest we make all elements default to the glyph render level but also expose the level as a plot/style option to ensure we have the flexibility in certain cases. The easiest thing would be to simply make it a style option.

@jbednar
Copy link
Member Author

jbednar commented Oct 6, 2017

That sounds perfect.

@jlstevens
Copy link
Contributor

I'm not sure that is ideal. Shouldn't HoloViews handle the z-order based on the overlay definition like @jbednar initially suggested? If that can work properly, I would rather do that than expose another plot/style option.

@jbednar
Copy link
Member Author

jbednar commented Oct 6, 2017

The proposal is to handle it based on the overlay definition, by default. That's the way we expect most people to use it. But it does seem useful to provide a style option that lets the user force some Element types to go on top or bottom, which could be very convenient for some workflows. Optional, but seems handy!

@jlstevens
Copy link
Contributor

I would rather keep the semantics clear if we can and introduce additional options only if we know they are necessary.

@jbednar
Copy link
Member Author

jbednar commented Oct 6, 2017

I don't think there is any semantic problem here. For each render level, Overlays are constructed in the order the Elements are specified, left to right. All Elements default to the glyph render_level, but if you want to force some Element types to be in front of the others, you can set that Element's render_level to annotation. If you want to force some Element types to be behind others, you can set the render_level to underlay. Seems clear to me, though I wouldn't cry if it were not available.

That said, aren't some of the render_levels special, in that they are not cropped to the viewport? If so having the ability to set the level explicitly will surely eventually be required, not just as a convenience.

@philippjfr
Copy link
Member

That said, aren't some of the render_levels special

That's correct, I think the overlay level lets you draw outside the bounds.

@jbednar
Copy link
Member Author

jbednar commented Oct 6, 2017

Drawing outside the bounds is a very important capability in some cases, and so I think the combined proposal (all at glyph level by default, style option to set explicit level) is appropriate.

That said, I think Jean-Luc may have already been exploiting the ability of Annotations to appear outside of the axis boundaries, e.g. to fake multi-line titles? If so, I guess there would be backwards compatibility implications of changing Annotations to use glyph level, as they would now require this style option? I'm personally ok with that, as I think the ability to draw outside of the axis boundaries is sometimes useful but can be very surprising and is probably more often a bug (e.g. when the position is calculated from a function), so I think I still come down in favor of glyph level for all by default.

@philippjfr
Copy link
Member

I doubt that a little bit, the Text element is at the glyph level already unless I'm completely mistaken.

@jbednar
Copy link
Member Author

jbednar commented Oct 6, 2017

I hope that's true, but I did see Jean-Luc make a plot a couple of days ago with items I think he said were hv.Text appearing outside the plot boundaries...

@philippjfr
Copy link
Member

philippjfr commented Oct 6, 2017

Doesn't work for me:

hv.Curve(range(10)) * hv.Text(0, 11, 'A')

@jbednar
Copy link
Member Author

jbednar commented Oct 6, 2017

Nor me:

image

@jlstevens?

@jlstevens
Copy link
Contributor

I hope that's true, but I did see Jean-Luc make a plot a couple of days ago with items I think he said were hv.Text appearing outside the plot boundaries...

The text wasn't outside the plot boundaries, it just had the y-axis disabled.

@jbednar
Copy link
Member Author

jbednar commented Oct 6, 2017

Ah! You fooled me. :-) Ok, then I stand by my proposal to have all Elements use glyph type consistently now.

@philippjfr
Copy link
Member

If we're actually worried about the semantics we could also just add a clip_bounds option (or similar) rather than providing full control over the render levels. I do think we should expose one of these options though.

@jbednar
Copy link
Member Author

jbednar commented Oct 6, 2017

I have no preference between those two alternatives, and support either one. They each have advantages; clip_bounds is a simpler, clearer concept, and meaningful outside of Bokeh, but render_level is more powerful and exposes more of the Bokeh machinery to those who want that. Either is fine by me.

@jlstevens
Copy link
Contributor

Whatever we decide, please let's make sure to keep improvements to the current behavior (i.e respecting the ordering as specified in the overlay) separate from any new plot options. We all agree with the former so such a PR won't need much discussion whereas new plot options will need more thought.

@philippjfr
Copy link
Member

I'm not actually aware of any elements in holoviews itself that are not at the glyph level at this point.

@jlstevens
Copy link
Contributor

Ok, so the original issues really only applies to geoviews.

@philippjfr
Copy link
Member

Appears what I said is not quite true, I noticed Bars are generally on top of other elements. That should be fixed.

@Dr-Irv
Copy link
Contributor

Dr-Irv commented Jan 17, 2018

I'm just learning HoloViews and doing the tutorial from pyviz. I've noticed the issue with bars (and area) being on top. For example, in the notebook https://github.com/pyviz/pyviz/blob/master/notebooks/02_Annotating_Data.ipynb , if you do the exercise in the middle that is listed as # Exercise: Make an overlay of the Spikes object from layout on top of the filled trajectory area of labelled_layout, and then try something like hv.Area(trajectory) * hv.Spikes(trajectory), the spikes don't show.

But you can make the spikes show by using the fill_alpha=0.5 option for Area.

So now the question is whether it is the responsibility of the user to specify the alpha/transparency values correctly when doing overlays, rather than assuming some particular drawing order.

Being VERY new to all of this, I may be totally misunderstanding things, but I thought I would add my observation.

@philippjfr
Copy link
Member

philippjfr commented Jan 18, 2018

No your intuition is right here, something is off about the Area zorder in this example. This should however already be resolved by this recent fix.

@jbednar jbednar added this to the v1.10.x milestone May 18, 2018
@jbednar
Copy link
Member Author

jbednar commented May 24, 2018

@philippjfr, how can we change the default level of tile sources to 'glyph', and find out if anything other than tile sources has an inappropriate level? I can see references to 'level' in TilePlot._init_glyph, but it's not setting the value to anything by default. Presumably it's inherited from Bokeh, so I can't tell (a) how to change it by default and (b) how to detect what else might inherit a non-glyph default.

@philippjfr
Copy link
Member

Presumably it's inherited from Bokeh, so I can't tell (a) how to change it by default and (b) how to detect what else might inherit a non-glyph default.

Two options, we can either set level as a default style option on WMTS, or we could have WMTSPlot override the bokeh default if no custom value is supplied. I've never quite worked out which I prefer.

@philippjfr philippjfr added this to the v1.11.x milestone Dec 27, 2018
@philippjfr philippjfr modified the milestones: v1.11.x, v1.12.0 Mar 22, 2019
@philippjfr philippjfr modified the milestones: v1.12.0, v1.12.x Apr 22, 2019
@philippjfr philippjfr modified the milestones: v1.12.x, v1.13.0 Jan 5, 2020
@tzuni
Copy link

tzuni commented Feb 12, 2020

I just ran into possibly a related issue but with the Matplotlib backend. When you make an overlay with more than three curves and then a horizontal or vertical line the fourth curve and beyond are drawn on top of the HLine or VLine thus breaking the layout order.

An example:

import numpy as np
import holoviews as hv
hv.extension('matplotlib')

one = hv.Curve([(0.1*i, np.sin(0.1*i)) for i in range(100)])
two = hv.Curve([(0.1*i, np.sin(0.1*i+0.75)) for i in range(100)])
three = hv.Curve([(0.1*i, np.sin(0.1*i+1.5)) for i in range(100)])
four = hv.Curve([(0.1*i, np.sin(0.1*i+2.25)) for i in range(100)])
five = hv.Curve([(0.1*i, np.sin(0.1*i+3.0)) for i in range(100)])
vline = hv.VLine(4.6).opts(color='black')
hline = hv.HLine(0.0).opts(color='purple')
(one * two * three * four * five * vline * hline).opts(
    hv.opts.Curve(linewidth=5),
    hv.opts.VLine(linewidth=5),
    hv.opts.HLine(linewidth=5))

Curves one, two, and three are drawn below the hline and vline but curves four and `five are drawn over them.

I'm working in linux on Python 3.7.5 with these versions:
holoviews==1.12.7
matplotlib==3.1.2

@philippjfr philippjfr modified the milestones: v1.13.0, v1.13.x Mar 3, 2020
@brunorpinho
Copy link

brunorpinho commented Nov 25, 2020

Just in case anyone else is confused.

Using the bokeh backend I was having an issue of a slope line that was supposed to be plotted on top of a datashaded scatterplot being plotted under it, so I couldn't see it.

I solved it by passing the level to the slope opts doing:

hv.Slope(1, 0).opts(level='overlay') * datashade_image

image

More info on level

@kitaev-chen
Copy link

I solved it by passing the level to the slope opts doing:

hv.Slope(1, 0).opts(level='overlay') * datashade_image

errorbars = hv.ErrorBars(pd.concat([x, y_mean, y_std], axis=1)).opts(level='overlay')

ValueError: Unexpected option 'level' for ErrorBars type across all extensions. No similar options found.

... ...

I have to say, holoviews is super unfriendly to the people who know little about the various parameters/opts of boken/matplot etc. Usually it will cost a lot of time to find the setting for a very simple function

@jbednar
Copy link
Member Author

jbednar commented May 3, 2021

@brunorpinho: Using the bokeh backend I was having an issue of a slope line that was supposed to be plotted on top of a datashaded scatterplot being plotted under it, so I couldn't see it.
I solved it by passing the level to the slope opts doing:
hv.Slope(1, 0).opts(level='overlay') * datashade_image

Here the problem wasn't the level, but the ordering. The HoloViews way of fixing the ordering is not by trying to mess with the level, but simply by putting items in the order you want them to overlay, left to right (bottom to top in the overlay):

datashade_image * hv.Slope(1, 0)

@kitaev-chen, maybe the Bokeh ErrorBars element doesn't support a level option, but that shouldn't matter if so, because the correct way to handle overlays in HoloViews in general is simply to order them left to right.

I agree that using backend-specific options (when they are needed!) could be a lot easier; see my proposal in #1820. We only lack time to implement it! If anyone has time and expertise (or money so we can hire someone with time and expertise) to do this, please let me know!

@kitaev-chen
Copy link

@kitaev-chen, maybe the Bokeh ErrorBars element doesn't support a level option, but that shouldn't matter if so, because the correct way to handle overlays in HoloViews in general is simply to order them left to right.

I agree that using backend-specific options (when they are needed!) could be a lot easier; see my proposal in #1820. We only lack time to implement it! If anyone has time and expertise (or money so we can hire someone with time and expertise) to do this, please let me know!

That's what bothers me a whole day. You can see the ErrorBars works, but when composing with bar chart, half of the errorbars was blocked no matter what order.

fig = errorbars
image

fig = bars * errorbars
image

@jlstevens
Copy link
Contributor

With bars * errorbars, the error bars should definitely be on top! Not sure what is going on in that screenshot but is looks like a bug to me. @philippjfr ?

I have to say, holoviews is super unfriendly to the people who know little about the various parameters/opts of bokeh/matplot etc. Usually it will cost a lot of time to find the setting for a very simple function

In the FAQ, see the Q: Why don’t you let me pass matplotlib_option as a style through to matplotlib? question which also applies to Bokeh. You should be able to make level available if you really want to, but as @jbednar says, the order of * should be respected (though not in your screenshot for some reason!).

@kitaev-chen
Copy link

@jlstevens

fig = bars * errorbars is the return of the single_fig function, then I did:

fig_dict_2D = OrderedDict({(rval, cval):single_fig(...) for rval in rvals for cval in cvals})
grid = hv.GridSpace(fig_dict_2D, kdims=[rlabel, clabel], sort=False)
ndlayout = hv.NdLayout(grid, kdims=[rlabel, clabel], sort=False)
nd_fig = ndlayout.opts(opts.Bars(width=250, height=200)).cols(n_cols)
show(hv.render(nd_fig))

But I try to show the single plot, the errorbars is still blocked by bar chart


bars = hv.Bars(
        df_draw, [xft], [yft1]).opts(
            opts.Bars(
                ylim=(0, 1.2),
                bar_width=0.5, 
                color=xft,
                show_legend=False,
                xlabel = xlabel, 
                ylabel = ylabel1, 
                cmap=colors, 
                )).opts(
                    fontsize={
                        'xticks': 7, 
                    },
                )

    errorbars = hv.ErrorBars(
        pd.concat([x, y_mean, y_std], axis=1)
        )#.opts(level='overlay')

fig = bars * errorbars

@kitaev-chen
Copy link

In the FAQ, see the Q: Why don’t you let me pass matplotlib_option as a style through to matplotlib? question which also applies to Bokeh. You should be able to make level available if you really want to, but as @jbednar says, the order of * should be respected (though not in your screenshot for some reason!).

Finally it works.

from holoviews import Store
Store.add_style_opts(hv.Bars, ['level'], backend='bokeh')

hv.Bars(...)
.opts(
                    fontsize={
                        'xticks': 7, 
                    },
                    level='image',
                )

Many thanks! You save me lots of time!

@brunorpinho
Copy link

@jbednar What is the purpose of level? I'm asking because I use it all the time for ordering, haha

@jbednar
Copy link
Member Author

jbednar commented May 4, 2021

Bokeh is independent of HoloViews and uses levels for its own purposes, and I don't think there's any compelling reason to set the level in HoloViews except to fix cases where the level was set incorrectly internally (which is the problem this issue is about). Named levels like Bokeh has only give you a very rough control over ordering, and you have to remember what order the names go in, which seems much less clear than just putting the expression in the order you want. I'd really recommend that people only mess with levels if they find a bug where the HoloViews left to right (bottom to top) ordering is not being respected.

@brunorpinho
Copy link

brunorpinho commented May 4, 2021

Bokeh is independent of HoloViews and uses levels for its own purposes, and I don't think there's any compelling reason to set the level in HoloViews except to fix cases where the level was set incorrectly internally (which is the problem this issue is about). Named levels like Bokeh has only give you a very rough control over ordering, and you have to remember what order the names go in, which seems much less clear than just putting the expression in the order you want. I'd really recommend that people only mess with levels if they find a bug where the HoloViews left to right (bottom to top) ordering is not being respected.

Got it! Thanks

@jbednar
Copy link
Member Author

jbednar commented Apr 11, 2022

Here's another case where the ordering has to be overridden with level; not sure why:

import panel as pn, hvplot.pandas, holoviews as hv, pandas as pd, colorcet as cc

df = pd.DataFrame(dict(x=[6E6,5E6,4E6,3E6,2E6,1E6], 
                       y=[2E6,1E6,3E6,4E6,0E6,5E6], val=[7,8,9,7,8,9]))
dfi = df.interactive

map_tiles = hv.element.tiles.EsriImagery().opts(level='underlay')

value = pn.widgets.Select(options=list(df.val.unique()), name='Value')

map_tiles * dfi[dfi.val==value].hvplot('x','y', kind='scatter')

(the scatter points are obscured by the map if .opts(level='underlay') is omitted). I strongly vote for all Bokeh elements to use the same level to avoid this problem!!!!!!!!!

@philippjfr
Copy link
Member

That one is pretty bizarre, don't know how that would even happen.

@jbednar
Copy link
Member Author

jbednar commented Apr 12, 2022

Bad things happen when we let there be multiple levels anywhere in the system. :-)

@TheoMathurin
Copy link
Contributor

TheoMathurin commented Jun 8, 2023

+1 on this one

It seems the issue with Errorbars is not limited to overlays with hv.Bars, other elements like hv.Curve are also affected.

errors = [(0.1*i, np.sin(0.1*i), np.random.rand()/2) for i in np.linspace(0, 100, 11)]
hv.Curve(errors).opts(line_width=5) * hv.ErrorBars(errors)

bokeh_plot

Solved it using hv.Store.add_style_opts(hv.ErrorBars, ['level'], backend='bokeh') + hv.ErrorBars(...).opts(level='overlay')

@G-Guillard
Copy link

G-Guillard commented Jan 5, 2024

hv.Slope also ignores natural ordering for overlay. It has already be mentioned by @brunorpinho in combination with a Datashader plot, but can be generalized with other kinds of plots, and from my experience (hv 1.17.1, bokeh 3.1.1) the slope is usually above the other plot(s).

(Using level="underlay" as suggested above does the trick.)

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

9 participants