Additional tick-like visual indicator on axis

I have a plot with additional y-axes and would like to add visual indicators (similar to ticks) at fixed positions to some of those axes. There should be one indicator to the right of the axis and one to the left, similar to the red bars in the screenshot below:

To achieve the outcome shown in the screenshot, I used Label annotations:

right_indicator = Label(x=-101, x_units='screen', y=y_value, text='_', y_range_name='red', text_color='#d62728', text_font_style='bold')
plot.add_layout(right_indicator, 'right')
...

However, this is not a good solution, as the position of the Label relative to the axis can change when the width of the elements changes during zooming.

I did not find a straightforward way to set the Label’s x coordinate relative to the axis, nor to add a custom fixed ticker with only those indicators in addition to the default ticker.

How can the desired outcome be achieved?

I don’t actually think there is a good approach to this at present. IIRC there was at one point some discussion about affording a way to “combine” multiple tickers on a single axis, but I can’t find that any issue was ever made, so there was never any movement on it. That wouldn’t really address your full use-case anyway, though, since you want to control the individual tick appearance, which is also not possible (i.e. there is just one tick_color for the entire axis, etc)

Bokeh also does not expose much support for users to draw things “outside” the central plot area, either, so the fact that you want this on a secondary axis away from the plot region also complicates things, unfortunately.

I am afraid I don’t really have any concrete immediate-term suggestions. For longer term, you might consider opening a GitHub development discussion if you want to prompt some discussion of what an appropriate API for this might look like.

@Getuem One way to achieve what you want is the following (hacky) way. Use a Div element where you style it with a bottom or top border and the desired width. Use a Panel to position the Div element. Below I have used target='frame' and bottom_right, hence that is the location of the lower right corner of the plot frame (y axis located on this side). anchor='bottom_left' means that the div should be anchored on the left side.

MARKER_WIDTH = 20

label = Panel(
    position = Node(target = 'frame', symbol = 'bottom_right'),
    anchor="bottom_left",
    stylesheets=[f"""
:host {{
  padding: 0px;
  background-color: transparent;
  font-size: 12px;
  z-index: 999;
}}
.marker {{
  background-color: whitesmoke;
  padding: 0px;
  height: 0px;
  line-height: 0px;
  width: {MARKER_WIDTH}px;
  border-bottom: 2px solid red;
}}
"""],
    elements=[
        Div(
            text='',
            css_classes=['marker'],
            margin = 0
            )
    ],
)

I then use a CustomJS callback to calculate a value of padding-bottom to apply to the Div element in order to position it at the desired vertical location on the secondary y-axis. It requires to get the start and end values for the range but also the screen px values for same range. In order to make sure the the marker is always at the correct location horizontally I query the width of the default y-axis and use that for margin-left of the Div element, taking into account the width itself of the marker.

from bokeh.io import curdoc
from bokeh.plotting import figure, show
from bokeh.models import Node
from bokeh.models import Panel, CustomJS, Div, DataRange1d, LinearAxis

x = [1, 2, 3, 4, 5]
y = [6, 7, 2, 4, 5]
y2 = [213, 155, 66, 234, 189]

p = figure(
    width=400,
    height=400,
    y_axis_location = "right",
    toolbar_location = "left"
    )
p.border_fill_color = "whitesmoke"
p.min_border_right = 50
r_square = p.scatter(x, y, marker = 'square', size = 8)
p.y_range.renderers = [r_square]
p.yaxis[0].html_id = 'y1'

r_circle  = p.scatter(
    x, y2, color='red', size=8,
    y_range_name="y2",
)
p.extra_y_ranges['y2'] = DataRange1d(renderers = [r_circle])

ax2 = LinearAxis(
    axis_label="y2",
    y_range_name="y2",
    html_id = 'y2'
)

p.add_layout(ax2, 'right')

MARKER_WIDTH = 20

label = Panel(
    position = Node(target = 'frame', symbol = 'bottom_right'),
    anchor="bottom_left",
    stylesheets=[f"""
:host {{
  padding: 0px;
  background-color: transparent;
  font-size: 12px;
  z-index: 999;
}}
.marker {{
  background-color: whitesmoke;
  padding: 0px;
  height: 0px;
  line-height: 0px;
  width: {MARKER_WIDTH}px;
  border-bottom: 2px solid red;
}}
"""],
    elements=[
        Div(
            text='',
            css_classes=['marker'],
            margin = 0
            )
    ],
)
p.elements.append(label)

cb = CustomJS(
    args = {
        'label': label,
        'marker_width': MARKER_WIDTH,
        'plot': p,
    },
    code = '''
        const y2_marker = 200;

        const host = document.querySelector('.bk-Figure');

        if (host == null) {
            return;
        }
        var elm = host;
        const classes = ['.bk-Canvas', '.bk-CanvasPanel.bk-right', '#y1'];
        for (const cls of classes) {
            elm = elm.shadowRoot.querySelector(cls);
        }
        
        setTimeout(() => {
            const y0_axis_width = elm.offsetWidth;
            const {start, end, frames} = plot.extra_y_ranges['y2'];

            const frame_set = frames.values().next();
            const {y0, y1, x1} = frame_set['value']._bbox;

            const marker_y_px = (y2_marker-start)/(end-start)*(y1-y0)-1;

            if ((marker_y_px < 0) || (marker_y_px > y1-y0)) {
                label.visible = false;
            } else {
                label.visible = true;
            }
            const margin_l = y0_axis_width - marker_width/2;
            label.styles = {'padding-bottom': `${marker_y_px}px`, 'margin-left': `${margin_l}px`};
        }, 0);

    '''
)
p.extra_y_ranges['y2'].js_on_change('start', cb)
curdoc().js_on_event("document_ready", cb)

show(p)

Plot with marker at 200 on the y2 axis.

1 Like

A bit ot a PITA but a clever workaround @Jonas_Grave_Kristens