|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | + <head> |
| 4 | + <meta charset="utf-8"> |
| 5 | + <meta name="apple-mobile-web-app-capable" content="yes"> |
| 6 | + <meta name="apple-mobile-web-app-status-bar-style" content="default"> |
| 7 | + <meta name="theme-color" content="#0072b5"> |
| 8 | + <meta name="name" content="Pyscript/Panel DeckGL Demo"> |
| 9 | + |
| 10 | + <title>Pyscript/Panel DeckGL Demo</title> |
| 11 | + |
| 12 | + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" type="text/css" /> |
| 13 | + <link rel="stylesheet" href="https://unpkg.com/@holoviz/panel@0.13.0/dist/css/widgets.css" type="text/css" /> |
| 14 | + <link rel="stylesheet" href="https://unpkg.com/@holoviz/panel@0.13.0/dist/css/markdown.css" type="text/css" /> |
| 15 | + <link rel="stylesheet" href="https://unpkg.com/@holoviz/panel@0.13.0/dist/css/loading.css" type="text/css" /> |
| 16 | + <link rel="stylesheet" href="https://unpkg.com/@holoviz/panel@0.13.0/dist/css/dataframe.css" type="text/css" /> |
| 17 | + |
| 18 | + <script type="text/javascript" src="https://unpkg.com/h3-js@3.7.2/dist/h3-js.umd.js"></script> |
| 19 | + <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/deck.gl@8.6.7/dist.min.js"></script> |
| 20 | + <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@deck.gl/json@8.6.7/dist.min.js"></script> |
| 21 | + <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@loaders.gl/csv@3.1.7/dist/dist.min.js"></script> |
| 22 | + <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@loaders.gl/json@3.1.7/dist/dist.min.js"></script> |
| 23 | + <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@loaders.gl/3d-tiles@3.1.7/dist/dist.min.js"></script> |
| 24 | + <script type="text/javascript" src="https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js"></script> |
| 25 | + <script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-2.4.2.js"></script> |
| 26 | + <script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.4.2.min.js"></script> |
| 27 | + <script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.4.2.min.js"></script> |
| 28 | + <script type="text/javascript" src="https://unpkg.com/@holoviz/panel@0.13.0/dist/panel.js"></script> |
| 29 | + |
| 30 | + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css"> |
| 31 | + <link rel="stylesheet" href="https://unpkg.com/@holoviz/panel@0.13.0/dist/bundled/bootstraptemplate/bootstrap.css"> |
| 32 | + <link rel="stylesheet" href="https://unpkg.com/@holoviz/panel@0.13.0/dist/bundled/defaulttheme/default.css"> |
| 33 | + |
| 34 | + <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"></script> |
| 35 | + <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"></script> |
| 36 | + <style> |
| 37 | + #sidebar { |
| 38 | + width: 400px; |
| 39 | + } |
| 40 | + </style> |
| 41 | + <link rel="stylesheet" href="../build/pyscript.css" /> |
| 42 | + <script defer src="../build/pyscript.js"></script> |
| 43 | + </head> |
| 44 | + <body> |
| 45 | + <py-env> |
| 46 | + - bokeh |
| 47 | + - numpy |
| 48 | + - pandas |
| 49 | + </py-env> |
| 50 | + |
| 51 | + <div class="container-fluid d-flex flex-column vh-100 overflow-hidden" id="container"> |
| 52 | + <nav class="navbar navbar-expand-md navbar-dark sticky-top shadow" id="header" style="background-color: #0072b5;"> |
| 53 | + <button type="button" class="navbar-toggle collapsed" id="sidebarCollapse"> |
| 54 | + <span class="navbar-toggler-icon"></span> |
| 55 | + </button> |
| 56 | + <div class="app-header"> |
| 57 | + <a class="title" href="/" > Panel</a> |
| 58 | + <span class="title"> -</span> |
| 59 | + <a class="title" href="" > Pyscript DeckGL NYC Taxi</a> |
| 60 | + </div> |
| 61 | + </nav> |
| 62 | + |
| 63 | + <div class="row overflow-hidden" id="content"> |
| 64 | + <div class="sidenav" id="sidebar"> |
| 65 | + <ul class="nav flex-column"> |
| 66 | + <div class="bk-root" id="widgets" data-root-id="1021"></div> |
| 67 | + <py-repl id="my-repl" auto-generate="true"> </py-repl> |
| 68 | + </ul> |
| 69 | + </div> |
| 70 | + <div class="col mh-100 float-left" style="padding: 0;"> |
| 71 | + <div class="bk-
F438
root" id="plot" data-root-id="1008"></div> |
| 72 | + </div> |
| 73 | + </div> |
| 74 | + </div> |
| 75 | + <py-script> |
| 76 | +import asyncio |
| 77 | +import micropip |
| 78 | + |
| 79 | +from io import StringIO |
| 80 | +from js import fetch |
| 81 | + |
| 82 | +await micropip.install(['panel==0.13.1a1']) |
| 83 | + |
| 84 | +import panel as pn |
| 85 | +import param |
| 86 | +import pandas as pd |
| 87 | + |
| 88 | +from panel.io.pyodide import show |
| 89 | + |
| 90 | +MAPBOX_KEY = "pk.eyJ1IjoicGFuZWxvcmciLCJhIjoiY2s1enA3ejhyMWhmZjNobjM1NXhtbWRrMyJ9.B_frQsAVepGIe-HiOJeqvQ" |
| 91 | + |
| 92 | +class App(pn.viewable.Viewer): |
| 93 | + |
| 94 | + data = param.DataFrame(precedence=-1) |
| 95 | + |
| 96 | + view = param.DataFrame(precedence=-1) |
| 97 | + |
| 98 | + arc_view = param.DataFrame(precedence=-1) |
| 99 | + |
| 100 | + radius = param.Integer(default=50, bounds=(20, 1000)) |
| 101 | + |
| 102 | + elevation = param.Integer(default=10, bounds=(0, 50)) |
| 103 | + |
| 104 | + hour = param.Integer(default=0, bounds=(0, 23)) |
| 105 | + |
| 106 | + speed = param.Integer(default=1, bounds=(0, 10), precedence=-1) |
| 107 | + |
| 108 | + play = param.Event(label='▷') |
| 109 | + |
| 110 | + def __init__(self, **params): |
| 111 | + self.deck_gl = None |
| 112 | + super().__init__(**params) |
| 113 | + self.deck_gl = pn.pane.DeckGL( |
| 114 | + dict(self.spec), mapbox_api_key=MAPBOX_KEY, throttle={'click': 10}, |
| 115 | + sizing_mode='stretch_both', margin=0) |
| 116 | + self.deck_gl.param.watch(self._update_arc_view, 'click_state') |
| 117 | + self._playing = False |
| 118 | + self._cb = pn.state.add_periodic_callback( |
| 119 | + self._update_hour, 1000//self.speed, start=False |
| 120 | + ) |
| 121 | + |
| 122 | + def __panel__(self): |
| 123 | + return self.deck_gl |
| 124 | + |
| 125 | + @property |
| 126 | + def spec(self): |
| 127 | + return { |
| 128 | + "initialViewState": { |
| 129 | + "bearing": 0, |
| 130 | + "latitude": 40.7, |
| 131 | + "longitude": -73.9, |
| 132 | + "maxZoom": 15, |
| 133 | + "minZoom": 5, |
| 134 | + "pitch": 40.5, |
| 135 | + "zoom": 11 |
| 136 | + }, |
| 137 | + "layers": [self.hex_layer, self.arc_layer], |
| 138 | + "mapStyle": "mapbox://styles/mapbox/dark-v9", |
| 139 | + "views": [ |
| 140 | + {"@@type": "MapView", "controller": True} |
| 141 | + ] |
| 142 | + } |
| 143 | + |
| 144 | + @property |
| 145 | + def hex_layer(self): |
| 146 | + return { |
| 147 | + "@@type": "HexagonLayer", |
| 148 | + "autoHighlight": True, |
| 149 | + "coverage": 1, |
| 150 | + "data": self.data if self.view is None else self.view, |
| 151 | + "elevationRange": [0, 100], |
| 152 | + "elevationScale": self.elevation, |
| 153 | + "radius": self.radius, |
| 154 | + "extruded": True, |
| 155 | + "getPosition": "@@=[pickup_x, pickup_y]", |
| 156 | + "id": "8a553b25-ef3a-489c-bbe2-e102d18a3211" |
| 157 | + } |
| 158 | + |
| 159 | + @property |
| 160 | + def arc_layer(self): |
| 161 | + return { |
| 162 | + "@@type": "ArcLayer", |
| 163 | + "id": 'arc-layer', |
| 164 | + "data": self.arc_view, |
| 165 | + "pickable": True, |
| 166 | + "getWidth": 1, |
| 167 | + "getSourcePosition": "@@=[pickup_x, pickup_y]", |
| 168 | + "getTargetPosition": "@@=[dropoff_x, dropoff_y]", |
| 169 | + "getSourceColor": [0, 255, 0, 180], |
| 170 | + "getTargetColor": [240, 100, 0, 180] |
| 171 | + } |
| 172 | + |
| 173 | + def _update_hour(self): |
| 174 | + self.hour = (self.hour+1) % 24 |
| 175 | + |
| 176 | + @param.depends('view', watch=True) |
| 177 | + def _update_arc_view(self, event=None): |
| 178 | + data = self.data if self.view is None else self.view |
| 179 | + if not self.deck_gl or not self.deck_gl.click_state: |
| 180 | + self.arc_view = data.iloc[:0] |
| 181 | + else: |
| 182 | + lon, lat = self.deck_gl.click_state['coordinate'] |
| 183 | + tol = 0.001 |
| 184 | + self.arc_view = data[ |
| 185 | + (df.pickup_x>=float(lon-tol)) & |
| 186 | + (df.pickup_x<=float(lon+tol)) & |
| 187 | + (df.pickup_y>=float(lat-tol)) & |
| 188 | + (df.pickup_y<=float(lat+tol)) |
| 189 | + ] |
| 190 | + |
| 191 | + @param.depends('hour', watch=True) |
| 192 | + def _update_hourly_view(self): |
| 193 | + self.view = self.data[self.data.hour==self.hour] |
| 194 | + |
| 195 | + @param.depends('speed', watch=True) |
| 196 | + def _update_speed(self): |
| 197 | + self._cb.period = 1000//self.speed |
| 198 | + |
| 199 | + @param.depends('play', watch=True) |
| 200 | + def _play_pause(self): |
| 201 | + if self._playing: |
| 202 | + self._cb.stop() |
| 203 | + self.param.play.label = '▷' |
| 204 | + self.param.speed.precedence = -1 |
| 205 | + else: |
| 206 | + self._cb.start() |
| 207 | + self.param.play.label = '❚❚' |
| 208 | + self.param.speed.precedence = 1 |
| 209 | + self._playing = not self._playing |
| 210 | + |
| 211 | + @param.depends('view', 'radius', 'elevation', 'arc_view', watch=True) |
| 212 | + def update_spec(self): |
| 213 | + self.deck_gl.object = dict(self.spec) |
| 214 | + |
| 215 | + |
| 216 | +data = await fetch('https://s3.eu-west-1.amazonaws.com/assets.holoviews.org/data/nyc_taxi_wide.csv') |
| 217 | +df = pd.read_csv(StringIO(await data.text())) |
| 218 | + |
| 219 | +app = App(data=df) |
| 220 | +controls = pn.Param(app.param, sizing_mode='stretch_width', show_name=False) |
| 221 | + |
| 222 | +await show(controls, 'widgets') |
| 223 | +await show(app, 'plot') |
| 224 | + </py-script> |
| 225 | + </body> |
| 226 | +</html> |
0 commit comments