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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ build/
tensorboard_logs/
examples/_test*
.mypy_cache/

# OS / editor cruft
.DS_Store
Thumbs.db
*.swp
*.swo
.idea/
76 changes: 76 additions & 0 deletions examples/bokeh_colab.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": "# livelossplot + Bokeh in Colab (issue #109 verification)\n\nOpen in Colab: <a href=\"https://colab.research.google.com/github/stared/livelossplot/blob/i109-bokeh-colab/examples/bokeh_colab.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\"/></a>\n\nInstalls livelossplot from the `i109-bokeh-colab` branch on GitHub (not PyPI), so the in-progress Colab fix for `BokehPlot` is exercised."
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": "!pip install --quiet --upgrade git+https://github.com/stared/livelossplot.git@i109-bokeh-colab"
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"import livelossplot\n",
"print('livelossplot:', livelossplot.__version__)\n",
"print('running in Colab:', 'google.colab' in sys.modules)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from time import sleep\n",
"import numpy as np\n",
"\n",
"from livelossplot import PlotLosses\n",
"from livelossplot.outputs import BokehPlot\n",
"\n",
"plotlosses = PlotLosses(outputs=[BokehPlot()])\n",
"\n",
"for i in range(10):\n",
" plotlosses.update({\n",
" 'acc': 1 - np.random.rand() / (i + 2.),\n",
" 'val_acc': 1 - np.random.rand() / (i + 0.5),\n",
" 'loss': 1. / (i + 2.),\n",
" 'val_loss': 1. / (i + 0.5),\n",
" })\n",
" plotlosses.send()\n",
" sleep(0.5)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## What you should see\n",
"\n",
"Two Bokeh panels (Accuracy, Loss) that **redraw with growing data lines** every ~0.5s. Before this fix, the panels rendered as empty skeletons and never updated (issue #109).\n",
"\n",
"If something still looks broken, please paste the failure into the issue."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
49 changes: 49 additions & 0 deletions livelossplot/outputs/bokeh_plot.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from typing import List, Dict, Tuple

from livelossplot.main_logger import MainLogger, LogItem
Expand Down Expand Up @@ -28,11 +29,17 @@ def __init__(
self.skip_first = skip_first # think about it
self.figures = {}
self.is_notebook = False
self.is_colab = 'google.colab' in sys.modules
self.output_file = output_file
self.colors = palettes.Category10[10]
self._colab_handle = None

def send(self, logger: MainLogger) -> None:
"""Draw figures with metrics and show"""
if self.is_colab:
self._send_colab(logger)
return

log_groups = logger.grouped_log_history()
new_grid_plot = False
for idx, (group_name, group_logs) in enumerate(log_groups.items(), start=1):
Expand All @@ -48,6 +55,46 @@ def send(self, logger: MainLogger) -> None:
else:
self.plotting.save(self.grid)

def _send_colab(self, logger: MainLogger) -> None:
"""Colab actively blocks Jupyter Comms, so push_notebook is dead there.
Render Bokeh into a complete HTML doc and embed it via <iframe srcdoc>:
the iframe is a self-contained browser context, so BokehJS finds its
sibling JSON-data element correctly (no DOM fragmentation from Colab's
output handling). Updates via display_id atomically replace the iframe
wrapper, browser rebuilds the inner document — no flicker."""
import html as html_lib

from bokeh.embed import file_html
from bokeh.resources import CDN
from IPython.display import HTML, display

log_groups = logger.grouped_log_history()
self.figures = {}
for group_name, group_logs in log_groups.items():
fig = self.plotting.figure(title=group_name)
self.figures[group_name] = self._draw_metric_subplot(fig, group_logs)
rows, row = [], []
for idx, fig in enumerate(self.figures.values(), start=1):
row.append(fig)
if idx % self.max_cols == 0:
rows.append(row)
row = []
if row:
rows.append(row)
grid = self.plotting.gridplot(rows, sizing_mode='scale_width', width=self.plot_width, height=self.plot_height)
bokeh_doc = file_html(grid, CDN, 'livelossplot')
iframe_height = self.plot_height * len(rows) + 60
iframe_html = (
f'<iframe srcdoc="{html_lib.escape(bokeh_doc, quote=True)}" '
f'style="width: 100%; height: {iframe_height}px; border: none;" '
f'sandbox="allow-scripts allow-same-origin"></iframe>'
)
payload = HTML(iframe_html)
if self._colab_handle is None:
self._colab_handle = display(payload, display_id=True)
else:
self._colab_handle.update(payload)

def _draw_metric_subplot(self, fig, group_logs: Dict[str, List[LogItem]]):
"""
Args:
Expand Down Expand Up @@ -101,6 +148,8 @@ def _set_output_mode(self, mode: str):
"""Set notebook or script mode"""
self.is_notebook = mode == 'notebook'
if self.is_notebook:
# Bokeh auto-detects Colab in recent versions; passing
# notebook_type='colab' explicitly raises in Bokeh 3.x.
self.io.output_notebook()
else:
self.io.output_file(self.output_file)
Loading