How to get glyph coordinates in screen units for use in CanvasTexture?

I would like to use a gradient color style for fill for a patch glyph. Based on search results here on discourse it seems one should try CanvasTexture. I have managed to get the gradient fillStyle to work with JS code. However, the problem is the manual part of getting the screen units that I need to use in order to define the location and size (height) of the texture canvas. For me it is important that the gradient is not repeated in the y-direction, hence it covers the whole height of the patch glyph.

So my question: is it possible to get screen units of the patch glyph coordinates?
If I change my app to bokeh serve setup, is it then possible to get the coordinates in screen units?

Thanks, any help appreciated.

(BTW: I have made one solution of gradient color fill using 40 thin patches, but I would like to see if the CanvasTexture could be a viable alternative.)

System: bokeh 2.4.3, macOS, python 3.9, FireFox 101.0.1.

from bokeh.io import show, save
from bokeh.plotting import figure
from bokeh.models import CanvasTexture


gradient_code = """
    const bk_plot = Bokeh.documents[0].get_model_by_name('plot');
    console.log(bk_plot.x_range.start); // null (plot not drawn yet?)
    console.log(bk_plot.x_range.end); // null (plot not drawn yet?)
    console.log('inner_width: ' + bk_plot.inner_width.toString()); // 0
    console.log('inner_height: ' + bk_plot.inner_height.toString()); // 0

    // can I get any data vs screen units from the patch glyph model?
    const bk_patch = Bokeh.documents[0].get_model_by_name('patch');
    console.log(bk_patch.glyph.properties.y);

    //console.log(ctx);

    // height of patch and y0 is in px units and based on my monitor
    // they correlate of course to the min and max of patch y coordinates 
    // how do I get from patch data units to screen units?
    const height = 500; // measured on my monitor
    const y0 = 53;      // measured on my monitor
    const y1 = height+y0;

    ctx.canvas.width = 100;
    ctx.canvas.height = y1;
    
    const grad = ctx.createLinearGradient(20, y0, 20, y1);
    ctx.globalAlpha = 0.8;
    grad.addColorStop(0, "black");
    grad.addColorStop(1, "yellow");

    ctx.fillStyle = grad;
    ctx.fillRect(0, y0, 100, y1);
"""

gradient = CanvasTexture(
    code = gradient_code,
    repetition = "repeat_x"
)

p = figure(
    title = "CanvasTexture",
    toolbar_location = None, tools = "",
    y_axis_location = "right", name = "plot"
    )

r = p.patch(
    x = [2, 3, 5, 6, 6, 5, 4, 2], 
    y = [10, 5, 9, 13, 15, 18, 19, 14],
    fill_color = None,
    fill_alpha = 0.5,
    line_color="grey",
    hatch_pattern = "image",
    hatch_scale=34,
    hatch_color = "grey",
    hatch_weight = 0.5,
    hatch_alpha = 0.5,
    hatch_extra = {"image": gradient},
    name = 'patch'
    )

save(p)
1 Like

There’s not any simple/common way. The screen coordinates are stored on the views, not the models, and the views are by and large hidden away. You can access the top level views via window.Bokeh.embed.index. You would need to traverse that hierarchy (e.g. descending through child_views and/or renderer_views properties) to find the PatchView that you want. Then you could access its JS attributes glyph.sx and glyph.sy.

It’s possible in the future we could consider adding some minimal API for querying the view tree more easily. Please feel free to submit a GitHub Issue.

@Bryan thanks for your answer, very much appreciated. I am able see the glyph data in screen units in the browser console. However, my JS skills are not so good and I struggle to write a loop the can traverse the window.Bokeh.embed.index object. The loops I have tried do not output anything so I guess I do not understand how to read the object in JS. From the console I would assume the Object { } indicates that it is in fact an object. I do not understand the number 1003 (ID?). If I use it as a key I get undefined (idxObj[1003]).

This i what I have tried:

    const idxObj = window.Bokeh.embed.index;
    console.log(idxObj);

    // all the loops here do not yield any output, why?
    // I want to traverse the idxObj
    for (const [key, value] of Object.entries(idxObj)) {
        console.log(key);
        console.log(value);
    }
    
    for (const key of Object.getOwnPropertyNames(idxObj)) {
        console.log(key);
    }

    for (const key in idxObj) {
      console.log(key);
    }
    
    //console.log(ctx);

    // height of patch and y0 is in px units and based on my monitor
    // they correlate of course to the min and max of patch y coordinates 
    // how do I get from patch data units to screen units?
    const height = 500; // measured on my monitor
    const y0 = 53;      // measured on my monitor
    const y1 = height+y0;

    ctx.canvas.width = 100;
    ctx.canvas.height = y1;
    
    const grad = ctx.createLinearGradient(20, y0, 20, y1);
    ctx.globalAlpha = 0.8;
    grad.addColorStop(0, "black");
    grad.addColorStop(1, "yellow");

    ctx.fillStyle = grad;
    ctx.fillRect(0, y0, 100, y1);
"""

Assuming there is just one “root”, and it is a Plot, then you can get a concrete list of all the renderer views with:

[...Object.values(window.Bokeh.embed.index)[0].renderer_views.values()]

It will probably help if you configure a name on the glyph, then you could search for r.name in a loop o over these, and once you find it, use r.glyph.sx etc.

@Bryan Thanks, but I do struggle to get the object values.

console.log(window.Bokeh.embed.index)

gives me (only first value shown)

Object {  }
	1003: Object { removed: {…}, _ready: Promise { "fulfilled" }, _idle_notified: true, … }
		_child_views: Map(0)
        ......

But I am not able to get the values out of the object, the array is empty:

console.log([...Object.values(window.Bokeh.embed.index)]);
 ->
Array []
	length: 0
	<prototype>: Array []

So is the window.Bokeh.embed.index object is specific kind?

Do I need to wait until the document is finalized before the Object is ready? And if so, how do I wait?

It seems one needs to wait until the document is idle. If running as a bokeh serve and having a DocumentReady event callback, one can update the patch glyph with hatch_extra and hatch_pattern which will then trigger the CanvasTexture and now one can retrieve the renderer_views.

@Bryan: is renderer_views accessible on the python side of Bokeh?

r = p.patch(
    x = [2, 3, 5, 6, 6, 5, 4, 2], 
    y = [10, 5, 9, 13, 15, 18, 19, 14],
    fill_color = None,
    fill_alpha = 0.5,
    line_color="grey",
    #hatch_pattern = "image",
    hatch_scale=34,
    hatch_color = "grey",
    hatch_weight = 0.5,
    hatch_alpha = 0.5,
    #hatch_extra = {"image": gradient},
    name = 'patch'
    )

def gradient_cb(event):
    r.glyph.update(
        hatch_extra = {"image": gradient},
        hatch_pattern = "image"
        )


curdoc().on_event(DocumentReady, gradient_cb)

No, all views are all only purely JS-side.