Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions solara/lab/components/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .chat import ChatBox, ChatInput, ChatMessage # noqa: F401
from .confirmation_dialog import ConfirmationDialog # noqa: F401
from .figurebokeh import FigureBokeh # noqa: F401
from .input_date import InputDate, InputDateRange # noqa: F401
from .input_time import InputTime as InputTime
from .menu import ClickMenu, ContextMenu, Menu # noqa: F401 F403
Expand Down
27 changes: 27 additions & 0 deletions solara/lab/components/bokehloaded.vue
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>
98 changes: 98 additions & 0 deletions solara/lab/components/figurebokeh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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


@solara.component
def FigureBokeh(
fig,
dependencies=None,
light_theme: str | Theme = "light_minimal",
dark_theme: str | Theme = "dark_minimal",
):
"""
Display a Bokeh figure or Plot.

## Example

```solara
import solara
import solara.lab
from bokeh.plotting import figure

@solara.component
def Page():
p = figure(width=600, height=400)
p.line(x=[1, 2, 3, 4, 5], y=[2, 4, 2, 7, 9])

return solara.lab.FigureBokeh(p)
```

For performance reasons, you might want to pass in a list of dependencies that indicate when
the figure changed, to avoid re-rendering it on every render.

## Arguments

* fig: `Plot` or `figure` object to display.
* dependencies: List of dependencies to watch for changes, if None, will rerender when `fig` is changed.
* light_theme: The name or `bokeh.themes.Theme` object to use for light mode. Defaults to `"light_minimal"`.
* dark_theme: The name or `bokeh.themes.Theme` object to use for dark mode. Defaults to `"dark_minimal"`.
"""
loaded = solara.use_reactive(False)
dark = solara.lab.use_dark_effective()
output_notebook(hide_banner=True)
BokehLoaded(loaded=loaded.value, on_loaded=loaded.set)

# TODO: there's an error with deletion on the doc. do we need to modify the underlying class?
fig_element = BokehModel.element(model=fig)

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
with 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)
setattr(fig_model, place, attr[length:])

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])

if loaded.value:
return fig_element
else:
# NOTE: the returned object will be a v.Sheet until Bokeh is loaded
# BUG: this will show the JS error temporarily before loading
with solara.Card(margin=0, elevation=0):
with solara.Row(justify="center"):
solara.SpinnerSolara(size="200px")
4 changes: 2 additions & 2 deletions solara/website/pages/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

route_order = ["/", "showcase", "documentation", "apps", "contact", "changelog", "roadmap", "pricing", "our_team", "careers", "about", "scale_ipywidgets"]


_redirects = {
"/docs": "/documentation/getting_started/introduction",
"/docs/installing": "/documentation/getting_started/installing",
Expand Down Expand Up @@ -118,6 +117,8 @@
"/documentation/examples/fullscreen/multipage": "/apps/multipage",
"/examples/fullscreen/scatter": "apps/scatter",
"/documentation/examples/fullscreen/scatter": "/apps/scatter",
"/examples/fullscreen/scatter-bokeh": "/apps/scatter-bokeh",
"/documentation/examples/fullscreen/scatter_bokeh": "/apps/scatter-bokeh",
"/examples/fullscreen/scrolling": "/apps/scrolling",
"/documentation/examples/fullscreen/scrolling": "/apps/scrolling",
"/examples/fullscreen/tutorial-streamlit": "/apps/tutorial-streamlit",
Expand Down Expand Up @@ -211,7 +212,6 @@
"/api/title": "/documentation/components/page/title",
}


server._redirects = _redirects
autorouting._redirects = _redirects

Expand Down
129 changes: 129 additions & 0 deletions solara/website/pages/apps/scatter-bokeh.py
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
solara.provide_cross_filter()

# 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.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")
16 changes: 16 additions & 0 deletions solara/website/pages/documentation/components/lab/figurebokeh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
# FigureBokeh

Display a Bokeh figure.

"""

import solara
from solara.website.components import NoPage
from solara.website.utils import apidoc

title = "FigureBokeh"

__doc__ += apidoc(solara.lab.components.figurebokeh.FigureBokeh.f) # type: ignore

Page = NoPage
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
redirect = "/apps/scatter-bokeh"

Page = True
47 changes: 47 additions & 0 deletions solara/website/pages/documentation/examples/visualization/bokeh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""# 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():
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])