├── .dockerignore ├── .gitignore ├── .lightning ├── .lightningignore ├── README.md ├── app.py ├── assets ├── awesome-panel-lightning.gif └── awesome-panel-lightning.mp4 ├── pages ├── __init__.py ├── big_data_viz.py ├── cross_filter.py ├── shared.py └── streaming.py ├── panel_lightning.py └── requirements.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | .venv -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | .venv/ 162 | .vscode/ 163 | -------------------------------------------------------------------------------- /.lightning: -------------------------------------------------------------------------------- 1 | name: awesome-panel-lightning 2 | -------------------------------------------------------------------------------- /.lightningignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .git 3 | .gitignore 4 | .venv 5 | .vscode 6 | assets 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PyPI - License](https://img.shields.io/pypi/l/panel-highcharts) ![Style Black](https://warehouse-camo.ingress.cmh1.psfhosted.org/fbfdc7754183ecf079bc71ddeabaf88f6cbc5c00/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f64652532307374796c652d626c61636b2d3030303030302e737667) [![Follow on Twitter](https://img.shields.io/twitter/follow/MarcSkovMadsen.svg?style=social)](https://twitter.com/MarcSkovMadsen) 2 | 3 | # ⚡ Awesome Panel Lightning 4 | 5 | Repository demonstrating the power of combining [Panel](https://panel.holoviz.org) and [lightning.ai](https://lightning.ai/) to build, scale and deploy powerful machinelearning and deeplearning data apps. 6 | 7 | [![awesome-panel-lightning tour](assets/awesome-panel-lightning.gif)](https://01g6g6axybbecmx47d9m419pe5.litng-ai-03.litng.ai/view?id=01g6g6axybbecmx47d9m419pe5) 8 | 9 | Check out the live [Example App](https://01g6g6axybbecmx47d9m419pe5.litng-ai-03.litng.ai/view?id=01g6g6axybbecmx47d9m419pe5). 10 | 11 | ## ⚙️ Install Locally 12 | 13 | ```bash 14 | git clone https://github.com/MarcSkovMadsen/awesome-panel-lightning.git 15 | cd awesome-panel-lightning 16 | ``` 17 | 18 | [Create](https://realpython.com/python-virtual-environments-a-primer/#create-it) and [activate](https://realpython.com/python-virtual-environments-a-primer/#activate-it) your local environment. 19 | 20 | Then install the requirements via 21 | 22 | ```bash 23 | pip install -r requirements.txt 24 | ``` 25 | 26 | Finally you can update the `name` of the app in the [.lightning](.lightning) file. 27 | 28 | ## 🏃 Run Locally 29 | 30 | Activate your virtual environment and run 31 | 32 | ```bash 33 | lightning run app app.py 34 | ``` 35 | 36 | ## ☁️ Run in lightning.ai cloud 37 | 38 | Activate your virtual environment and run 39 | 40 | ```bash 41 | lightning run app app.py --cloud 42 | ``` 43 | 44 | and follow the instructions 45 | 46 | ## Add or Update the App Pages 47 | 48 | The different pages of the app of configured in the [app.py](`app.py`) file. 49 | 50 | Most pages are configured as files. But you can also use functions that return a Panel `Viewable` 51 | like the `introduction` function. 52 | 53 | ```python 54 | class LitApp(lapp.LightningFlow): 55 | def __init__(self): 56 | super().__init__() 57 | self.lit_intro = LitPanelPage(page=introduction, parallel=True) 58 | self.lit_big_data_viz = LitPanelPage(page="pages/big_data_viz.py", parallel=True) 59 | self.lit_crossfilter = LitPanelPage(page="pages/cross_filter.py", parallel=True) 60 | self.lit_streaming = LitPanelPage(page="pages/streaming.py", parallel=True) 61 | 62 | def run(self): 63 | self.lit_intro.run() 64 | self.lit_big_data_viz.run() 65 | self.lit_crossfilter.run() 66 | self.lit_streaming.run() 67 | 68 | def configure_layout(self): 69 | return [ 70 | self.lit_intro.get_tab(name="Introduction"), 71 | self.lit_crossfilter.get_tab(name="Crossfiltering"), 72 | self.lit_streaming.get_tab(name="Streaming"), 73 | self.lit_big_data_viz.get_tab(name="Big Data Viz"), 74 | ] 75 | 76 | app = lapp.LightningApp(LitApp()) 77 | ``` 78 | 79 | ## 🐛 Active issues 80 | 81 | - [LightningFlow expects too fast an initial response from other servers](https://github.com/Lightning-AI/lightning/issues/13381) -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | """A lightning.ai app serving multiple Panel pages""" 2 | from __future__ import annotations 3 | 4 | import lightning_app as lapp 5 | import panel as pn 6 | 7 | from panel_lightning import LitPanelPage 8 | 9 | def introduction(): 10 | ACCENT = "#792EE5" 11 | 12 | pn.extension(sizing_mode="stretch_width") 13 | 14 | info = """ 15 | **[Panel](https://panel.holoviz.org/)** and the [HoloViz](https://holoviz.org/) ecosystem provides unique and powerful features such as big data viz via [DataShader](https://datashader.org/), easy cross filtering via [HoloViews](https://holoviews.org/), streaming and much more. 16 | 17 | Panel works with the tools you know and love ❤️. Panel ties into the PyData and Jupyter ecosystems as you can develop in notebooks and use ipywidgets. You can also develop in .py files. 18 | 19 | Panel is one of the 4 most popular data app frameworks in Python with [more than 400.000 downloads a month](https://pyviz.org/tools.html#dashboarding). It's especially popular in the scientific community. 20 | 21 | Panel is used by for example Rapids to power [CuxFilter](https://github.com/rapidsai/cuxfilter), a CuDF based big data viz framework. 22 | 23 | Panel can be deployed on your favorite server or cloud including **[lightning.ai](https://lightning.ai)**. 24 | """ 25 | 26 | def lightning_string(value): 27 | return "# " + "⚡" * value 28 | 29 | lightning = pn.widgets.IntSlider(value=5, start=0, end=10, name="⚡") 30 | ilightning_string = pn.bind(lightning_string, value=lightning) 31 | 32 | check_out = """ 33 | Check out my site [awesome-panel.org](https://awesome-panel.org) for more inspiration. 34 | """ 35 | 36 | component = pn.Column(info, lightning, ilightning_string, check_out, sizing_mode="stretch_width") 37 | 38 | return pn.template.FastListTemplate( 39 | site="Awesome Panel", 40 | title="Introduction", 41 | accent_base_color=ACCENT, 42 | header_background=ACCENT, 43 | main=[component], 44 | ) 45 | 46 | 47 | class LitApp(lapp.LightningFlow): 48 | def __init__(self): 49 | super().__init__() 50 | self.lit_intro = LitPanelPage(page=introduction, parallel=True) 51 | self.lit_big_data_viz = LitPanelPage(page="pages/big_data_viz.py", parallel=True) 52 | self.lit_crossfilter = LitPanelPage(page="pages/cross_filter.py", parallel=True) 53 | self.lit_streaming = LitPanelPage(page="pages/streaming.py", parallel=True) 54 | 55 | def run(self): 56 | self.lit_intro.run() 57 | self.lit_big_data_viz.run() 58 | self.lit_crossfilter.run() 59 | self.lit_streaming.run() 60 | 61 | def configure_layout(self): 62 | return [ 63 | self.lit_intro.get_tab(name="Introduction"), 64 | self.lit_crossfilter.get_tab(name="Crossfiltering"), 65 | self.lit_streaming.get_tab(name="Streaming"), 66 | self.lit_big_data_viz.get_tab(name="Big Data Viz"), 67 | ] 68 | 69 | 70 | 71 | # lightning run app app.py 72 | app = lapp.LightningApp(LitApp()) 73 | -------------------------------------------------------------------------------- /assets/awesome-panel-lightning.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-panel/awesome-panel-lightning/58458c420c96d161b5211c5341dfecd438acc4df/assets/awesome-panel-lightning.gif -------------------------------------------------------------------------------- /assets/awesome-panel-lightning.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-panel/awesome-panel-lightning/58458c420c96d161b5211c5341dfecd438acc4df/assets/awesome-panel-lightning.mp4 -------------------------------------------------------------------------------- /pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-panel/awesome-panel-lightning/58458c420c96d161b5211c5341dfecd438acc4df/pages/__init__.py -------------------------------------------------------------------------------- /pages/big_data_viz.py: -------------------------------------------------------------------------------- 1 | import panel as pn 2 | 3 | pn.extension() 4 | 5 | pn.Column( 6 | "# Example coming up", 7 | pn.pane.HTML( 8 | "", 9 | sizing_mode="stretch_both", 10 | ), 11 | sizing_mode="stretch_both", 12 | ).servable() -------------------------------------------------------------------------------- /pages/cross_filter.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, Dict 4 | 5 | import holoviews as hv 6 | import panel as pn 7 | from bokeh.sampledata import iris 8 | from holoviews import opts 9 | 10 | pn.extension() 11 | 12 | print("starting cross filter") 13 | 14 | ACCENT = "#792EE5" 15 | LIGHTNING_SPINNER_URL = ( 16 | "https://cdn.jsdelivr.net/gh/MarcSkovMadsen/awesome-panel-assets@master/spinners/material/" 17 | "bar_chart_lightning_purple.svg" 18 | ) 19 | LIGHTNING_SPINNER = pn.pane.HTML( 20 | f"" 21 | ) 22 | OPTS: Dict[str, Dict[str, Any]] = { 23 | "all": { 24 | "scatter": {"color": ACCENT, "responsive": True, "size": 10}, 25 | "hist": {"color": ACCENT, "responsive": True}, 26 | }, 27 | "bokeh": { 28 | "scatter": {"tools": ["hover"], "active_tools": ["box_select"]}, 29 | "hist": {"tools": ["hover"], "active_tools": ["box_select"]}, 30 | }, 31 | } 32 | 33 | MAXIMIZE_FIRST_PANEL = """ 34 | .bk-root { height: calc( 100vh - 150px ) !important; } 35 | """ 36 | NO_HEADER_RAW_CSS = """ 37 | nav#header {display: None} 38 | """ 39 | 40 | pn.config.raw_css.append(NO_HEADER_RAW_CSS) 41 | pn.config.raw_css.append(MAXIMIZE_FIRST_PANEL) 42 | 43 | 44 | component = pn.Column(LIGHTNING_SPINNER, sizing_mode="stretch_both") 45 | 46 | def load(*args): 47 | 48 | pn.extension(sizing_mode="stretch_width") 49 | hv.extension("bokeh") 50 | dataset = hv.Dataset(iris.flowers) 51 | 52 | scatter = hv.Scatter(dataset, kdims=["sepal_length"], vdims=["sepal_width"]).opts( 53 | responsive=True 54 | ) 55 | hist = hv.operation.histogram(dataset, dimension="petal_width", normed=False) 56 | 57 | selection_linker = hv.selection.link_selections.instance() 58 | scatter = selection_linker(scatter).opts( 59 | opts.Scatter(**OPTS["all"]["scatter"], **OPTS["bokeh"]["scatter"]) 60 | ) 61 | hist = selection_linker(hist).opts( 62 | opts.Histogram(**OPTS["all"]["hist"], **OPTS["bokeh"]["hist"], height=300) 63 | ) 64 | component[:] = [ 65 | pn.pane.HoloViews(scatter, sizing_mode="stretch_both"), 66 | hist, 67 | ] 68 | 69 | 70 | pn.state.onload(load) 71 | 72 | pn.template.FastListTemplate( 73 | title="Cross Filter", theme="dark", theme_toggle=False, main=[component] 74 | ).servable() 75 | -------------------------------------------------------------------------------- /pages/shared.py: -------------------------------------------------------------------------------- 1 | import panel as pn 2 | 3 | LIGHTNING_PURPLE = "#792EE5" 4 | 5 | LIGHTNING_SPINNER_URL = ( 6 | "https://cdn.jsdelivr.net/gh/MarcSkovMadsen/awesome-panel-assets@master/spinners/material/" 7 | "bar_chart_lightning_purple.svg" 8 | ) 9 | 10 | def get_lightning_spinner(url=LIGHTNING_SPINNER_URL, **params) -> pn.pane.HTML: 11 | """Returns a loding spinner for your lightning.ai app""" 12 | return pn.pane.HTML( 13 | f"", **params 14 | ) 15 | 16 | LIGHTNING_SPINNER = get_lightning_spinner(sizing_mode="fixed", width=100, height=100) 17 | -------------------------------------------------------------------------------- /pages/streaming.py: -------------------------------------------------------------------------------- 1 | import panel as pn 2 | import numpy as np 3 | 4 | pn.extension() 5 | 6 | print("starting streaming") 7 | 8 | ACCENT = "#792EE5" 9 | LIGHTNING_SPINNER_URL = ( 10 | "https://cdn.jsdelivr.net/gh/MarcSkovMadsen/awesome-panel-assets@master/spinners/material/" 11 | "bar_chart_lightning_purple.svg" 12 | ) 13 | LIGHTNING_SPINNER = pn.pane.HTML( 14 | f"" 15 | ) 16 | 17 | ncols = 4 18 | objects = [LIGHTNING_SPINNER] 19 | grid = pn.layout.GridBox(objects=objects, ncols=ncols) 20 | 21 | 22 | def load(*args): 23 | objects = [ 24 | pn.indicators.Trend( 25 | title=f"⚡ Panel {index+1}", 26 | data={"x": np.arange(50), "y": np.random.rand(50).cumsum()}, 27 | plot_type="area", 28 | sizing_mode="stretch_both", 29 | plot_color=ACCENT, 30 | ) 31 | for index in range(0, ncols**2) 32 | ] 33 | 34 | grid.objects = objects 35 | 36 | def stream_data(): 37 | for trend in objects: 38 | trend.stream( 39 | { 40 | "x": [trend.data["x"][-1] + 1], 41 | "y": [trend.data["y"][-1] + np.random.randn()], 42 | }, 43 | rollover=50, 44 | ) 45 | 46 | pn.state.add_periodic_callback(stream_data, period=500) 47 | 48 | 49 | pn.state.onload(load) 50 | 51 | grid.servable() 52 | -------------------------------------------------------------------------------- /panel_lightning.py: -------------------------------------------------------------------------------- 1 | """The panel_lightning module provides the `LitPanelPage` that makes it easy 2 | to add a Panel page to your `LightningFlow`. 3 | """ 4 | from __future__ import annotations 5 | 6 | import pathlib 7 | from types import ModuleType 8 | from typing import Callable, Dict, Union 9 | 10 | import lightning_app as lapp 11 | import panel as pn 12 | from bokeh.application.handlers import code 13 | from bokeh.server.server import Server 14 | from lightning_app.core import constants 15 | 16 | Page = Union[Callable[[], pn.viewable.Viewable], str, pathlib.Path] 17 | 18 | 19 | class _CodeRunnerWithFastInitialResponse(code.CodeRunner): 20 | """Helper class to provide a fast initial response to the lightning.ai server 21 | 22 | C.f. https://github.com/Lightning-AI/lightning/issues/13335 23 | """ 24 | 25 | _is_patched = False 26 | _initial_response = True 27 | 28 | def run(self, module: ModuleType, post_check: Callable[[], None] | None = None) -> None: 29 | """Patches the code.CodeRunner to provide a fast initial response""" 30 | if self._initial_response: 31 | pn.pane.HTML("

Fast Initial Response. Please reload the page.

").servable() 32 | self._initial_response = False 33 | else: 34 | super().run(module, post_check) 35 | 36 | @classmethod 37 | def patch(cls): 38 | """Patches the Panel server to provide a fast initial response from pages created via 39 | .py and .ipynb files.""" 40 | if not cls._is_patched: 41 | cls._is_patched = True 42 | code.CodeRunner = _CodeRunnerWithFastInitialResponse 43 | 44 | 45 | class _PageWithFastInitialResponse: # pylint: disable=too-few-public-methods 46 | """Helper class to provide a fast initial response to the Lightning.AI server 47 | 48 | C.f. https://github.com/Lightning-AI/lightning/issues/13335 49 | """ 50 | 51 | def __init__(self, page: Callable[[], pn.viewable.Viewable]): 52 | """Wraps the page function to provide a fast initial response 53 | 54 | Args: 55 | page (Callable[[], pn.viewable.Viewable]): A function returning a Panel viewable 56 | """ 57 | self._initial_response = True 58 | self._page = page 59 | 60 | def get(self): 61 | """Runs the page function and returns the result. 62 | 63 | The first time the get function is a run a fast page function is run 64 | """ 65 | if self._initial_response: 66 | self._initial_response = False 67 | return pn.pane.HTML("

Fast Initial Response

") 68 | 69 | return self._page() 70 | 71 | 72 | class LitPanelPage(lapp.LightningWork): 73 | """Can serve a single page Panel app""" 74 | 75 | def __init__(self, page: Page, **params): 76 | """Can serve a single page Panel app 77 | 78 | Args: 79 | page (Page): 80 | A function returning a Panel `Viewable` or the path of a file containing a 81 | Panel application. 82 | """ 83 | if isinstance(page, (str, pathlib.Path)): 84 | _CodeRunnerWithFastInitialResponse.patch() 85 | page = str(page) 86 | self._page = page 87 | else: 88 | self._page = _PageWithFastInitialResponse(page=page).get 89 | 90 | self._server: Union[None, Server] = None 91 | 92 | super().__init__(**params) 93 | 94 | def run(self, *args, **kwargs): 95 | """Starts the server and serves the page""" 96 | # print("websocket_origin", self.websocket_origin) 97 | self._server = pn.serve( 98 | {"/": self._page}, 99 | port=self.port, 100 | address=self.host, 101 | websocket_origin="*", 102 | show=False, 103 | ) 104 | 105 | def stop(self): 106 | """Stops the server""" 107 | if self._server: 108 | self._server.stop() 109 | 110 | def get_tab(self, name: str) -> Dict[str, str | "LitPanelPage"]: 111 | """Returns a *tab* definition to be included in the layout of a LightingFlow""" 112 | return {"name": name, "content": self} 113 | 114 | @property 115 | def websocket_origin(self) -> str: 116 | """Returns""" 117 | print("APP_SERVER_HOST", constants.APP_SERVER_HOST) 118 | host = constants.APP_SERVER_HOST.replace("http://", "").replace("https://", "") 119 | if host != "127.0.0.1": 120 | host = "*." + host.split(".")[-1] 121 | return host + ":" + str(self.port) 122 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | lightning 2 | panel 3 | pandas 4 | holoviews 5 | hvplot --------------------------------------------------------------------------------