Skip to content

Add support for integration with Bokeh#3190

Draft
mattpap wants to merge 1 commit into
Netflix:masterfrom
mattpap:mattpap/bokeh_embed
Draft

Add support for integration with Bokeh#3190
mattpap wants to merge 1 commit into
Netflix:masterfrom
mattpap:mattpap/bokeh_embed

Conversation

@mattpap
Copy link
Copy Markdown

@mattpap mattpap commented May 11, 2026

PR Type

  • Bug fix
  • New feature
  • Core Runtime change (higher bar -- see CONTRIBUTING.md)
  • Docs / tooling
  • Refactoring

Summary

This PR add support for Bokeh for interactive visualizations and dashboards. This provides an alternative for existing support for Altair/Vega and Matplotlib. In this PR new component type BokehEmbed is added, which accepts either a Bokeh's Document or a UIElement (plots, layouts, widgets, etc.).

A few notes:

  1. I will add documentation and examples to metaflow-docs soon. I will update this PR when that's done.
  2. I'm not sure about the name of the component. Perhaps simply Bokeh would be better?
  3. Currently Bokeh's JS runtime (@bokeh/bokehjs) is added in full to the main bundle. That's a lot of JavaScript, which significantly increases the size of the bundle. I need to figure out a way to load Bokeh's models (the bulk of the the size) dynamically. Alternatively, this support could be split off into its own bundle and imported in cards that actually use Bokeh.
  4. I didn't yet explore implementing dynamic updates (i.e. BokehEmbed.update()).
  5. I need explore the possibility to share models between multiple instances of BokehEmbed(). In the example below, it would allow e.g. the select widget to affect the plot. Currently this would fail with models can be owned only by a single document in Bokeh.
  6. Due to (3) I did not update main.js bundle at this point.
Example
from metaflow import FlowSpec, step, current, card, pypi
from metaflow.cards import Artifact, BokehEmbed, Markdown, Table

class BokehDemo(FlowSpec):
    @step
    def start(self):
        self.next(self.bokeh)

    @pypi(python="3.14", packages={"bokeh": "3.9"})
    @card(type="blank")
    @step
    def bokeh(self):
        import numpy as np
        from bokeh.plotting import figure
        from bokeh.layouts import column
        from bokeh.models import BoxSelectTool, CustomJS, Div, RangeSlider

        description = Div(text="""\
Use <b>box select</b> tool (second on the right) to make selections and modify selections using sliders.
""")

        N = 4000
        x = np.random.random(size=N) * 100
        y = np.random.random(size=N) * 100
        radii = np.random.random(size=N) * 1.5
        colors = np.array([(r, g, 150) for r, g in zip(50+2*x, 30+2*y)], dtype=np.uint8)

        x0, x1, y0, y1 = 20, 80, 20, 80

        box_select = BoxSelectTool(persistent=True, continuous=True)
        box_select.overlay.update(left=x0, right=x1, top=y1, bottom=y0)

        x_slider = RangeSlider(value=(x0, x1), start=0, end=100, title="X")
        y_slider = RangeSlider(value=(y0, y1), start=0, end=100, title="Y")

        plot = figure(tools=["pan", box_select, "hover"])
        renderer = plot.circle(x, y, radius=radii, fill_color=colors, fill_alpha=0.6, line_color=None)

        layout = column(description, x_slider, y_slider, plot)

        update_overlay = CustomJS(args=dict(x_slider=x_slider, y_slider=y_slider, box_select=box_select), code="""
            const [x0, x1] = x_slider.value
            const [y0, y1] = y_slider.value
            const lrtb = {left: x0, right: x1, top: y1, bottom: y0}
            box_select.overlay.setv(lrtb)

            // TODO the above should imply the following; remove this when upgrading to Bokeh 3.10
            const view = cb_context.index.get_one(box_select)
            const screen_lrtb = view._compute_lrtb(lrtb)
            const {left, right, top, bottom} = screen_lrtb
            view._do_select([left, right], [top, bottom], true)
        """)
        x_slider.js_on_change("value", update_overlay)
        y_slider.js_on_change("value", update_overlay)

        update_sliders = CustomJS(args=dict(x_slider=x_slider, y_slider=y_slider), code="""
            const {left: x0, right: x1, top: y1, bottom: y0} = this
            x_slider.value = [x0, x1]
            y_slider.value = [y0, y1]
        """)

        box_select.overlay.js_on_change("left", update_sliders)
        box_select.overlay.js_on_change("right", update_sliders)
        box_select.overlay.js_on_change("top", update_sliders)
        box_select.overlay.js_on_change("bottom", update_sliders)

        selection_changed = CustomJS(args=dict(description=description), code="""
            description.text = `You've selected <b>${this.indices.length}</b> data points.`
        """)

        renderer.data_source.selected.js_on_change("indices", selection_changed)

        current.card.append(BokehEmbed(layout))
        self.next(self.bokeh_in_table)

    @pypi(python="3.14", packages={"bokeh": "3.9"})
    @card(type="blank")
    @step
    def bokeh_in_table(self):
        import numpy as np
        from bokeh.plotting import figure
        from bokeh.core.enums import MarkerType

        plot = figure(width=400, height=50, y_axis_type=None)
        plot.hbar(left=[0, 1, 2], right=[1, 2, 3], y=[10, 12, 14])

        from bokeh.models import Select
        marker_type = Select(value="circle", options=[*MarkerType])
        table = Table([
            ["first row", Artifact({"a": 2})],
            ["second row", BokehEmbed(plot)],
            ["third row", BokehEmbed(marker_type)],
        ])
        current.card.append(table)
        self.next(self.end)

    @step
    def end(self):
        pass

if __name__ == "__main__":
    BokehDemo()
Screencast.from.2026-05-14.12-41-29.webm
image

Issue

(no issue currently assigned)

Tests

  • Unit tests added/updated
  • Reproduction script provided (required for Core Runtime)
  • CI passes
  • If tests are impractical: explain why below and provide manual evidence above

Non-Goals

AI Tool Usage

  • No AI tools were used in this contribution
  • AI tools were used (describe below)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant