-
Notifications
You must be signed in to change notification settings - Fork 197
feat: Bokeh support as FigureBokeh #997
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
rileythai
wants to merge
12
commits into
widgetti:master
Choose a base branch
from
rileythai:figurebokeh
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 1 commit
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
9272987
feat: bokeh support
rileythai cb23a9c
fix: no main return in example
rileythai 3efdbe5
fix: no main return in example ((part II)
rileythai f343741
fix: FigureBokeh as component
rileythai ebff57d
feat: FigureBokeh docs page
rileythai 47f5221
fix: conditional hook (provide) in scatter-bokeh example
rileythai f050a50
fix: routing for scatter-bokeh example
rileythai 33ffa52
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 5355c93
fix: patched BokehModel
rileythai da7476f
Merge branch 'master' into figurebokeh
rileythai 95ae355
fix: match arguments
rileythai d20333e
doc: add pitfalls to FigureBokeh
rileythai File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| <template> | ||
| <div v-if="!loaded"> | ||
| <div class="loading-text"></div> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script> | ||
| module.exports = { | ||
| mounted() { | ||
| const check = () => { | ||
| if (window.Bokeh) { | ||
| this.loaded = true; | ||
| return; | ||
| } | ||
| setTimeout(check, 100); | ||
| }; | ||
| check(); | ||
| }, | ||
| }; | ||
| </script> | ||
|
|
||
| <style> | ||
| .loading-text { | ||
| margin-top: 10px; | ||
| font-size: 16px; | ||
| } | ||
| </style> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| from typing import Callable | ||
|
|
||
| import solara | ||
| from solara.components.component_vue import component_vue | ||
| from bokeh.io import output_notebook | ||
| from bokeh.models import Plot | ||
| from bokeh.plotting import figure | ||
| from bokeh.themes import Theme | ||
| from jupyter_bokeh import BokehModel | ||
|
|
||
|
|
||
| @component_vue("bokehloaded.vue") | ||
| def BokehLoaded(loaded: bool, on_loaded: Callable[[bool], None]): | ||
| pass | ||
|
|
||
|
|
||
| def FigureBokeh( | ||
| fig, | ||
| dependencies=None, | ||
| light_theme: str | Theme = "light_minimal", | ||
| dark_theme: str | Theme = "dark_minimal", | ||
| ): | ||
| # NOTE: no docstring because not a component. | ||
| loaded = solara.use_reactive(False) | ||
| dark = solara.lab.use_dark_effective() | ||
| fig_key = solara.use_uuid4([]) | ||
| output_notebook(hide_banner=True) | ||
| BokehLoaded(loaded=loaded.value, on_loaded=loaded.set) | ||
| if loaded.value: | ||
| # TODO: there's an error with deletion on the doc. do we need to modify the underlying class? | ||
| fig_element = BokehModel.element(model=fig).key(fig_key) | ||
|
|
||
| def update_data(): | ||
| fig_widget: BokehModel = solara.get_widget(fig_element) | ||
| fig_model: Plot | figure = fig_widget._model # base class for figure | ||
| if fig != fig_model: # don't run through on first startup | ||
| # pause until all updates complete | ||
| fig_model.hold_render = True | ||
|
|
||
| # extend renderer set and cull previous | ||
| length = len(fig_model.renderers) | ||
| fig_model.renderers.extend(fig.renderers) | ||
| fig_model.renderers = fig_model.renderers[length:] | ||
|
|
||
| # similarly update plot layout properties | ||
| places = ["above", "below", "center", "left", "right"] | ||
| for place in places: | ||
| attr = getattr(fig_model, place) | ||
| newattr = getattr(fig, place) | ||
| length = len(attr) | ||
| attr.extend(newattr) | ||
| if place == "right": | ||
| fig_model.hold_render = False | ||
| setattr(fig_model, place, attr[length:]) | ||
| return | ||
|
|
||
| def update_theme(): | ||
| # NOTE: using bokeh.io.curdoc and this _document prop will point to the same object | ||
| fig_widget: BokehModel = solara.get_widget(fig_element) | ||
| if dark: | ||
| fig_widget._document.theme = dark_theme | ||
| else: | ||
| fig_widget._document.theme = light_theme | ||
|
|
||
| solara.use_effect(update_data, dependencies or fig) | ||
| solara.use_effect(update_theme, [dark, loaded.value]) | ||
| return fig_element | ||
| else: | ||
| # NOTE: we don't return this as to not break effect callbacks outside this function | ||
| with solara.Card(margin=0, elevation=0): | ||
| # the card expands to fit space | ||
| with solara.Row(justify="center"): | ||
| solara.SpinnerSolara(size="200px") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| import pathlib | ||
| import sys | ||
|
|
||
| from typing import Optional, cast | ||
|
|
||
| import vaex | ||
| import vaex.datasets | ||
|
|
||
| import solara | ||
| import solara.lab | ||
| from bokeh.models import ColumnDataSource | ||
| from bokeh.plotting import figure | ||
| from bokeh.transform import linear_cmap, factor_cmap | ||
|
|
||
| github_url = solara.util.github_url(__file__) | ||
| if sys.platform != "emscripten": | ||
| pycafe_url = solara.util.pycafe_url(path=pathlib.Path(__file__), requirements=["vaex", "bokeh"]) | ||
| else: | ||
| pycafe_url = None | ||
|
|
||
| df_sample = vaex.datasets.titanic() | ||
|
|
||
|
|
||
| class State: | ||
| color = solara.reactive(cast(Optional[str], None)) | ||
| x = solara.reactive(cast(Optional[str], None)) | ||
| y = solara.reactive(cast(Optional[str], None)) | ||
| df = solara.reactive(cast(Optional[vaex.DataFrame], None)) | ||
|
|
||
| @staticmethod | ||
| def load_sample(): | ||
| State.x.value = "age" | ||
| State.y.value = "fare" | ||
| State.color.value = "body" | ||
| State.df.value = df_sample | ||
|
|
||
| @staticmethod | ||
| def reset(): | ||
| State.df.value = None | ||
|
|
||
|
|
||
| @solara.component | ||
| def Page(): | ||
| df = State.df.value | ||
| selected, on_selected = solara.use_state({"x": [0, 0]}) # noqa: SH101 | ||
|
|
||
| # the PivotTable will set this cross filter | ||
| filter, _ = solara.use_cross_filter(id(df), name="scatter") | ||
|
|
||
| # only apply the filter if the filter or dataframe changes | ||
| def filter_df(): | ||
| if (filter is not None) and (df is not None): | ||
| return df[filter] | ||
| else: | ||
| return df | ||
|
|
||
| dff = solara.use_memo(filter_df, dependencies=[df, filter]) | ||
|
|
||
| with solara.AppBar(): | ||
| solara.lab.ThemeToggle() | ||
| with solara.Sidebar(): | ||
| with solara.Card("Controls", margin=0, elevation=0): | ||
| with solara.Column(): | ||
| with solara.Row(): | ||
| solara.Button("Sample dataset", color="primary", text=True, outlined=True, on_click=State.load_sample, disabled=df is not None) | ||
| solara.Button("Clear dataset", color="primary", text=True, outlined=True, on_click=State.reset) | ||
|
|
||
| if df is not None: | ||
| columns = df.get_column_names() | ||
| solara.Select("Column x", values=columns, value=State.x) | ||
| solara.Select("Column y", values=columns, value=State.y) | ||
| solara.Select("Color", values=columns, value=State.color) | ||
|
|
||
| solara.provide_cross_filter() | ||
| solara.PivotTable(df, ["pclass"], ["sex"], selected=selected, on_selected=on_selected) | ||
|
|
||
| if dff is not None: | ||
| source = ColumnDataSource( | ||
| data={ | ||
| "x": dff[State.x.value].values, | ||
| "y": dff[State.y.value].values, | ||
| "z": dff[State.color.value].values, | ||
| } | ||
| ) | ||
| if State.x.value and State.y.value: | ||
| p = figure(x_axis_label=State.x.value, y_axis_label=State.y.value, width_policy="max", height=700) | ||
|
|
||
| # add a scatter, colorbar, and mapper | ||
| color_expr = dff[State.color.value] | ||
| if (color_expr.dtype == "string") or (color_expr.dtype == "bool"): | ||
| mapper = factor_cmap | ||
| factors = color_expr.unique() | ||
| try: | ||
| factors.remove(None) | ||
| except ValueError: | ||
| pass | ||
| args = dict(palette=f"Viridis{min(11, max(3, color_expr.nunique()))}", factors=factors) | ||
| else: | ||
| mapper = linear_cmap | ||
| args = dict(palette="Viridis256", low=color_expr.min()[()], high=color_expr.max()[()]) | ||
|
|
||
| s = p.scatter(source=source, x="x", y="y", size=12, fill_color=mapper(field_name="z", **args)) | ||
| p.add_layout(s.construct_color_bar(title=State.color.value, label_standoff=6, padding=5, border_line_color=None), "right") | ||
|
|
||
| solara.lab.FigureBokeh(p, dark_theme="carbon") | ||
|
|
||
| else: | ||
| solara.Warning("Select x and y columns") | ||
|
|
||
| else: | ||
| solara.Info("No data loaded, click on the sample dataset button to load a sample dataset, or upload a file.") | ||
|
|
||
| with solara.Column(style={"max-width": "400px"}): | ||
| solara.Button(label="View source", icon_name="mdi-github-circle", attributes={"href": github_url, "target": "_blank"}, text=True, outlined=True) | ||
| if sys.platform != "emscripten": | ||
| solara.Button( | ||
| label="Edit this example live on py.cafe", | ||
| icon_name="mdi-coffee-to-go-outline", | ||
| attributes={"href": pycafe_url, "target": "_blank"}, | ||
| text=True, | ||
| outlined=True, | ||
| ) | ||
|
|
||
|
|
||
| @solara.component | ||
| def Layout(children): | ||
| route, routes = solara.use_route() | ||
| dark_effective = solara.lab.use_dark_effective() | ||
| return solara.AppLayout(children=children, toolbar_dark=dark_effective, color=None) # if dark_effective else "primary") |
3 changes: 3 additions & 0 deletions
3
solara/website/pages/documentation/examples/fullscreen/scatter_bokeh.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| redirect = "/apps/scatter-bokeh" | ||
|
|
||
| Page = True |
48 changes: 48 additions & 0 deletions
48
solara/website/pages/documentation/examples/visualization/bokeh.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| """# Scatter plot using Bokeh | ||
|
|
||
| This example shows how to use Bokeh to create a scatter plot and a select box to do some filtering. | ||
|
|
||
| Inspired by the bokeh documentation. | ||
| """ | ||
|
|
||
| from bokeh.models import ColorBar, DataRange1d, LinearColorMapper | ||
|
|
||
| from bokeh.plotting import figure, ColumnDataSource | ||
| from bokeh.sampledata import penguins | ||
|
|
||
| import solara | ||
|
|
||
| title = "Scatter plot using Bokeh" | ||
|
|
||
| df = penguins.data | ||
|
|
||
|
|
||
| @solara.component | ||
| def Page(): | ||
| all_species = df["species"].unique().tolist() | ||
| species = solara.use_reactive(all_species[0]) | ||
| with solara.Div() as main: | ||
|
rileythai marked this conversation as resolved.
Outdated
|
||
| solara.Select(label="Species", value=species, values=all_species) | ||
| dff = df[df["species"] == species.value] | ||
|
|
||
| source = ColumnDataSource( | ||
| data={ | ||
| "x": dff["bill_length_mm"].values, | ||
| "y": dff["bill_depth_mm"].values, | ||
| "z": dff["body_mass_g"].values, | ||
| } | ||
| ) | ||
|
|
||
| # make a figure | ||
| p = figure( | ||
| x_range=DataRange1d(), y_range=DataRange1d(), x_axis_label="Bill length [mm]", y_axis_label="Bill depth [mm]", width_policy="max", height=400 | ||
| ) | ||
|
|
||
| # add a scatter, colorbar, and mapper | ||
| mapper = LinearColorMapper(palette="Viridis256", low=dff["body_mass_g"].min(), high=dff["body_mass_g"].max()) | ||
| cb = ColorBar(color_mapper=mapper, title="Body mass [g]") | ||
| p.scatter(source=source, x="x", y="y", marker="circle", size=8, fill_color={"field": "z", "transform": mapper}) | ||
| p.add_layout(cb, "right") | ||
|
|
||
| solara.lab.FigureBokeh(p, dark_theme="carbon", dependencies=[species]) | ||
| return main | ||
|
rileythai marked this conversation as resolved.
Outdated
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.