-
Notifications
You must be signed in to change notification settings - Fork 146
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
281 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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 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 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 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 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 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: | ||
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 |