Spaces:
Build error
Build error
Initial commit
Browse files- .gitignore +2 -0
- Dockerfile +13 -0
- README.md +2 -10
- app.py +431 -0
- requirements.txt +7 -0
- styles.css +56 -0
- superzip.csv +0 -0
.gitignore
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
.venv
|
2 |
+
__pycache__
|
Dockerfile
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.9
|
2 |
+
|
3 |
+
WORKDIR /code
|
4 |
+
|
5 |
+
COPY ./requirements.txt /code/requirements.txt
|
6 |
+
|
7 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
8 |
+
|
9 |
+
COPY . .
|
10 |
+
|
11 |
+
EXPOSE 7860
|
12 |
+
|
13 |
+
CMD ["shiny", "run", "app.py", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
@@ -1,11 +1,3 @@
|
|
1 |
-
|
2 |
-
title: Superzip
|
3 |
-
emoji: 👀
|
4 |
-
colorFrom: red
|
5 |
-
colorTo: yellow
|
6 |
-
sdk: docker
|
7 |
-
pinned: false
|
8 |
-
license: cc0-1.0
|
9 |
-
---
|
10 |
|
11 |
-
|
|
|
1 |
+
## Data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
+
The `superzip.csv` is the result of [this script](https://github.com/rstudio/shinycoreci-apps/blob/main/apps/063-superzip-example/global.R)
|
app.py
ADDED
@@ -0,0 +1,431 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from typing import List, Optional, Tuple
|
3 |
+
|
4 |
+
import ipyleaflet as leaf
|
5 |
+
import ipywidgets
|
6 |
+
import matplotlib as mpl
|
7 |
+
import numpy as np
|
8 |
+
import pandas as pd
|
9 |
+
import plotly.figure_factory as ff
|
10 |
+
import plotly.graph_objs as go
|
11 |
+
from htmltools import head_content
|
12 |
+
from ipyleaflet import basemaps
|
13 |
+
from matplotlib import cm
|
14 |
+
from shiny import *
|
15 |
+
from shiny.types import SilentException
|
16 |
+
|
17 |
+
from shinywidgets import *
|
18 |
+
|
19 |
+
color_palette = cm.get_cmap("viridis", 10)
|
20 |
+
|
21 |
+
|
22 |
+
# TODO: how to handle nas (pd.isna)?
|
23 |
+
def col_numeric(domain: Tuple[float, float], na_color: str = "#808080"):
|
24 |
+
rescale = mpl.colors.Normalize(domain[0], domain[1])
|
25 |
+
|
26 |
+
def _(vals: List[float]) -> List[str]:
|
27 |
+
cols = color_palette(rescale(vals))
|
28 |
+
return [mpl.colors.to_hex(v) for v in cols]
|
29 |
+
|
30 |
+
return _
|
31 |
+
|
32 |
+
|
33 |
+
# TODO: when this issue is fixed, we won't have to sample anymore
|
34 |
+
# https://github.com/rstudio/prism/issues/119
|
35 |
+
app_dir = os.path.dirname(__file__)
|
36 |
+
allzips = pd.read_csv(os.path.join(app_dir, "superzip.csv")).sample(
|
37 |
+
n=10000, random_state=42
|
38 |
+
)
|
39 |
+
|
40 |
+
# ------------------------------------------------------------------------
|
41 |
+
# Define user interface
|
42 |
+
# ------------------------------------------------------------------------
|
43 |
+
|
44 |
+
vars = {
|
45 |
+
"Score": "Overall score",
|
46 |
+
"College": "% college educated",
|
47 |
+
"Income": "Median income",
|
48 |
+
"Population": "Population",
|
49 |
+
}
|
50 |
+
|
51 |
+
css = open(os.path.join(app_dir, "styles.css"), "r").readlines()
|
52 |
+
|
53 |
+
ui_map = ui.TagList(
|
54 |
+
output_widget("map", width="100%", height="100%"),
|
55 |
+
ui.panel_fixed(
|
56 |
+
ui.h2("SuperZIP explorer"),
|
57 |
+
ui.input_select("variable", "Heatmap variable", vars),
|
58 |
+
output_widget("density_score", height="200px"),
|
59 |
+
output_widget("density_college", height="200px"),
|
60 |
+
output_widget("density_income", height="200px"),
|
61 |
+
output_widget("density_pop", height="200px"),
|
62 |
+
id="controls",
|
63 |
+
class_="panel panel-default",
|
64 |
+
width="330px",
|
65 |
+
height="auto",
|
66 |
+
draggable=True,
|
67 |
+
top="60px",
|
68 |
+
left="auto",
|
69 |
+
right="20px",
|
70 |
+
bottom="auto",
|
71 |
+
),
|
72 |
+
ui.div(
|
73 |
+
"Data compiled for ",
|
74 |
+
ui.tags.em("Coming Apart: The State of White America, 1960-2010"),
|
75 |
+
" by Charles Murray (Crown Forum, 2012).",
|
76 |
+
id="cite",
|
77 |
+
),
|
78 |
+
)
|
79 |
+
|
80 |
+
app_ui = ui.page_navbar(
|
81 |
+
ui.nav(
|
82 |
+
"Interactive map",
|
83 |
+
ui.div(head_content(ui.tags.style(css)), ui_map, class_="outer"),
|
84 |
+
),
|
85 |
+
ui.nav(
|
86 |
+
"Data explorer",
|
87 |
+
ui.row(
|
88 |
+
ui.column(3, ui.output_ui("data_intro")),
|
89 |
+
ui.column(9, output_widget("data", height="100%")),
|
90 |
+
),
|
91 |
+
ui.row(
|
92 |
+
ui.column(2),
|
93 |
+
ui.column(8, output_widget("table_map")),
|
94 |
+
ui.column(2),
|
95 |
+
),
|
96 |
+
),
|
97 |
+
title="Superzip",
|
98 |
+
)
|
99 |
+
|
100 |
+
# ------------------------------------------------------------------------
|
101 |
+
# non-reactive helper functions
|
102 |
+
# ------------------------------------------------------------------------
|
103 |
+
|
104 |
+
|
105 |
+
def density_plot(
|
106 |
+
overall: pd.DataFrame,
|
107 |
+
in_bounds: pd.DataFrame,
|
108 |
+
var: str,
|
109 |
+
selected: Optional[pd.DataFrame] = None,
|
110 |
+
title: Optional[str] = None,
|
111 |
+
showlegend: bool = False,
|
112 |
+
):
|
113 |
+
dat = [overall[var], in_bounds[var]]
|
114 |
+
if var == "Population":
|
115 |
+
dat = [np.log10(x) for x in dat]
|
116 |
+
|
117 |
+
# Create distplot with curve_type set to 'normal'
|
118 |
+
fig = ff.create_distplot(
|
119 |
+
dat,
|
120 |
+
["Overall", "In bounds"],
|
121 |
+
colors=["black", "#6DCD59"],
|
122 |
+
show_rug=False,
|
123 |
+
show_hist=False,
|
124 |
+
)
|
125 |
+
# Remove tick labels
|
126 |
+
fig.update_layout(
|
127 |
+
# hovermode="x",
|
128 |
+
height=200,
|
129 |
+
showlegend=showlegend,
|
130 |
+
margin=dict(l=0, r=0, t=0, b=0),
|
131 |
+
legend=dict(x=0.5, y=1, orientation="h", xanchor="center", yanchor="bottom"),
|
132 |
+
xaxis=dict(
|
133 |
+
title=title if title is not None else var,
|
134 |
+
showgrid=False,
|
135 |
+
showline=False,
|
136 |
+
zeroline=False,
|
137 |
+
),
|
138 |
+
yaxis=dict(
|
139 |
+
showgrid=False,
|
140 |
+
showline=False,
|
141 |
+
showticklabels=False,
|
142 |
+
zeroline=False,
|
143 |
+
),
|
144 |
+
)
|
145 |
+
# hovermode itsn't working properly when dynamically, absolutely positioned
|
146 |
+
for _, trace in enumerate(fig.data):
|
147 |
+
trace.update(hoverinfo="none")
|
148 |
+
|
149 |
+
if selected is not None:
|
150 |
+
x = selected[var].tolist()[0]
|
151 |
+
if var == "Population":
|
152 |
+
x = np.log10(x)
|
153 |
+
fig.add_shape(
|
154 |
+
type="line",
|
155 |
+
x0=x,
|
156 |
+
x1=x,
|
157 |
+
y0=0,
|
158 |
+
y1=1,
|
159 |
+
yref="paper",
|
160 |
+
line=dict(width=1, dash="dashdot", color="gray"),
|
161 |
+
)
|
162 |
+
|
163 |
+
return go.FigureWidget(data=fig.data, layout=fig.layout)
|
164 |
+
|
165 |
+
|
166 |
+
def create_map(**kwargs):
|
167 |
+
map = leaf.Map(
|
168 |
+
center=(37.45, -88.85),
|
169 |
+
zoom=4,
|
170 |
+
scroll_wheel_zoom=True,
|
171 |
+
attribution_control=False,
|
172 |
+
**kwargs,
|
173 |
+
)
|
174 |
+
map.add_layer(leaf.basemap_to_tiles(basemaps.CartoDB.DarkMatter))
|
175 |
+
return map
|
176 |
+
|
177 |
+
|
178 |
+
# ------------------------------------------------------------------------
|
179 |
+
# Server logic
|
180 |
+
# ------------------------------------------------------------------------
|
181 |
+
|
182 |
+
|
183 |
+
def server(input: Inputs, output: Outputs, session: Session):
|
184 |
+
# ------------------------------------------------------------------------
|
185 |
+
# Main map logic
|
186 |
+
# ------------------------------------------------------------------------
|
187 |
+
map = create_map(layout=ipywidgets.Layout(width="100%", height="100%"))
|
188 |
+
register_widget("map", map)
|
189 |
+
|
190 |
+
# Keeps track of whether we're showing markers (zoomed in) or heatmap (zoomed out)
|
191 |
+
show_markers = reactive.Value(False)
|
192 |
+
|
193 |
+
@reactive.Effect
|
194 |
+
def _():
|
195 |
+
nzips = zips_in_bounds().shape[0]
|
196 |
+
show_markers.set(nzips < 200)
|
197 |
+
|
198 |
+
# When the variable changes, either update marker colors or redraw the heatmap
|
199 |
+
@reactive.Effect
|
200 |
+
@reactive.event(input.variable)
|
201 |
+
def _():
|
202 |
+
zips = zips_in_bounds()
|
203 |
+
if not show_markers():
|
204 |
+
remove_heatmap()
|
205 |
+
map.add_layer(layer_heatmap())
|
206 |
+
else:
|
207 |
+
zip_colors = dict(zip(zips.Zipcode, zips_marker_color()))
|
208 |
+
for x in map.layers:
|
209 |
+
if x.name.startswith("marker-"):
|
210 |
+
zipcode = int(x.name.split("-")[1])
|
211 |
+
if zipcode in zip_colors:
|
212 |
+
x.color = zip_colors[zipcode]
|
213 |
+
|
214 |
+
# When bounds change, maybe add new markers
|
215 |
+
@reactive.Effect
|
216 |
+
@reactive.event(lambda: zips_in_bounds())
|
217 |
+
def _():
|
218 |
+
if not show_markers():
|
219 |
+
return
|
220 |
+
zips = zips_in_bounds()
|
221 |
+
if zips.empty:
|
222 |
+
return
|
223 |
+
|
224 |
+
# Be careful not to create markers until we know we need to add it
|
225 |
+
current_markers = set(
|
226 |
+
[m.name for m in map.layers if m.name.startswith("marker-")]
|
227 |
+
)
|
228 |
+
zips["Color"] = zips_marker_color()
|
229 |
+
for _, row in zips.iterrows():
|
230 |
+
if ("marker-" + str(row.Zipcode)) not in current_markers:
|
231 |
+
map.add_layer(create_marker(row, color=row.Color))
|
232 |
+
|
233 |
+
# Change from heatmap to markers: remove the heatmap and show markers
|
234 |
+
# Change from markers to heatmap: hide the markers and add the heatmap
|
235 |
+
@reactive.Effect
|
236 |
+
@reactive.event(show_markers)
|
237 |
+
def _():
|
238 |
+
if show_markers():
|
239 |
+
map.remove_layer(layer_heatmap())
|
240 |
+
else:
|
241 |
+
map.add_layer(layer_heatmap())
|
242 |
+
|
243 |
+
opacity = 0.6 if show_markers() else 0.0
|
244 |
+
|
245 |
+
for x in map.layers:
|
246 |
+
if x.name.startswith("marker-"):
|
247 |
+
x.fill_opacity = opacity
|
248 |
+
x.opacity = opacity
|
249 |
+
|
250 |
+
@reactive.Calc
|
251 |
+
def zips_in_bounds():
|
252 |
+
bb = reactive_read(map, "bounds")
|
253 |
+
if not bb:
|
254 |
+
# TODO: this should really be `raise SilentException`...why doesn't it work?
|
255 |
+
# return pd.DataFrame()
|
256 |
+
raise SilentException
|
257 |
+
|
258 |
+
lats = (bb[0][0], bb[1][0])
|
259 |
+
lons = (bb[0][1], bb[1][1])
|
260 |
+
return allzips[
|
261 |
+
(allzips.Lat >= lats[0])
|
262 |
+
& (allzips.Lat <= lats[1])
|
263 |
+
& (allzips.Long >= lons[0])
|
264 |
+
& (allzips.Long <= lons[1])
|
265 |
+
]
|
266 |
+
|
267 |
+
@reactive.Calc
|
268 |
+
def zips_marker_color():
|
269 |
+
vals = allzips[input.variable()]
|
270 |
+
domain = (vals.min(), vals.max())
|
271 |
+
vals_in_bb = zips_in_bounds()[input.variable()]
|
272 |
+
return col_numeric(domain)(vals_in_bb)
|
273 |
+
|
274 |
+
@reactive.Calc
|
275 |
+
def layer_heatmap():
|
276 |
+
locs = allzips[["Lat", "Long", input.variable()]].to_numpy()
|
277 |
+
return leaf.Heatmap(
|
278 |
+
locations=locs.tolist(),
|
279 |
+
name="heatmap",
|
280 |
+
# R> cat(paste0(round(scales::rescale(log10(1:10), to = c(0.05, 1)), 2), ": '", viridis::viridis(10), "'"), sep = "\n")
|
281 |
+
gradient={
|
282 |
+
0.05: "#440154",
|
283 |
+
0.34: "#482878",
|
284 |
+
0.5: "#3E4A89",
|
285 |
+
0.62: "#31688E",
|
286 |
+
0.71: "#26828E",
|
287 |
+
0.79: "#1F9E89",
|
288 |
+
0.85: "#35B779",
|
289 |
+
0.91: "#6DCD59",
|
290 |
+
0.96: "#B4DE2C",
|
291 |
+
1: "#FDE725",
|
292 |
+
},
|
293 |
+
)
|
294 |
+
|
295 |
+
def remove_heatmap():
|
296 |
+
for x in map.layers:
|
297 |
+
if x.name == "heatmap":
|
298 |
+
map.remove_layer(x)
|
299 |
+
|
300 |
+
zip_selected = reactive.Value(None)
|
301 |
+
|
302 |
+
@output(id="density_score")
|
303 |
+
@render_widget
|
304 |
+
def _():
|
305 |
+
return density_plot(
|
306 |
+
allzips,
|
307 |
+
zips_in_bounds(),
|
308 |
+
selected=zip_selected(),
|
309 |
+
var="Score",
|
310 |
+
title="Overall Score",
|
311 |
+
showlegend=True,
|
312 |
+
)
|
313 |
+
|
314 |
+
@output(id="density_income")
|
315 |
+
@render_widget
|
316 |
+
def _():
|
317 |
+
return density_plot(
|
318 |
+
allzips, zips_in_bounds(), selected=zip_selected(), var="Income"
|
319 |
+
)
|
320 |
+
|
321 |
+
@output(id="density_college")
|
322 |
+
@render_widget
|
323 |
+
def _():
|
324 |
+
return density_plot(
|
325 |
+
allzips, zips_in_bounds(), selected=zip_selected(), var="College"
|
326 |
+
)
|
327 |
+
|
328 |
+
@output(id="density_pop")
|
329 |
+
@render_widget
|
330 |
+
def _():
|
331 |
+
return density_plot(
|
332 |
+
allzips,
|
333 |
+
zips_in_bounds(),
|
334 |
+
selected=zip_selected(),
|
335 |
+
var="Population",
|
336 |
+
title="log10(Population)",
|
337 |
+
)
|
338 |
+
|
339 |
+
def create_marker(row, **kwargs):
|
340 |
+
m = leaf.CircleMarker(
|
341 |
+
location=(row.Lat, row.Long),
|
342 |
+
popup=ipywidgets.HTML(
|
343 |
+
f"""
|
344 |
+
{row.City}, {row.State} ({row.Zipcode})<br/>
|
345 |
+
{row.Score:.1f} overall score<br/>
|
346 |
+
{row.College:.1f}% college educated<br/>
|
347 |
+
${row.Income:.0f}k median income<br/>
|
348 |
+
{row.Population} people<br/>
|
349 |
+
"""
|
350 |
+
),
|
351 |
+
name=f"marker-{row.Zipcode}",
|
352 |
+
**kwargs,
|
353 |
+
)
|
354 |
+
|
355 |
+
def _on_click(**kwargs):
|
356 |
+
coords = kwargs["coordinates"]
|
357 |
+
idx = (allzips.Lat == coords[0]) & (allzips.Long == coords[1])
|
358 |
+
zip_selected.set(allzips[idx])
|
359 |
+
|
360 |
+
m.on_click(_on_click)
|
361 |
+
|
362 |
+
return m
|
363 |
+
|
364 |
+
@output(id="data_intro")
|
365 |
+
@render.ui
|
366 |
+
def _():
|
367 |
+
zips = zips_in_bounds()
|
368 |
+
|
369 |
+
md = ui.markdown(
|
370 |
+
f"""
|
371 |
+
{zips.shape[0]} zip codes are currently within the map's viewport, and amongst them:
|
372 |
+
|
373 |
+
* {100*zips.Superzip.mean():.1f}% are superzips
|
374 |
+
* Mean income is ${zips.Income.mean():.0f}k 💰
|
375 |
+
* Mean population is {zips.Population.mean():.0f} 👨🏽👩🏽👦🏽
|
376 |
+
* Mean college educated is {zips.College.mean():.1f}% 🎓
|
377 |
+
|
378 |
+
Use the filter controls on the table's columns to drill down further or
|
379 |
+
click on a row to
|
380 |
+
""",
|
381 |
+
)
|
382 |
+
|
383 |
+
return ui.div(md, class_="my-3 lead")
|
384 |
+
|
385 |
+
selected_table_row = reactive.Value(pd.DataFrame())
|
386 |
+
|
387 |
+
@output(id="data")
|
388 |
+
@render_widget
|
389 |
+
def _():
|
390 |
+
import qgrid
|
391 |
+
|
392 |
+
dat = zips_in_bounds().drop(["Lat", "Long", "Color"], axis=1, errors="ignore")
|
393 |
+
|
394 |
+
w = qgrid.show_grid(
|
395 |
+
dat,
|
396 |
+
grid_options={"editable": False},
|
397 |
+
column_definitions={"index": {"maxWidth": 0, "minWidth": 0, "width": 0}},
|
398 |
+
)
|
399 |
+
|
400 |
+
def _on_change(event, widget):
|
401 |
+
idx = event["new"][0]
|
402 |
+
selected_table_row.set(zips_in_bounds().iloc[[idx]])
|
403 |
+
|
404 |
+
w.on("selection_changed", _on_change)
|
405 |
+
|
406 |
+
return w
|
407 |
+
|
408 |
+
table_map = create_map()
|
409 |
+
|
410 |
+
@output(id="table_map")
|
411 |
+
@render_widget
|
412 |
+
def _():
|
413 |
+
if selected_table_row().empty:
|
414 |
+
return None
|
415 |
+
else:
|
416 |
+
return table_map
|
417 |
+
|
418 |
+
# TODO: currently there is a bug where clicking the popup causes an error,
|
419 |
+
# but I _think_ this'll get fixed in the next release of ipywidgets/ipyleaflet
|
420 |
+
# https://github.com/jupyter-widgets/ipywidgets/issues/3384
|
421 |
+
@reactive.Effect
|
422 |
+
@reactive.event(selected_table_row)
|
423 |
+
def _():
|
424 |
+
for x in table_map.layers:
|
425 |
+
if x.name.startswith("marker"):
|
426 |
+
table_map.remove_layer(x)
|
427 |
+
for _, row in selected_table_row().iterrows():
|
428 |
+
table_map.add_layer(create_marker(row))
|
429 |
+
|
430 |
+
|
431 |
+
app = App(app_ui, server)
|
requirements.txt
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
shiny
|
2 |
+
shinywidgets
|
3 |
+
matplotlib
|
4 |
+
scipy
|
5 |
+
qgrid@git+https://github.com/cpsievert/qgrid
|
6 |
+
ipyleaflet
|
7 |
+
plotly
|
styles.css
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
input[type="number"] {
|
2 |
+
max-width: 80%;
|
3 |
+
}
|
4 |
+
|
5 |
+
div.outer {
|
6 |
+
position: fixed;
|
7 |
+
top: 55px;
|
8 |
+
left: 0;
|
9 |
+
right: 0;
|
10 |
+
bottom: 0;
|
11 |
+
overflow: hidden;
|
12 |
+
padding: 0;
|
13 |
+
}
|
14 |
+
|
15 |
+
/* Customize fonts */
|
16 |
+
body, label, input, button, select {
|
17 |
+
font-family: 'Helvetica Neue', Helvetica;
|
18 |
+
font-weight: 200;
|
19 |
+
}
|
20 |
+
h1, h2, h3, h4 { font-weight: 400; }
|
21 |
+
|
22 |
+
#controls {
|
23 |
+
/* Appearance */
|
24 |
+
background-color: white;
|
25 |
+
padding: 0 20px 20px 20px;
|
26 |
+
cursor: move;
|
27 |
+
/* Fade out while not hovering */
|
28 |
+
opacity: 0.65;
|
29 |
+
zoom: 0.9;
|
30 |
+
transition: opacity 500ms 1s;
|
31 |
+
z-index: 700;
|
32 |
+
}
|
33 |
+
#controls:hover {
|
34 |
+
/* Fade in while hovering */
|
35 |
+
opacity: 0.95;
|
36 |
+
transition-delay: 0;
|
37 |
+
}
|
38 |
+
|
39 |
+
/* Position and style citation */
|
40 |
+
#cite {
|
41 |
+
position: absolute;
|
42 |
+
bottom: 10px;
|
43 |
+
left: 10px;
|
44 |
+
font-size: 12px;
|
45 |
+
z-index: 700;
|
46 |
+
}
|
47 |
+
|
48 |
+
/* If not using map tiles, show a white background */
|
49 |
+
.leaflet-container {
|
50 |
+
background-color: white !important;
|
51 |
+
}
|
52 |
+
|
53 |
+
/* FigureWidget doesn't support config?!? */
|
54 |
+
.plotly .modebar-container {
|
55 |
+
display: none;
|
56 |
+
}
|
superzip.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|