Skip to content

[BUG] Behaviour of automated padding for ranges is inconsistent #9724

Closed

Description

@Tikonderoga

Exists in Bokeh from 0.13.0 till 1.4.0
Tested on Windows 10 and Ubunto 16.0, on Firefox and Chrome (most recent versions).

That's the weird bug, so explanation will be rather long.

Example 1:

from bokeh.plotting import figure, output_file, show

x = [0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
y1 = [30 for xx in x]

legend_items = []
p = figure(plot_width=500, plot_height=400)
l1 = p.line(x, y1, line_width=2, color='red')
p.y_range.start = 0
li1 = LegendItem(label='Line 1', renderers=[l1])
legend_items.append(li1)
legend = Legend(items=legend_items, location=(0, -30))
legend.click_policy = "hide"
p.add_layout(legend, 'left')

show(p)

That produces pretty looking chart:
image

Automatic padding added a little bit of space between horizontal line and top end of the chart, so line is clearly visible.

Example 2:

from bokeh.plotting import figure, output_file, show

x = [0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
y1 = [300 for xx in x]

legend_items = []
p = figure(plot_width=500, plot_height=400)
l1 = p.line(x, y1, line_width=2, color='red')
p.y_range.start = 0
li1 = LegendItem(label='Line 1', renderers=[l1])
legend_items.append(li1)
legend = Legend(items=legend_items, location=(0, -30))
legend.click_policy = "hide"
p.add_layout(legend, 'left')

show(p)

image

With line located at Y=300, there's no automatic padding added, so line located directly on the border of the chart.

One can argue, it can be easily fixed bu using customized y_range, but manually setting y_range bounds makes it hard to use new feature from 1.4.0 (#9144):

Example 3 (only works in Bokeh 1.4.0):

from bokeh.plotting import figure, output_file, show

x = [0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
y1 = [30 for xx in x]
y2 = [60 for xx in x]
y3 = [90 for xx in x]

legend_items = []

# create a new plot with a log axis type
p = figure(plot_width=500, plot_height=400)

l1 = p.line(x, y1, line_width=2, color='red')
l2 = p.line(x, y2, line_width=2, color='blue')
l3 = p.line(x, y3, line_width=2, color='green')

p.y_range.start = 0

li1 = LegendItem(label='Line 1', renderers=[l1])
li2 = LegendItem(label='Line 2', renderers=[l2])
li3 = LegendItem(label='Line 3', renderers=[l3])
legend_items.append(li1)
legend_items.append(li2)
legend_items.append(li3)

p.y_range.only_visible = True

legend = Legend(items=legend_items, location=(0, -30))
legend.click_policy = "hide"
p.add_layout(legend, 'left')

show(p)

Now we are using automated Y-range scaling.
All lines are clearly visible no matter how many lines we have hidden:

image

or

image

But if we change Y-values for lines back to 300...

Example 4 (only works in Bokeh 1.4.0):

from bokeh.plotting import figure, output_file, show

x = [0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
y1 = [300 for xx in x]
y2 = [600 for xx in x]
y3 = [900 for xx in x]

legend_items = []

# create a new plot with a log axis type
p = figure(plot_width=500, plot_height=400)

l1 = p.line(x, y1, line_width=2, color='red')
l2 = p.line(x, y2, line_width=2, color='blue')
l3 = p.line(x, y3, line_width=2, color='green')

p.y_range.start = 0

li1 = LegendItem(label='Line 1', renderers=[l1])
li2 = LegendItem(label='Line 2', renderers=[l2])
li3 = LegendItem(label='Line 3', renderers=[l3])
legend_items.append(li1)
legend_items.append(li2)
legend_items.append(li3)

p.y_range.only_visible = True

legend = Legend(items=legend_items, location=(0, -30))
legend.click_policy = "hide"
p.add_layout(legend, 'left')

show(p)

For 3 lines it works fine:

image

For 2 lines it works fine:

image

But with only 1 line visible automated padding not working again:

image

What I would expect?

I would expect to see similar gap between red line and top boundary of the chart, as I see for blue and green lines.

Why it's important?

Without callbacks there's no way to change y_range for the chart when certain lines became hidden and visible. As far as I understand, #9144 was supposed to provide simple way to do so, but since automated padding not always working the way you expect it to work, it's almost useless.

Activity

  1. bryevdv commented on Feb 24, 2020

    @bryevdv
    Member

    @Tikonderoga can you please try and report back with 2.0.0rc1 (available on pypi or bokeh/dev channel on anaconda.org)

    Please note the starting with 2.0 hiding glyphs in the legend will remove them from automatic ranging computations (often requested feature)

    Also, AFAICT, a summary of the issue is this, please confirm:

    When one end of a DataRange1d is set explicitly (e.g. p.y_range.start = 0) the other end stops applying padding (but should not stop).

  2. Tikonderoga commented on Feb 24, 2020

    @Tikonderoga
    ContributorAuthor

    @bryevdv Sorry, as I mentioned, it's weird issue and my description was lacking.
    Let me try it again, using your template.

    Issue (exists for all versions of Bokeh?):

    When one end of a DataRange1d is set explicitly (e.g. p.y_range.start = 0) the automatic padding for other end does not scale well (see the difference in padding for different Y-ranges in next screen).

    image

    In Bokeh > 1.4.0 is manifests itself in a case when p.y_range.only_visible = True:

    Y-range is small, all works fine:
    image

    Y-range is large, breaks when 1 line visible:
    image

    I think there's something wrong in a way how automatic padding is being calculated - it's looks like it's not taking size of range into consideration. And when you have only one line visible out of many, same code path is being used.

  3. bryevdv commented on Feb 26, 2020

    @bryevdv
    Member

    @Tikonderoga I have a suspicion, but have not yet been able to look into it. You could check out the BokehJS for DataRange1d in the mean time, if you like. My suspicion is this:

    • the default padding units are percentage
    • the percentage is always applied to the computed auto-range, even if one end is fixed
    • the computed auto-range with a straight line is zero
    • therefore the computed padding is zero (a percentage of zero)

    If this is actually the case, I am not sure I could conclude that the observed behavior is not, in fact, the correct behavior. I would not be in favor of adding magic, implicit special cases. So what could be done? Some ideas:

    • Nothing. In cases like this (which don't seem like they would be common), absolute padding units are an alternative option
    • Add a "min_padding" threshold that is always and uniformly applied (this avoids hidden magic for this one special case)
  4. bryevdv commented on Feb 26, 2020

    @bryevdv
    Member

    Looking quickly at the code, I do believe this is the case, and as I mentioned above this behavior not inconsisten, it is in fact completely consistent: the padding is always a percentage of the computed min/max range, even if that range is zero.

    For now, changing range_padding_units is an option. If you are interested to work up a PR to add a new min_padding property, then I would be happy to help out. Otherwise, I am inclined to close this with noaction since it is a pretty obscure corner case.

  5. Tikonderoga commented on Feb 26, 2020

    @Tikonderoga
    ContributorAuthor

    @bryevdv
    First of all, I would still argue it's inconsistent:

    Look at the image below. For all lines, at any Y-value, computed min/max range would be zero. Why there's padding added for lines A, B and C, but not for D?

    image

    Also, which part of Bokeh code should I look into?
    Is it bokeh/bokehjs/src/lib/models/ranges/data_range1d.ts
    ?

  6. bryevdv commented on Feb 26, 2020

    @bryevdv
    Member

    It's not really possible for me to speculate with just the images. Are there other invisible glyphs? What is the configuration of the data range? For that matter, are the "ABCD" labels or text glyphs, or were they added outside Bokeh after the fact completely? Code for what you have above would help clarify.

    Is it bokeh/bokehjs/src/lib/models/ranges/data_range1d.ts

    Yes that's the main file of interest. The range will ask any available renderers for their bounds (and then union them together), so there's also some possibility that the bounds computation in the Line glyph is also relevant.

  7. Tikonderoga commented on Feb 26, 2020

    @Tikonderoga
    ContributorAuthor

    @bryevdv

    Sure, I'll provide code and a lot of test cases where padding (both manual and automatic) appears to be broken. There will be multiple comments

    Case 1.

    from bokeh.plotting import figure, output_file, show
    from bokeh.layouts import gridplot
    from bokeh.models import ColumnDataSource, Range1d, LabelSet, Label
    
    x = [0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
    y1 = [2 for xx in x]
    y2 = [5 for xx in x]
    y3 = [50 for xx in x]
    y4 = [100 for xx in x]
    y5 = [500 for xx in x]
    
    p1 = figure(plot_width=300, plot_height=300)
    l1 = p1.line(x, y1, line_width=2, color='red')
    p1.y_range.start = 0
    
    p2 = figure(plot_width=300, plot_height=300)
    l2 = p2.line(x, y2, line_width=2, color='red')
    p2.y_range.start = 0
    
    p3 = figure(plot_width=300, plot_height=300)
    l3 = p3.line(x, y3, line_width=2, color='red')
    p3.y_range.start = 0
    
    p4 = figure(plot_width=300, plot_height=300)
    l4 = p4.line(x, y4, line_width=2, color='red')
    p4.y_range.start = 0
    
    p5 = figure(plot_width=300, plot_height=300)
    l5 = p5.line(x, y5, line_width=2, color='red')
    p5.y_range.start = 0
    
    grid = gridplot([[p1, p2, p3, p4, p5]], plot_width=250, plot_height=250)
    
    show(grid)
    

    image

    What's wrong?
    Computed range for all lines is 0, yet for first few lines, where Y is relatively small, automated padding is added, but for line with Y=500 its too small.

  8. bryevdv commented on Feb 26, 2020

    @bryevdv
    Member

    @Tikonderoga That's perfect I can run that and actually probe what the JS is doing.

    FYI I think you could just pass e.g. y1=2 to the line call and not generate those lists.

  9. bryevdv commented on Feb 26, 2020

    @bryevdv
    Member

    Looking at those now it looks possibly like the padding is always 1, which simple shows up very tiny on the scale of the last plot.

  10. Tikonderoga commented on Feb 26, 2020

    @Tikonderoga
    ContributorAuthor

    Case 2: Auto-padding works correctly!

    from bokeh.plotting import figure, output_file, show
    from bokeh.layouts import gridplot
    from bokeh.models import ColumnDataSource, Range1d, LabelSet, Label
    
    x = [0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
    y1 = [2 for xx in x]
    y1[1] = 3
    y2 = [5 for xx in x]
    y2[1] = 7.5
    y3 = [50 for xx in x]
    y3[1] = 55
    y4 = [100 for xx in x]
    y4[1] = 150
    y5 = [500 for xx in x]
    y5[1] = 750
    
    p1 = figure(plot_width=300, plot_height=300)
    l1 = p1.line(x, y1, line_width=2, color='red')
    
    p2 = figure(plot_width=300, plot_height=300)
    l2 = p2.line(x, y2, line_width=2, color='red')
    
    p3 = figure(plot_width=300, plot_height=300)
    l3 = p3.line(x, y3, line_width=2, color='red')
    
    p4 = figure(plot_width=300, plot_height=300)
    l4 = p4.line(x, y4, line_width=2, color='red')
    
    p5 = figure(plot_width=300, plot_height=300)
    l5 = p5.line(x, y5, line_width=2, color='red')
    
    grid = gridplot([[p1, p2, p3, p4, p5], [None, None]], plot_width=250, plot_height=250)
    
    show(grid)
    
    

    image

    Case 3 : Auto-padding broken with y_range.start = 0

    from bokeh.plotting import figure, output_file, show
    from bokeh.layouts import gridplot
    from bokeh.models import ColumnDataSource, Range1d, LabelSet, Label
    
    x = [0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
    y1 = [2 for xx in x]
    y1[1] = 3
    y2 = [5 for xx in x]
    y2[1] = 7.5
    y3 = [50 for xx in x]
    y3[1] = 55
    y4 = [100 for xx in x]
    y4[1] = 150
    y5 = [500 for xx in x]
    y5[1] = 750
    
    p1 = figure(plot_width=300, plot_height=300)
    l1 = p1.line(x, y1, line_width=2, color='red')
    p1.y_range.start = 0
    
    p2 = figure(plot_width=300, plot_height=300)
    l2 = p2.line(x, y2, line_width=2, color='red')
    p2.y_range.start = 0
    
    p3 = figure(plot_width=300, plot_height=300)
    l3 = p3.line(x, y3, line_width=2, color='red')
    p3.y_range.start = 0
    
    p4 = figure(plot_width=300, plot_height=300)
    l4 = p4.line(x, y4, line_width=2, color='red')
    p4.y_range.start = 0
    
    p5 = figure(plot_width=300, plot_height=300)
    l5 = p5.line(x, y5, line_width=2, color='red')
    p5.y_range.start = 0
    
    grid = gridplot([[p1, p2, p3, p4, p5], [None, None]], plot_width=250, plot_height=250)
    
    show(grid)
    
    

    image

    That's an interesting one: all charts appears to be okay, even the last one, but one in the middle is not.

  11. Tikonderoga commented on Feb 26, 2020

    @Tikonderoga
    ContributorAuthor

    Case 4: Playing with range_padding. Not working for straight lines!

    from bokeh.plotting import figure, output_file, show
    from bokeh.layouts import gridplot
    from bokeh.models import ColumnDataSource, Range1d, LabelSet, Label
    
    x = [0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
    y1 = [500 for xx in x]
    
    p1 = figure(plot_width=300, plot_height=300)
    l1 = p1.line(x, y1, line_width=2, color='red')
    p1.y_range.start = 0
    t1 = Label(x=20, y=100, x_units='screen', y_units='screen',
                     text='No parameters set', render_mode='css',
                     border_line_color='black', border_line_alpha=0,
                     background_fill_color='white', background_fill_alpha=1.0,
                     text_font_size = '8pt')
    
    p1.add_layout(t1)
    
    p2 = figure(plot_width=300, plot_height=300)
    l2 = p2.line(x, y1, line_width=2, color='red')
    p2.y_range.start = 0
    p2.y_range.range_padding_units = 'percent'
    p2.y_range.range_padding = 0.1
    t2 = Label(x=20, y=100, x_units='screen', y_units='screen',
                     text="range_padding_units = percent\n range_padding = 0.1", render_mode='css',
                     border_line_color='black', border_line_alpha=0,
                     background_fill_color='white', background_fill_alpha=1.0,
                     text_font_size = '8pt')
    
    p2.add_layout(t2)
    
    p3 = figure(plot_width=300, plot_height=300)
    l3 = p3.line(x, y1, line_width=2, color='red')
    p3.y_range.start = 0
    p3.y_range.range_padding_units = 'percent'
    p3.y_range.range_padding = 10
    t3 = Label(x=20, y=100, x_units='screen', y_units='screen',
                     text="range_padding_units = percent\n range_padding = 10", render_mode='css',
                     border_line_color='black', border_line_alpha=0,
                     background_fill_color='white', background_fill_alpha=1.0,
                     text_font_size = '8pt')
    
    p3.add_layout(t3)
    
    
    p4 = figure(plot_width=300, plot_height=300)
    l4 = p4.line(x, y1, line_width=2, color='red')
    p4.y_range.start = 0
    p4.y_range.range_padding_units = 'absolute'
    p4.y_range.range_padding = 0
    t4 = Label(x=20, y=100, x_units='screen', y_units='screen',
                     text="range_padding_units = absolute\n range_padding = 0", render_mode='css',
                     border_line_color='black', border_line_alpha=0,
                     background_fill_color='white', background_fill_alpha=1.0,
                     text_font_size = '8pt')
    
    p4.add_layout(t4)
    
    
    p5 = figure(plot_width=300, plot_height=300)
    l5 = p5.line(x, y1, line_width=2, color='red')
    p5.y_range.start = 0
    p5.y_range.range_padding_units = 'absolute'
    p5.y_range.range_padding = 100
    t5 = Label(x=20, y=100, x_units='screen', y_units='screen',
                     text="range_padding_units = absolute\n range_padding = 100", render_mode='css',
                     border_line_color='black', border_line_alpha=0,
                     background_fill_color='white', background_fill_alpha=1.0,
                     text_font_size = '8pt')
    
    p5.add_layout(t5)
    
    grid = gridplot([[p1, p2, p3, p4, p5], [None, None]], plot_width=250, plot_height=250)
    
    show(grid)
    

    image

    If computed range for line is 0, manual padding not working at all.

  12. Tikonderoga commented on Feb 26, 2020

    @Tikonderoga
    ContributorAuthor

    Case 5: manual padding with non-straight lines:

    from bokeh.plotting import figure, output_file, show
    from bokeh.layouts import gridplot
    from bokeh.models import ColumnDataSource, Range1d, LabelSet, Label
    
    x = [0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
    y1 = [500 for xx in x]
    y1[2] = 550
    
    p1 = figure(plot_width=300, plot_height=300)
    l1 = p1.line(x, y1, line_width=2, color='red')
    p1.y_range.start = 0
    t1 = Label(x=20, y=100, x_units='screen', y_units='screen',
                     text='No parameters set', render_mode='css',
                     border_line_color='black', border_line_alpha=0,
                     background_fill_color='white', background_fill_alpha=1.0,
                     text_font_size = '8pt')
    
    p1.add_layout(t1)
    
    p2 = figure(plot_width=300, plot_height=300)
    l2 = p2.line(x, y1, line_width=2, color='red')
    p2.y_range.start = 0
    p2.y_range.range_padding_units = 'percent'
    p2.y_range.range_padding = 0.1
    t2 = Label(x=20, y=100, x_units='screen', y_units='screen',
                     text="range_padding_units = percent\n range_padding = 0.1", render_mode='css',
                     border_line_color='black', border_line_alpha=0,
                     background_fill_color='white', background_fill_alpha=1.0,
                     text_font_size = '8pt')
    
    p2.add_layout(t2)
    
    p3 = figure(plot_width=300, plot_height=300)
    l3 = p3.line(x, y1, line_width=2, color='red')
    p3.y_range.start = 0
    p3.y_range.range_padding_units = 'percent'
    p3.y_range.range_padding = 10
    t3 = Label(x=20, y=100, x_units='screen', y_units='screen',
                     text="range_padding_units = percent\n range_padding = 10", render_mode='css',
                     border_line_color='black', border_line_alpha=0,
                     background_fill_color='white', background_fill_alpha=1.0,
                     text_font_size = '8pt')
    
    p3.add_layout(t3)
    
    
    p4 = figure(plot_width=300, plot_height=300)
    l4 = p4.line(x, y1, line_width=2, color='red')
    p4.y_range.start = 0
    p4.y_range.range_padding_units = 'absolute'
    p4.y_range.range_padding = 0
    t4 = Label(x=20, y=100, x_units='screen', y_units='screen',
                     text="range_padding_units = absolute\n range_padding = 0", render_mode='css',
                     border_line_color='black', border_line_alpha=0,
                     background_fill_color='white', background_fill_alpha=1.0,
                     text_font_size = '8pt')
    
    p4.add_layout(t4)
    
    
    p5 = figure(plot_width=300, plot_height=300)
    l5 = p5.line(x, y1, line_width=2, color='red')
    p5.y_range.start = 0
    p5.y_range.range_padding_units = 'absolute'
    p5.y_range.range_padding = 100
    t5 = Label(x=20, y=100, x_units='screen', y_units='screen',
                     text="range_padding_units = absolute\n range_padding = 100", render_mode='css',
                     border_line_color='black', border_line_alpha=0,
                     background_fill_color='white', background_fill_alpha=1.0,
                     text_font_size = '8pt')
    
    p5.add_layout(t5)
    
    grid = gridplot([[p1, p2, p3, p4, p5], [None, None]], plot_width=250, plot_height=250)
    
    show(grid)
    

    image

    1. Percentages are not really percentages? Setting padding to 10% increases Y range by 250 (middle chart)
    2. Padding in absolute units working!
  13. 10 remaining items

  14. Tikonderoga commented on Feb 28, 2020

    @Tikonderoga
    ContributorAuthor

    @mattpap @bryevdv
    I'm trying to build a fix and verify it before creating pull request.
    I've managed to build Bokeh locally, but when I'm trying to run tests in notebook, I'm seeing following error:

    image

    How would I fix that?
    Should I go to Discourse with such questions?

  15. mattpap commented on Feb 28, 2020

    @mattpap
    Contributor

    My personal approach to this is as follows:

    • terminal 1: BOKEH_DEV=true bokeh static
    • terminal 2: BOKEH_RESOURCES=server-dev jupyter-lab

    This will make sure that you always have the most recently built bundles and debugging experience in jlab is the most efficient. Another personal preference is not to use jlab for bokeh development, as it's usually more trouble than it's worth.

  16. bryevdv commented on Feb 28, 2020

    @bryevdv
    Member

    BOKEH_RESOURCES=inline is another option, but also agree: if you are not working on something that is specifically jupyter-related, then working there adds quite a lot of (IMO) otherwise unnecessary complication.

  17. Tikonderoga commented on Feb 28, 2020

    @Tikonderoga
    ContributorAuthor

    Thanks,

    output_notebook(resources=INLINE)
    

    does the trick.

  18. Tikonderoga commented on Feb 28, 2020

    @Tikonderoga
    ContributorAuthor

    And follow-up question: is there any instructions how to contribute to bokeh source code?
    Or is it just "create branch -> test -> create pull request" routine?

  19. bryevdv commented on Feb 28, 2020

    @bryevdv
    Member

    @Tikonderoga yes exactly PRs from forks are the standard way to proceed

  20. modified the milestones: , 2.0.1 on Mar 19, 2020
  21. github-actions commented on Oct 27, 2024

    @github-actions

    This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

  22. locked as resolved and limited conversation to collaborators on Oct 27, 2024
  23. added theissue type on Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions