Create a new glyph from multi-dimensional data


I would like to create a visualization of multi-dimensional data (i.e. the dimension of the y-coordinate is > 1) using custom glyphs. Example:

What would be the most straight-forward way to achieve this?

@Matus_Gasparik There are lots of potential ways to approach this:

  • Four separate calls to one of the “rect” glyphs [1], one for each quadrant [2], each with a single color
  • A single call to one of the rect glyphs, with a 4x-long data arrays that have all the data for all the quadrants, and also an associated column for colors for every quadrant drawn (or a colormapper)
  • An actual custom extension glyph, by far the most work.

Which one is “best” really depends on the shape of your data to start, so you’ll have to offer a bit more details for any further guidance.

  1. rect, block, quad, vbar, hbar are all options, they each have a different “fit” in terms of the data they accept ↩︎

  2. i.e .one call to quad to draw all the “upper left” quadrants, another call to quad to draw all the “upper right” quadrants, etc. ↩︎

1 Like

Thanks, @Bryan, for your prompt answer! These are actually great suggestions for this particular example.

In long term, I’m actually interested in the last option (of extending Bokeh with custom Glyphs), because I want to try different (maybe less trivial) Glyphs. In essence, I want to use such a glyph as a sort of “marker type” for values which are themselves vectors consisting of multiple elements (attributes). Ideally, the input data are x,y-grid coordinates (placement of individual glyphs) and “values-coordinates” with a shape of [x * y * num_attributes].

From a quick view in the documentation there are no examples for how to create custom glyphs. Any hints or suggestions that will point me in the right direction will be very appreciated…

Custom extensions are not especially common, and also more involved. That combination is why there is only minimal documentation around the topic (many other things are more impactful for typical users). So unfortunately the best I can really do is point you at the general documentation for creating custom extensions:

Custom extensions — Bokeh 3.0.3 Documentation

And also point you towards the implementation of an existing built-in glyph that might server as a useful reference:

bokeh/block.ts at branch-3.1 · bokeh/bokeh · GitHub

I also have to warn you explicitly that BokehJS is still under very active development, and is not considered 100% stable at this point. An extension written today may need changes to work with future releases. BokehJS used to be purely an implementation detail and hidden entirely. We are working towards making it a more stable “public” target for development, but like most every OSS project, there are resource/priority trade-offs.

1 Like

Thanks, again @Bryan. I guess I’ll stick then with the easier options then. Reverse-engineering how the API works from the BokehJS src code is indeed going to be substantially more work, then rendering “per-partes” using the existing glyphs…

Random tip from non-dev but heavy user: CustomJSTransform can probably be quite useful here (within certain limitations). I’ve used it heavily to translate geometries (simplest example being a quad top/bottom to a rect center/height) and/or calculate derived properties/values on the JS side.

1 Like

Actually a very good suggestion! If you only want to store “raw” data in your CDS and have things like data coordinates be computed from those only in the browser, this would be an option.

Thanks, @gmerritt123, for the suggestion.This looks really promising. So in the end I could handle putting all sub-glyphs (like those 4 squares in the example above) into their respective positions using a single CustomJSTransform, right? But in that case, I would need to handle the style attributes (colors, etc.) separately I guess?!

Well, not quite, but sort of.

The obstacle to literally having a single CustomJSTransform is that currently the JS snippet that you provide only gets the array of values to transform. It does not know the column name they came from, so it can’t condition on that name. I think that might actually be a reasonable and reasonably simple new feature to add, so I’d really encourage you to file a GitHub Issue for requesting it.

In the mean time, you could make a little helper factory function, something like this, that reuses the same JS code, but does something different based on the column name:

def mything(colname, source):
    code = f"""
    // "template" the specific column for this instance of the transform
    const colname = {{colname}}

    // condition on the column name here
    if (colname == "foo") {...}
    else if (colname == "bar") {...}

    # return a transform templated for this specific colname
    return transform(colname, 
                     CustomJSTransform(args=dict(source=source, code=code)))

Then you can call mything to transform different columns in your CDS, and the code can do something different and appropriate based on the column name.

You might need to pass additional parameters to mything to template into the code, given that you have four different glyphs to drive (i.e. some might get the same column name but be for a different quandrant). You could also template in multiple column names for that matter, I passed in the source as an arg so inside the JS code you can look at any/all of the columns to do whatever the work is. Since you haven’t actually provided any concrete details about the data or how it is to be transformed, I can’t really speculate any further or more concretely, but hopefully this is enough to get you started.

When/if the feature is added to pass the column name automatically to the JS code snippet, then this kind of templating at the Python level would not be needed.

OK, thanks. I was actually thinking along the lines of having a single array of values like [a1, b1, c1, d1, a2, b2, c2, d2, a3, b3, ...] and re-arranging the 1,2,3,4’s into their respective quandrants in a single loop. But these are really details. I will try to come up with some prototypes and post them here. I am relatively new to Bokeh and I also noticed lots of new functionality / documentation since the last time I used it (which is fantastic!).

You could also template in multiple column names for that matter, I passed in the source as an arg so inside the JS code you can look at any/all of the columns to do whatever the work is.

Continuing this thread of thought, could this also work like so:

p.rect('x', 'y', transform=my_data_transform,...)

… instead of like so:

p.rect(x=transform('x', my_data_transform), y=transform('y', my_data_transform),...)

The v_func would probably need to return a dict of arrays (or a JS object)?!?

If this is feasible and not utterly complicated I could open a GH Issue.

Lastly, I intentionally named the function my_data_transform. But, how about transforming the geometric primitives directly with some affine 2D transformation functions? That could be useful for creating custom glyphs (but I acknowledge that this is a very specific use case, not interesting for the majority of users).

I’ll point you to this thread as well → when I spoke of “certain limitations” I was primarily thinking of this one: that the transformed result needs to be the same length as the original CDS. CustomJSTransform NOT agnostic of CDS length?

Yes a transform just gets an array of values in a variable always named xs and is expected to return an identical length array of transformed values. There’s no way to return a dict of columns or anything like that. So you would need to configure each glyph coordinate with its own transform. If transforms could receive the name of the column they are transforming, then it could be the same transform. But otherwise for now, it can be separate instances that re-use basically the same code as I described above.

affine 2D transformation functions

BokehJS has some utilities for affine transforms built-in that could potentially by used:

bokeh/affine.ts at branch-3.1 · bokeh/bokeh · GitHub

You can “import” that module the same as other example import modules for base classes, etc. in the custom extension examples.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.