Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,13 @@ repos:
- types-cffi
- types-openpyxl
- types-jinja2
- bokeh
ci:
autoupdate_schedule: monthly
skip:
- update-algo-selection-code
# Skip mypy stubtest on pre-commit.ci due to maximum size limitations. This is
# unlikely to get better in the future as dependencies keep growing. Local runs
# of pre-commit would still execute stubtest. For CI, we have a separate GitHub
# Action that runs stubtest.
- mypy
1 change: 1 addition & 0 deletions .tools/envs/testenv-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies:
- pandas # run, tests
- plotly>=6.2 # run, tests
- matplotlib # tests
- bokeh # tests
- pybaum>=0.1.2 # run, tests
- scipy>=1.2.1 # run, tests
- sqlalchemy # run, tests
Expand Down
1 change: 1 addition & 0 deletions .tools/envs/testenv-nevergrad.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies:
- pandas # run, tests
- plotly>=6.2 # run, tests
- matplotlib # tests
- bokeh # tests
- pybaum>=0.1.2 # run, tests
- scipy>=1.2.1 # run, tests
- sqlalchemy # run, tests
Expand Down
1 change: 1 addition & 0 deletions .tools/envs/testenv-numpy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies:
- joblib # run, tests
- plotly>=6.2 # run, tests
- matplotlib # tests
- bokeh # tests
- pybaum>=0.1.2 # run, tests
- scipy>=1.2.1 # run, tests
- sqlalchemy # run, tests
Expand Down
1 change: 1 addition & 0 deletions .tools/envs/testenv-others.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies:
- pandas # run, tests
- plotly>=6.2 # run, tests
- matplotlib # tests
- bokeh # tests
- pybaum>=0.1.2 # run, tests
- scipy>=1.2.1 # run, tests
- sqlalchemy # run, tests
Expand Down
1 change: 1 addition & 0 deletions .tools/envs/testenv-pandas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies:
- joblib # run, tests
- plotly>=6.2 # run, tests
- matplotlib # tests
- bokeh # tests
- pybaum>=0.1.2 # run, tests
- scipy>=1.2.1 # run, tests
- sqlalchemy # run, tests
Expand Down
1 change: 1 addition & 0 deletions .tools/envs/testenv-plotly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies:
- numpy >= 2 # run, tests
- pandas # run, tests
- matplotlib # tests
- bokeh # tests
- pybaum>=0.1.2 # run, tests
- scipy>=1.2.1 # run, tests
- sqlalchemy # run, tests
Expand Down
1 change: 1 addition & 0 deletions docs/rtd_environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies:
- furo
- pybaum
- matplotlib
- bokeh
- seaborn
- numpy
- pandas
Expand Down
36 changes: 36 additions & 0 deletions docs/source/how_to/how_to_change_plotting_backend.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@
"\n",
":::\n",
"\n",
":::{tab-item} Bokeh\n",
"\n",
"To select the Bokeh backend, set `backend=\"bokeh\"`.\n",
"\n",
"The returned figure object is a [`bokeh.plotting.figure`](https://docs.bokeh.org/en/latest/docs/reference/plotting/figure.html).\n",
"\n",
"```{note}\n",
"In case of grid plots (such as `convergence_plot` or `slice_plot`), the returned object is a [`bokeh.models.GridPlot`](https://docs.bokeh.org/en/latest/docs/reference/models/plots.html#bokeh.models.GridPlot) object.\n",
"```\n",
"\n",
"```{warning}\n",
"Bokeh applies themes globally. Passing the `template` parameter to a plotting function updates the theme for all existing and future Bokeh plots. If you do not pass `template`, a default template is applied, which will also change the global theme.\n",
"```\n",
"::::"
]
},
Expand Down Expand Up @@ -134,6 +147,29 @@
"source": [
"ax = om.criterion_plot(results, backend=\"matplotlib\")"
]
},
{
"cell_type": "markdown",
"id": "10",
"metadata": {},
"source": [
"## Bokeh"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "11",
"metadata": {},
"outputs": [],
"source": [
"from bokeh.io import output_notebook, show\n",
"\n",
"output_notebook()\n",
"\n",
"p = om.criterion_plot(results, backend=\"bokeh\")\n",
"show(p)"
]
}
],
"metadata": {
Expand Down
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies:
- pandas # run, tests
- plotly>=6.2 # run, tests
- matplotlib # tests
- bokeh # tests
- pybaum>=0.1.2 # run, tests
- scipy>=1.2.1 # run, tests
- sqlalchemy # run, tests
Expand Down
2 changes: 1 addition & 1 deletion src/optimagic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def _is_installed(module_name: str) -> bool:
# ======================================================================================

IS_MATPLOTLIB_INSTALLED = _is_installed("matplotlib")

IS_BOKEH_INSTALLED = _is_installed("bokeh")

# ======================================================================================
# Check if pandas version is newer or equal to version 2.1.0
Expand Down
198 changes: 193 additions & 5 deletions src/optimagic/visualization/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import numpy as np
import plotly.graph_objects as go

from optimagic.config import IS_MATPLOTLIB_INSTALLED
from optimagic.config import IS_BOKEH_INSTALLED, IS_MATPLOTLIB_INSTALLED
from optimagic.exceptions import InvalidPlottingBackendError, NotInstalledError
from optimagic.visualization.plotting_utilities import LineData, MarkerData

if TYPE_CHECKING:
import bokeh
import matplotlib.pyplot as plt


Expand Down Expand Up @@ -409,9 +410,191 @@ def _grid_line_plot_matplotlib(
return axes


def _line_plot_bokeh(
lines: list[LineData],
*,
title: str | None,
xlabel: str | None,
xrange: tuple[float, float] | None,
ylabel: str | None,
yrange: tuple[float, float] | None,
template: str | None,
height: int | None,
width: int | None,
legend_properties: dict[str, Any] | None,
margin_properties: dict[str, Any] | None,
horizontal_line: float | None,
marker: MarkerData | None,
subplot: Any | None = None,
) -> "bokeh.plotting.figure":
from bokeh import themes
from bokeh.io import curdoc
from bokeh.models import Scatter
from bokeh.models.annotations import Legend, LegendItem, Span
from bokeh.plotting import figure

if template is None:
template = "light_minimal"
curdoc().theme = themes.built_in_themes[template]

if subplot is not None:
p = subplot
else:
p = figure()

if title is not None:
p.title.text = title
if xlabel is not None:
p.xaxis.axis_label = xlabel.format(linebreak="\n")
if xrange is not None:
p.x_range.start, p.x_range.end = xrange
if ylabel is not None:
p.yaxis.axis_label = ylabel.format(linebreak="\n")
if yrange is not None:
p.y_range.start, p.y_range.end = yrange
if height is not None:
p.height = height
if width is not None:
p.width = width

_legend_items = []
for line in lines:
glyph = p.line(
line.x,
line.y,
line_color=line.color,
line_width=2,
)

if line.show_in_legend:
_legend_items.append(LegendItem(label=line.name, renderers=[glyph]))

if horizontal_line is not None:
glyph = Span(
location=horizontal_line,
dimension="width",
line_color=p.yaxis.axis_line_color or "gray",
line_width=p.yaxis.axis_line_width or 2,
)
p.add_layout(glyph)

if marker is not None:
glyph = Scatter(
x=marker.x,
y=marker.y,
marker="dot",
fill_color=marker.color,
line_color=marker.color,
size=30,
)
p.add_glyph(glyph)

if _legend_items:
legend_kwargs = legend_properties.copy() if legend_properties else {}
place = legend_kwargs.pop("place", "center")
text = legend_kwargs.pop("title", None)

legend = Legend(items=_legend_items, **(legend_kwargs))
p.add_layout(legend, place=place)
p.legend.title = text

return p


def _grid_line_plot_bokeh(
lines_list: list[list[LineData]],
*,
n_rows: int,
n_cols: int,
titles: list[str] | None,
xlabels: list[str] | None,
xrange: tuple[float, float] | None,
share_x: bool,
ylabels: list[str] | None,
yrange: tuple[float, float] | None,
share_y: bool,
template: str | None,
height: int | None,
width: int | None,
legend_properties: dict[str, Any] | None,
margin_properties: dict[str, Any] | None,
plot_title: str | None,
marker_list: list[MarkerData] | None,
make_subplot_kwargs: dict[str, Any] | None = None,
) -> "bokeh.models.GridPlot":
"""Create a grid of line plots using Bokeh.

Args:
...: All other argument descriptions can be found in the docstring of the
`grid_line_plot` function.

Returns:
A Bokeh gridplot object.

"""

from bokeh.layouts import gridplot
from bokeh.plotting import figure

plots: list[list[figure]] = []

for row in range(n_rows):
subplot_row: list[Any] = []
for col in range(n_cols):
idx = row * n_cols + col
if idx >= len(lines_list):
break

p = figure()

if share_x:
if row > 0:
# Share x-range with the top-most subplot in the same column
p.x_range = plots[0][col].x_range
if row < n_rows - 1:
# Hide tick labels except for subplots in the last row
p.xaxis.major_label_text_font_size = "0pt"
if share_y:
if col > 0:
# Share y-range with the left-most subplot in the same row
p.y_range = subplot_row[0].y_range

# Hide tick labels except for subplots in the first column
p.yaxis.major_label_text_font_size = "0pt"

_line_plot_bokeh(
lines_list[idx],
title=titles[idx] if titles else None,
xlabel=xlabels[idx] if xlabels else None,
xrange=xrange,
ylabel=ylabels[idx] if ylabels else None,
yrange=yrange,
template=template,
height=None,
width=None,
legend_properties=legend_properties,
margin_properties=None,
horizontal_line=None,
marker=marker_list[idx] if marker_list else None,
subplot=p,
)

subplot_row.append(p)
plots.append(subplot_row)

grid = gridplot( # type: ignore[call-overload]
plots,
height=height // n_rows if height else None,
width=width // n_cols if width else None,
toolbar_location="right",
)

return grid


def line_plot(
lines: list[LineData],
backend: Literal["plotly", "matplotlib"] = "plotly",
backend: Literal["plotly", "matplotlib", "bokeh"] = "plotly",
*,
title: str | None = None,
xlabel: str | None = None,
Expand Down Expand Up @@ -474,7 +657,7 @@ def line_plot(

def grid_line_plot(
lines_list: list[list[LineData]],
backend: Literal["plotly", "matplotlib"] = "plotly",
backend: Literal["plotly", "matplotlib", "bokeh"] = "plotly",
*,
n_rows: int,
n_cols: int,
Expand Down Expand Up @@ -562,18 +745,23 @@ def grid_line_plot(
_line_plot_matplotlib,
_grid_line_plot_matplotlib,
),
"bokeh": (
IS_BOKEH_INSTALLED,
_line_plot_bokeh,
_grid_line_plot_bokeh,
),
}


@overload
def _get_plot_function(
backend: Literal["plotly", "matplotlib"], grid_plot: Literal[False]
backend: Literal["plotly", "matplotlib", "bokeh"], grid_plot: Literal[False]
) -> LinePlotFunction: ...


@overload
def _get_plot_function(
backend: Literal["plotly", "matplotlib"], grid_plot: Literal[True]
backend: Literal["plotly", "matplotlib", "bokeh"], grid_plot: Literal[True]
) -> GridLinePlotFunction: ...


Expand Down
Loading
Loading