diff --git a/.gitignore b/.gitignore index 4740b51..76d8cb3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,10 @@ build/ tensorboard_logs/ examples/_test* .mypy_cache/ + +# OS / editor cruft +.DS_Store +Thumbs.db +*.swp +*.swo +.idea/ diff --git a/examples/bokeh_colab.ipynb b/examples/bokeh_colab.ipynb new file mode 100644 index 0000000..128e703 --- /dev/null +++ b/examples/bokeh_colab.ipynb @@ -0,0 +1,76 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# livelossplot + Bokeh in Colab (issue #109 verification)\n\nOpen in Colab: \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 +} \ No newline at end of file diff --git a/livelossplot/outputs/bokeh_plot.py b/livelossplot/outputs/bokeh_plot.py index 9b8f12d..105a540 100644 --- a/livelossplot/outputs/bokeh_plot.py +++ b/livelossplot/outputs/bokeh_plot.py @@ -1,3 +1,4 @@ +import sys from typing import List, Dict, Tuple from livelossplot.main_logger import MainLogger, LogItem @@ -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): @@ -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 ' + ) + 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: @@ -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)