MarcSkovMadsen commited on
Commit
8c38616
1 Parent(s): 8c22469

Upload 5 files

Browse files
Files changed (5) hide show
  1. README.md +17 -5
  2. index.html +0 -0
  3. index.js +646 -0
  4. index.py +542 -0
  5. requirements.txt +5 -0
README.md CHANGED
@@ -1,11 +1,23 @@
1
  ---
2
- title: Commuting Flows Italy
3
- emoji: 📊
4
- colorFrom: red
5
- colorTo: pink
6
  sdk: static
7
  pinned: false
8
  license: mit
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Commuting flows between Italian Regions
3
+ emoji: 🚌
4
+ colorFrom: purple
5
+ colorTo: green
6
  sdk: static
7
  pinned: false
8
  license: mit
9
  ---
10
 
11
+ See [commuting_flows_italy](https://awesome-panel.org/resources/commuting_flows_italy/) by [awesome-panel.org](https://awesome-panel.org) for more info.
12
+
13
+ ## Serve
14
+
15
+ ```python
16
+ panel serve *.py --autoreload
17
+ ```
18
+
19
+ ## Build
20
+
21
+ ```bash
22
+ panel convert *.py --to pyodide-worker
23
+ ```
index.html CHANGED
The diff for this file is too large to render. See raw diff
 
index.js ADDED
@@ -0,0 +1,646 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ importScripts("https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js");
2
+
3
+ function sendPatch(patch, buffers, msg_id) {
4
+ self.postMessage({
5
+ type: 'patch',
6
+ patch: patch,
7
+ buffers: buffers
8
+ })
9
+ }
10
+
11
+ async function startApplication() {
12
+ console.log("Loading pyodide!");
13
+ self.postMessage({type: 'status', msg: 'Loading pyodide'})
14
+ self.pyodide = await loadPyodide();
15
+ self.pyodide.globals.set("sendPatch", sendPatch);
16
+ console.log("Loaded!");
17
+ await self.pyodide.loadPackage("micropip");
18
+ const env_spec = ['https://cdn.holoviz.org/panel/wheels/bokeh-3.3.2-py3-none-any.whl', 'https://cdn.holoviz.org/panel/1.3.6/dist/wheels/panel-1.3.6-py3-none-any.whl', 'pyodide-http==0.2.1', 'holoviews', 'numpy', 'pandas', 'shapely']
19
+ for (const pkg of env_spec) {
20
+ let pkg_name;
21
+ if (pkg.endsWith('.whl')) {
22
+ pkg_name = pkg.split('/').slice(-1)[0].split('-')[0]
23
+ } else {
24
+ pkg_name = pkg
25
+ }
26
+ self.postMessage({type: 'status', msg: `Installing ${pkg_name}`})
27
+ try {
28
+ await self.pyodide.runPythonAsync(`
29
+ import micropip
30
+ await micropip.install('${pkg}');
31
+ `);
32
+ } catch(e) {
33
+ console.log(e)
34
+ self.postMessage({
35
+ type: 'status',
36
+ msg: `Error while installing ${pkg_name}`
37
+ });
38
+ }
39
+ }
40
+ console.log("Packages loaded!");
41
+ self.postMessage({type: 'status', msg: 'Executing code'})
42
+ const code = `
43
+
44
+ import asyncio
45
+
46
+ from panel.io.pyodide import init_doc, write_doc
47
+
48
+ init_doc()
49
+
50
+ """
51
+ Source: https://awesome-panel.org/resources/commuting_flows_italian_regions/
52
+ """
53
+ import holoviews as hv
54
+ import numpy as np
55
+ import pandas as pd
56
+ import panel as pn
57
+ from bokeh.models import HoverTool
58
+ from shapely.geometry import LineString
59
+
60
+ # Load the bokeh extension
61
+ hv.extension("bokeh")
62
+
63
+ # Set the sizing mode
64
+ pn.extension(sizing_mode="stretch_width")
65
+
66
+ # Dashboard title
67
+ DASH_TITLE = "Commuting flows between Italian Regions"
68
+
69
+ # Default colors for the dashboard
70
+ ACCENT = "#2f4f4f"
71
+ INCOMING_COLOR = "rgba(0, 108, 151, 0.75)"
72
+ OUTGOING_COLOR = "rgba(199, 81, 51, 0.75)"
73
+ INTERNAL_COLOR = "rgba(47, 79, 79, 0.55)"
74
+
75
+ # Default colors for indicators
76
+ DEFAULT_COLOR = "white"
77
+ TITLE_SIZE = "18pt"
78
+ FONT_SIZE = "20pt"
79
+
80
+ # Min/Max node size
81
+ MIN_PT_SIZE = 7
82
+ MAX_PT_SIZE = 10
83
+
84
+ # Min/Max curve width
85
+ MIN_LW = 1
86
+ MAX_LW = 10
87
+
88
+ # Dataframes dtypes
89
+ ITA_REGIONS_DTYPES = {
90
+ "cod_reg": "uint8",
91
+ "den_reg": "object",
92
+ "x": "object",
93
+ "y": "object",
94
+ }
95
+
96
+ NODES_DTYPES = {
97
+ "cod_reg": "uint8",
98
+ "x": "float64",
99
+ "y": "float64",
100
+ }
101
+
102
+ EDGES_DTYPES = {
103
+ "motivo": "object",
104
+ "interno": "bool",
105
+ "flussi": "uint32",
106
+ "reg_o": "uint8",
107
+ "reg_d": "uint8",
108
+ "x_o": "float64",
109
+ "y_o": "float64",
110
+ "x_d": "float64",
111
+ "y_d": "float64",
112
+ }
113
+
114
+ # Dictionary that maps region code to its name
115
+ ITA_REGIONS = {
116
+ 1: "Piemonte",
117
+ 2: "Valle d'Aosta/Vallée d'Aoste",
118
+ 3: "Lombardia",
119
+ 4: "Trentino-Alto Adige/Südtirol",
120
+ 5: "Veneto",
121
+ 6: "Friuli-Venezia Giulia",
122
+ 7: "Liguria",
123
+ 8: "Emilia-Romagna",
124
+ 9: "Toscana",
125
+ 10: "Umbria",
126
+ 11: "Marche",
127
+ 12: "Lazio",
128
+ 13: "Abruzzo",
129
+ 14: "Molise",
130
+ 15: "Campania",
131
+ 16: "Puglia",
132
+ 17: "Basilicata",
133
+ 18: "Calabria",
134
+ 19: "Sicilia",
135
+ 20: "Sardegna",
136
+ }
137
+
138
+ # Dictionary of options (Label/option) for commuting purpose
139
+ COMMUTING_PURPOSE = {
140
+ "Work": "Lavoro",
141
+ "Study": "Studio",
142
+ "Total": "Totale",
143
+ }
144
+
145
+ # Dashboard description
146
+ DASH_DESCR = f"""
147
+ <div>
148
+ <hr />
149
+ <p>A Panel dashboard showing <b style="color:{INCOMING_COLOR};">incoming</b>
150
+ and <b style="color:{OUTGOING_COLOR};">outgoing</b> commuting flows
151
+ for work and study between Italian Regions.</p>
152
+ <p>The width of the curves reflects the magnitude of the flows.</p>
153
+ <p>
154
+ <a href="https://www.istat.it/it/archivio/139381" target="_blank">Commuting data</a> from the
155
+ 15th Population and Housing Census (Istat, 2011).
156
+ </p>
157
+ <p>
158
+ <a href="https://www.istat.it/it/archivio/222527" target="_blank">Administrative boundaries</a> from
159
+ ISTAT.
160
+ </p>
161
+ <hr />
162
+ </div>
163
+ """
164
+
165
+ CSS_FIX = """
166
+ :host(.outline) .bk-btn.bk-btn-primary.bk-active, :host(.outline) .bk-btn.bk-btn-primary:active {
167
+ color: var(--foreground-on-accent-rest) !important;
168
+ }
169
+ """
170
+
171
+ if not CSS_FIX in pn.config.raw_css:
172
+ pn.config.raw_css.append(CSS_FIX)
173
+
174
+
175
+ def get_incoming_numind(edges, region_code, comm_purpose):
176
+ """
177
+ Returns the total incoming commuters to the selected Region.
178
+ """
179
+
180
+ # Get the value of incoming commuters
181
+ if comm_purpose == "Totale":
182
+ query = f"reg_d == {region_code} & interno == 0"
183
+ else:
184
+ query = f"(reg_d == {region_code} & motivo == '{comm_purpose}' & interno == 0)"
185
+
186
+ flows = edges.query(query)["flussi"].sum()
187
+
188
+ return pn.indicators.Number(
189
+ name="Incoming",
190
+ value=flows,
191
+ default_color=DEFAULT_COLOR,
192
+ styles={"background": INCOMING_COLOR, "padding": "5px 10px 5px 10px", "border-radius": "5px"},
193
+ title_size=TITLE_SIZE,
194
+ font_size=FONT_SIZE,
195
+ sizing_mode="stretch_width",
196
+ align="center",
197
+ css_classes=["center_number"],
198
+ )
199
+
200
+
201
+ def get_outgoing_numind(edges, region_code, comm_purpose):
202
+ """
203
+ Returns the outgoing commuters from
204
+ the selected Region.
205
+ """
206
+
207
+ # Get the value of outgoing commuters
208
+ if comm_purpose == "Totale":
209
+ query = f"reg_o == {region_code} & interno == 0"
210
+ else:
211
+ query = f"(reg_o == {region_code} & motivo == '{comm_purpose}' & interno == 0)"
212
+
213
+ flows = edges.query(query)["flussi"].sum()
214
+
215
+ return pn.indicators.Number(
216
+ name="Outgoing",
217
+ value=flows,
218
+ default_color=DEFAULT_COLOR,
219
+ styles={"background": OUTGOING_COLOR, "padding": "5px 10px 5px 10px", "border-radius": "5px"},
220
+ title_size=TITLE_SIZE,
221
+ font_size=FONT_SIZE,
222
+ sizing_mode="stretch_width",
223
+ align="center",
224
+ css_classes=["center_number"],
225
+ )
226
+
227
+
228
+ def get_internal_numind(edges, region_code, comm_purpose):
229
+ """
230
+ Returns the number of internal commuters of
231
+ the selected Region.
232
+ """
233
+
234
+ # Get the value of internal commuters
235
+ if comm_purpose == "Totale":
236
+ query = f"reg_o == {region_code} & interno == 1"
237
+ else:
238
+ query = f"(reg_o == {region_code} & motivo == '{comm_purpose}' & interno == 1)"
239
+
240
+ flows = edges.query(query)["flussi"].sum()
241
+
242
+ return pn.indicators.Number(
243
+ name="Internal mobility",
244
+ value=flows,
245
+ default_color=DEFAULT_COLOR,
246
+ styles={"background": INTERNAL_COLOR, "padding": "5px 10px 5px 10px", "border-radius": "5px"},
247
+ title_size=TITLE_SIZE,
248
+ font_size=FONT_SIZE,
249
+ sizing_mode="stretch_width",
250
+ align="center",
251
+ css_classes=["center_number"],
252
+ )
253
+
254
+
255
+ def filter_edges(edges, region_code, comm_purpose):
256
+ """
257
+ This function filters the rows of the edges for
258
+ the selected Region and commuting purpose.
259
+ """
260
+
261
+ if comm_purpose == "Totale":
262
+ query = f"(reg_o == {region_code} & interno == 0) |"
263
+ query += f" (reg_d == {region_code} & interno == 0)"
264
+ else:
265
+ query = f"(reg_o == {region_code} & motivo == '{comm_purpose}' & interno == 0) |"
266
+ query += f" (reg_d == {region_code} & motivo == '{comm_purpose}' & interno == 0)"
267
+ return edges.query(query)
268
+
269
+
270
+ def get_nodes(nodes, edges, region_code, comm_purpose):
271
+ """
272
+ Get the graph's nodes for the selected Region and commuting purpose
273
+ """
274
+
275
+ # Filter the edges by Region and commuting purpose
276
+ filt_edges = filter_edges(edges, region_code, comm_purpose)
277
+
278
+ # Find the unique values of region codes
279
+ region_codes = np.unique(filt_edges[["reg_o", "reg_d"]].values)
280
+
281
+ # Filter the nodes
282
+ nodes = nodes[nodes["cod_reg"].isin(region_codes)]
283
+
284
+ # Reoder the columns for hv.Graph
285
+ nodes = nodes[["x", "y", "cod_reg"]]
286
+
287
+ # Assign the node size
288
+ nodes["size"] = np.where(
289
+ nodes["cod_reg"] == region_code, MAX_PT_SIZE, MIN_PT_SIZE
290
+ )
291
+
292
+ # Assigns a marker to the nodes
293
+ nodes["marker"] = np.where(
294
+ nodes["cod_reg"] == region_code, "square", "circle"
295
+ )
296
+
297
+ return nodes
298
+
299
+
300
+ def get_bezier_curve(x_o, y_o, x_d, y_d, steps=25):
301
+ """
302
+ Draw a Bézier curve defined by a start point, endpoint and a control points
303
+ Source: https://stackoverflow.com/questions/69804595/trying-to-make-a-bezier-curve-on-pygame-library
304
+ """
305
+
306
+ # Generate the O/D linestring
307
+ od_line = LineString([(x_o, y_o), (x_d, y_d)])
308
+
309
+ # Calculate the offset distance of the control point
310
+ offset_distance = od_line.length / 2
311
+
312
+ # Create a line parallel to the original at the offset distance
313
+ offset_pline = od_line.parallel_offset(offset_distance, "left")
314
+
315
+ # Get the XY coodinates of the control point
316
+ ctrl_x = offset_pline.centroid.x
317
+ ctrl_y = offset_pline.centroid.y
318
+
319
+ # Calculate the XY coordinates of the Bézier curve
320
+ t = np.array([i * 1 / steps for i in range(0, steps + 1)])
321
+ x_coords = x_o * (1 - t) ** 2 + 2 * (1 - t) * t * ctrl_x + x_d * t**2
322
+ y_coords = y_o * (1 - t) ** 2 + 2 * (1 - t) * t * ctrl_y + y_d * t**2
323
+
324
+ return (x_coords, y_coords)
325
+
326
+
327
+ def get_edge_width(flow, min_flow, max_flow):
328
+ """
329
+ This function calculates the width of the curves
330
+ according to the magnitude of the flow.
331
+ """
332
+
333
+ return MIN_LW + np.power(flow - min_flow, 0.57) * (
334
+ MAX_LW - MIN_LW
335
+ ) / np.power(max_flow - min_flow, 0.57)
336
+
337
+
338
+ def get_edges(nodes, edges, region_code, comm_purpose):
339
+ """
340
+ Get the graph's edges for the selected Region and commuting purpose
341
+ """
342
+
343
+ # Filter the edges by Region and commuting purpose
344
+ filt_edges = filter_edges(edges, region_code, comm_purpose).copy()
345
+
346
+ # Aggregate the flows by Region of origin and destination
347
+ if comm_purpose == "Totale":
348
+ filt_edges = (
349
+ filt_edges.groupby(["reg_o", "reg_d"])
350
+ .agg(
351
+ motivo=("motivo", "first"),
352
+ interno=("interno", "first"),
353
+ flussi=("flussi", "sum"),
354
+ )
355
+ .reset_index()
356
+ )
357
+
358
+ # Assign Region names
359
+ filt_edges.loc[:,"den_reg_o"] = filt_edges["reg_o"].map(ITA_REGIONS)
360
+ filt_edges.loc[:,"den_reg_d"] = filt_edges["reg_d"].map(ITA_REGIONS)
361
+
362
+ # Add xy coordinates of origin
363
+ filt_edges = filt_edges.merge(
364
+ nodes.add_suffix("_o"), left_on="reg_o", right_on="cod_reg_o"
365
+ )
366
+
367
+ # Add xy coordinates of destination
368
+ filt_edges = filt_edges.merge(
369
+ nodes.add_suffix("_d"), left_on="reg_d", right_on="cod_reg_d"
370
+ )
371
+
372
+ # Get the Bézier curve
373
+ filt_edges["curve"] = filt_edges.apply(
374
+ lambda row: get_bezier_curve(
375
+ row["x_o"], row["y_o"], row["x_d"], row["y_d"]
376
+ ),
377
+ axis=1,
378
+ )
379
+
380
+ # Get the minimum/maximum flow
381
+ min_flow = filt_edges["flussi"].min()
382
+ max_flow = filt_edges["flussi"].max()
383
+
384
+ # Calculate the curve width
385
+ filt_edges["width"] = filt_edges.apply(
386
+ lambda row: get_edge_width(
387
+ row["flussi"],
388
+ min_flow,
389
+ max_flow,
390
+ ),
391
+ axis=1,
392
+ )
393
+
394
+ # Assigns the color to the incoming/outgoing edges
395
+ filt_edges["color"] = np.where(
396
+ filt_edges["reg_d"] == region_code, INCOMING_COLOR, OUTGOING_COLOR
397
+ )
398
+
399
+ filt_edges = filt_edges.sort_values(by="flussi")
400
+
401
+ return filt_edges
402
+
403
+
404
+ def get_flow_map(nodes, edges, region_admin_bounds, region_code, comm_purpose):
405
+ """
406
+ Returns a Graph showing incoming and outgoing commuting flows
407
+ for the selected Region and commuting purpose.
408
+ """
409
+
410
+ def hook(plot, element):
411
+ """
412
+ Custom hook for disabling x/y tick lines/labels
413
+ """
414
+ plot.state.xaxis.major_tick_line_color = None
415
+ plot.state.xaxis.minor_tick_line_color = None
416
+ plot.state.xaxis.major_label_text_font_size = "0pt"
417
+ plot.state.yaxis.major_tick_line_color = None
418
+ plot.state.yaxis.minor_tick_line_color = None
419
+ plot.state.yaxis.major_label_text_font_size = "0pt"
420
+
421
+ # Define a custom Hover tool
422
+ flow_map_hover = HoverTool(
423
+ tooltips=[
424
+ ("Origin", "@den_reg_o"),
425
+ ("Destination", "@den_reg_d"),
426
+ ("Commuters", "@flussi"),
427
+ ]
428
+ )
429
+
430
+ # Get the Nodes of the selected Region and commuting purpose
431
+ region_graph_nodes = get_nodes(nodes, edges, region_code, comm_purpose)
432
+
433
+ # Get the Edges of the selected Region and commuting purpose
434
+ region_graph_edges = get_edges(nodes, edges, region_code, comm_purpose)
435
+
436
+ # Get the list of Bézier curves
437
+ curves = region_graph_edges["curve"].to_list()
438
+
439
+ # Get the administrative boundary of the selected Region
440
+ region_admin_bound = region_admin_bounds[
441
+ (region_admin_bounds["cod_reg"] == region_code)
442
+ ].to_dict("records")
443
+
444
+ # Draw the administrative boundary using hv.Path
445
+ region_admin_bound_path = hv.Path(region_admin_bound)
446
+ region_admin_bound_path.opts(color=ACCENT, line_width=1.0)
447
+
448
+ # Build a Graph from Edges, Nodes and Bézier curves
449
+ region_flow_graph = hv.Graph(
450
+ (region_graph_edges.drop("curve", axis=1), region_graph_nodes, curves)
451
+ )
452
+
453
+ # Additional plot options
454
+ region_flow_graph.opts(
455
+ title="Incoming and outgoing commuting flows",
456
+ xlabel="",
457
+ ylabel="",
458
+ node_color="white",
459
+ node_hover_fill_color="magenta",
460
+ node_line_color=ACCENT,
461
+ node_size="size",
462
+ node_marker="marker",
463
+ edge_color="color",
464
+ edge_hover_line_color="magenta",
465
+ edge_line_width="width",
466
+ inspection_policy="edges",
467
+ tools=[flow_map_hover],
468
+ hooks=[hook],
469
+ frame_height=500,
470
+ )
471
+
472
+ # Compose the flow map
473
+ flow_map = (
474
+ hv.element.tiles.CartoLight()
475
+ * region_admin_bound_path
476
+ * region_flow_graph
477
+ )
478
+
479
+ return flow_map
480
+
481
+
482
+ # Load the edges as a Dataframe
483
+ @pn.cache
484
+ def get_edges_df():
485
+ return pd.read_json(
486
+ "https://raw.githubusercontent.com/ivandorte/panel-commuting-istat/main/data/edges.json",
487
+ orient="split",
488
+ dtype=EDGES_DTYPES,
489
+ )
490
+ edges_df = get_edges_df()
491
+
492
+ # Load the nodes as a Dataframe
493
+ @pn.cache
494
+ def get_nodes_df():
495
+ return pd.read_json(
496
+ "https://raw.githubusercontent.com/ivandorte/panel-commuting-istat/main/data/nodes.json",
497
+ orient="split",
498
+ dtype=NODES_DTYPES,
499
+ )
500
+
501
+ nodes_df = get_nodes_df()
502
+
503
+ # Load the italian regions as a Dataframe
504
+ def get_region_admin_bounds_df():
505
+ return pd.read_json(
506
+ "https://raw.githubusercontent.com/ivandorte/panel-commuting-istat/main/data/italian_regions.json",
507
+ orient="split",
508
+ dtype=ITA_REGIONS_DTYPES,
509
+ )
510
+ region_admin_bounds_df = get_region_admin_bounds_df()
511
+
512
+ # Region selector
513
+ region_options = dict(map(reversed, ITA_REGIONS.items()))
514
+ region_options = dict(sorted(region_options.items()))
515
+
516
+ region_select = pn.widgets.Select(
517
+ name="Region:",
518
+ options=region_options,
519
+ sizing_mode="stretch_width",
520
+ )
521
+
522
+ # Toggle buttons to select the commuting purpose
523
+ purpose_select = pn.widgets.ToggleGroup(
524
+ name="",
525
+ options=COMMUTING_PURPOSE,
526
+ behavior="radio",
527
+ sizing_mode="stretch_width",
528
+ button_type="primary", button_style="outline"
529
+ )
530
+
531
+ # Description pane
532
+ descr_pane = pn.pane.HTML(DASH_DESCR, styles={"text-align": "left"})
533
+
534
+ # Numeric indicator for incoming flows
535
+ incoming_numind_bind = pn.bind(
536
+ get_incoming_numind,
537
+ edges=edges_df,
538
+ region_code=region_select,
539
+ comm_purpose=purpose_select,
540
+ )
541
+
542
+ # Numeric indicator for outgoing flows
543
+ outgoing_numind_bind = pn.bind(
544
+ get_outgoing_numind,
545
+ edges=edges_df,
546
+ region_code=region_select,
547
+ comm_purpose=purpose_select,
548
+ )
549
+
550
+ # Numeric indicator for internal flows
551
+ internal_numind_bind = pn.bind(
552
+ get_internal_numind,
553
+ edges=edges_df,
554
+ region_code=region_select,
555
+ comm_purpose=purpose_select,
556
+ )
557
+
558
+ # Flow map
559
+ flowmap_bind = pn.bind(
560
+ get_flow_map,
561
+ nodes=nodes_df,
562
+ edges=edges_df,
563
+ region_admin_bounds=region_admin_bounds_df,
564
+ region_code=region_select,
565
+ comm_purpose=purpose_select,
566
+ )
567
+
568
+ # Compose the layout
569
+ layout = pn.Row(
570
+ pn.Column(
571
+ region_select,
572
+ purpose_select,
573
+ pn.Row(incoming_numind_bind, outgoing_numind_bind),
574
+ internal_numind_bind,
575
+ descr_pane,
576
+ width=350,
577
+ ),
578
+ flowmap_bind,
579
+ )
580
+
581
+ pn.template.FastListTemplate(
582
+ site="",
583
+ logo="https://raw.githubusercontent.com/ivandorte/panel-commuting-istat/main/icons/home_work.svg",
584
+ title=DASH_TITLE,
585
+ theme="default",
586
+ theme_toggle=False,
587
+ accent=ACCENT,
588
+ neutral_color="white",
589
+ main=[layout],
590
+ main_max_width="1000px",
591
+ ).servable()
592
+
593
+ await write_doc()
594
+ `
595
+
596
+ try {
597
+ const [docs_json, render_items, root_ids] = await self.pyodide.runPythonAsync(code)
598
+ self.postMessage({
599
+ type: 'render',
600
+ docs_json: docs_json,
601
+ render_items: render_items,
602
+ root_ids: root_ids
603
+ })
604
+ } catch(e) {
605
+ const traceback = `${e}`
606
+ const tblines = traceback.split('\n')
607
+ self.postMessage({
608
+ type: 'status',
609
+ msg: tblines[tblines.length-2]
610
+ });
611
+ throw e
612
+ }
613
+ }
614
+
615
+ self.onmessage = async (event) => {
616
+ const msg = event.data
617
+ if (msg.type === 'rendered') {
618
+ self.pyodide.runPythonAsync(`
619
+ from panel.io.state import state
620
+ from panel.io.pyodide import _link_docs_worker
621
+
622
+ _link_docs_worker(state.curdoc, sendPatch, setter='js')
623
+ `)
624
+ } else if (msg.type === 'patch') {
625
+ self.pyodide.globals.set('patch', msg.patch)
626
+ self.pyodide.runPythonAsync(`
627
+ state.curdoc.apply_json_patch(patch.to_py(), setter='js')
628
+ `)
629
+ self.postMessage({type: 'idle'})
630
+ } else if (msg.type === 'location') {
631
+ self.pyodide.globals.set('location', msg.location)
632
+ self.pyodide.runPythonAsync(`
633
+ import json
634
+ from panel.io.state import state
635
+ from panel.util import edit_readonly
636
+ if state.location:
637
+ loc_data = json.loads(location)
638
+ with edit_readonly(state.location):
639
+ state.location.param.update({
640
+ k: v for k, v in loc_data.items() if k in state.location.param
641
+ })
642
+ `)
643
+ }
644
+ }
645
+
646
+ startApplication()
index.py ADDED
@@ -0,0 +1,542 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Source: https://awesome-panel.org/resources/commuting_flows_italian_regions/
3
+ """
4
+ import holoviews as hv
5
+ import numpy as np
6
+ import pandas as pd
7
+ import panel as pn
8
+ from bokeh.models import HoverTool
9
+ from shapely.geometry import LineString
10
+
11
+ # Load the bokeh extension
12
+ hv.extension("bokeh")
13
+
14
+ # Set the sizing mode
15
+ pn.extension(sizing_mode="stretch_width")
16
+
17
+ # Dashboard title
18
+ DASH_TITLE = "Commuting flows between Italian Regions"
19
+
20
+ # Default colors for the dashboard
21
+ ACCENT = "#2f4f4f"
22
+ INCOMING_COLOR = "rgba(0, 108, 151, 0.75)"
23
+ OUTGOING_COLOR = "rgba(199, 81, 51, 0.75)"
24
+ INTERNAL_COLOR = "rgba(47, 79, 79, 0.55)"
25
+
26
+ # Default colors for indicators
27
+ DEFAULT_COLOR = "white"
28
+ TITLE_SIZE = "18pt"
29
+ FONT_SIZE = "20pt"
30
+
31
+ # Min/Max node size
32
+ MIN_PT_SIZE = 7
33
+ MAX_PT_SIZE = 10
34
+
35
+ # Min/Max curve width
36
+ MIN_LW = 1
37
+ MAX_LW = 10
38
+
39
+ # Dataframes dtypes
40
+ ITA_REGIONS_DTYPES = {
41
+ "cod_reg": "uint8",
42
+ "den_reg": "object",
43
+ "x": "object",
44
+ "y": "object",
45
+ }
46
+
47
+ NODES_DTYPES = {
48
+ "cod_reg": "uint8",
49
+ "x": "float64",
50
+ "y": "float64",
51
+ }
52
+
53
+ EDGES_DTYPES = {
54
+ "motivo": "object",
55
+ "interno": "bool",
56
+ "flussi": "uint32",
57
+ "reg_o": "uint8",
58
+ "reg_d": "uint8",
59
+ "x_o": "float64",
60
+ "y_o": "float64",
61
+ "x_d": "float64",
62
+ "y_d": "float64",
63
+ }
64
+
65
+ # Dictionary that maps region code to its name
66
+ ITA_REGIONS = {
67
+ 1: "Piemonte",
68
+ 2: "Valle d'Aosta/Vallée d'Aoste",
69
+ 3: "Lombardia",
70
+ 4: "Trentino-Alto Adige/Südtirol",
71
+ 5: "Veneto",
72
+ 6: "Friuli-Venezia Giulia",
73
+ 7: "Liguria",
74
+ 8: "Emilia-Romagna",
75
+ 9: "Toscana",
76
+ 10: "Umbria",
77
+ 11: "Marche",
78
+ 12: "Lazio",
79
+ 13: "Abruzzo",
80
+ 14: "Molise",
81
+ 15: "Campania",
82
+ 16: "Puglia",
83
+ 17: "Basilicata",
84
+ 18: "Calabria",
85
+ 19: "Sicilia",
86
+ 20: "Sardegna",
87
+ }
88
+
89
+ # Dictionary of options (Label/option) for commuting purpose
90
+ COMMUTING_PURPOSE = {
91
+ "Work": "Lavoro",
92
+ "Study": "Studio",
93
+ "Total": "Totale",
94
+ }
95
+
96
+ # Dashboard description
97
+ DASH_DESCR = f"""
98
+ <div>
99
+ <hr />
100
+ <p>A Panel dashboard showing <b style="color:{INCOMING_COLOR};">incoming</b>
101
+ and <b style="color:{OUTGOING_COLOR};">outgoing</b> commuting flows
102
+ for work and study between Italian Regions.</p>
103
+ <p>The width of the curves reflects the magnitude of the flows.</p>
104
+ <p>
105
+ <a href="https://www.istat.it/it/archivio/139381" target="_blank">Commuting data</a> from the
106
+ 15th Population and Housing Census (Istat, 2011).
107
+ </p>
108
+ <p>
109
+ <a href="https://www.istat.it/it/archivio/222527" target="_blank">Administrative boundaries</a> from
110
+ ISTAT.
111
+ </p>
112
+ <hr />
113
+ </div>
114
+ """
115
+
116
+ CSS_FIX = """
117
+ :host(.outline) .bk-btn.bk-btn-primary.bk-active, :host(.outline) .bk-btn.bk-btn-primary:active {
118
+ color: var(--foreground-on-accent-rest) !important;
119
+ }
120
+ """
121
+
122
+ if not CSS_FIX in pn.config.raw_css:
123
+ pn.config.raw_css.append(CSS_FIX)
124
+
125
+
126
+ def get_incoming_numind(edges, region_code, comm_purpose):
127
+ """
128
+ Returns the total incoming commuters to the selected Region.
129
+ """
130
+
131
+ # Get the value of incoming commuters
132
+ if comm_purpose == "Totale":
133
+ query = f"reg_d == {region_code} & interno == 0"
134
+ else:
135
+ query = f"(reg_d == {region_code} & motivo == '{comm_purpose}' & interno == 0)"
136
+
137
+ flows = edges.query(query)["flussi"].sum()
138
+
139
+ return pn.indicators.Number(
140
+ name="Incoming",
141
+ value=flows,
142
+ default_color=DEFAULT_COLOR,
143
+ styles={"background": INCOMING_COLOR, "padding": "5px 10px 5px 10px", "border-radius": "5px"},
144
+ title_size=TITLE_SIZE,
145
+ font_size=FONT_SIZE,
146
+ sizing_mode="stretch_width",
147
+ align="center",
148
+ css_classes=["center_number"],
149
+ )
150
+
151
+
152
+ def get_outgoing_numind(edges, region_code, comm_purpose):
153
+ """
154
+ Returns the outgoing commuters from
155
+ the selected Region.
156
+ """
157
+
158
+ # Get the value of outgoing commuters
159
+ if comm_purpose == "Totale":
160
+ query = f"reg_o == {region_code} & interno == 0"
161
+ else:
162
+ query = f"(reg_o == {region_code} & motivo == '{comm_purpose}' & interno == 0)"
163
+
164
+ flows = edges.query(query)["flussi"].sum()
165
+
166
+ return pn.indicators.Number(
167
+ name="Outgoing",
168
+ value=flows,
169
+ default_color=DEFAULT_COLOR,
170
+ styles={"background": OUTGOING_COLOR, "padding": "5px 10px 5px 10px", "border-radius": "5px"},
171
+ title_size=TITLE_SIZE,
172
+ font_size=FONT_SIZE,
173
+ sizing_mode="stretch_width",
174
+ align="center",
175
+ css_classes=["center_number"],
176
+ )
177
+
178
+
179
+ def get_internal_numind(edges, region_code, comm_purpose):
180
+ """
181
+ Returns the number of internal commuters of
182
+ the selected Region.
183
+ """
184
+
185
+ # Get the value of internal commuters
186
+ if comm_purpose == "Totale":
187
+ query = f"reg_o == {region_code} & interno == 1"
188
+ else:
189
+ query = f"(reg_o == {region_code} & motivo == '{comm_purpose}' & interno == 1)"
190
+
191
+ flows = edges.query(query)["flussi"].sum()
192
+
193
+ return pn.indicators.Number(
194
+ name="Internal mobility",
195
+ value=flows,
196
+ default_color=DEFAULT_COLOR,
197
+ styles={"background": INTERNAL_COLOR, "padding": "5px 10px 5px 10px", "border-radius": "5px"},
198
+ title_size=TITLE_SIZE,
199
+ font_size=FONT_SIZE,
200
+ sizing_mode="stretch_width",
201
+ align="center",
202
+ css_classes=["center_number"],
203
+ )
204
+
205
+
206
+ def filter_edges(edges, region_code, comm_purpose):
207
+ """
208
+ This function filters the rows of the edges for
209
+ the selected Region and commuting purpose.
210
+ """
211
+
212
+ if comm_purpose == "Totale":
213
+ query = f"(reg_o == {region_code} & interno == 0) |"
214
+ query += f" (reg_d == {region_code} & interno == 0)"
215
+ else:
216
+ query = f"(reg_o == {region_code} & motivo == '{comm_purpose}' & interno == 0) |"
217
+ query += f" (reg_d == {region_code} & motivo == '{comm_purpose}' & interno == 0)"
218
+ return edges.query(query)
219
+
220
+
221
+ def get_nodes(nodes, edges, region_code, comm_purpose):
222
+ """
223
+ Get the graph's nodes for the selected Region and commuting purpose
224
+ """
225
+
226
+ # Filter the edges by Region and commuting purpose
227
+ filt_edges = filter_edges(edges, region_code, comm_purpose)
228
+
229
+ # Find the unique values of region codes
230
+ region_codes = np.unique(filt_edges[["reg_o", "reg_d"]].values)
231
+
232
+ # Filter the nodes
233
+ nodes = nodes[nodes["cod_reg"].isin(region_codes)]
234
+
235
+ # Reoder the columns for hv.Graph
236
+ nodes = nodes[["x", "y", "cod_reg"]]
237
+
238
+ # Assign the node size
239
+ nodes["size"] = np.where(
240
+ nodes["cod_reg"] == region_code, MAX_PT_SIZE, MIN_PT_SIZE
241
+ )
242
+
243
+ # Assigns a marker to the nodes
244
+ nodes["marker"] = np.where(
245
+ nodes["cod_reg"] == region_code, "square", "circle"
246
+ )
247
+
248
+ return nodes
249
+
250
+
251
+ def get_bezier_curve(x_o, y_o, x_d, y_d, steps=25):
252
+ """
253
+ Draw a Bézier curve defined by a start point, endpoint and a control points
254
+ Source: https://stackoverflow.com/questions/69804595/trying-to-make-a-bezier-curve-on-pygame-library
255
+ """
256
+
257
+ # Generate the O/D linestring
258
+ od_line = LineString([(x_o, y_o), (x_d, y_d)])
259
+
260
+ # Calculate the offset distance of the control point
261
+ offset_distance = od_line.length / 2
262
+
263
+ # Create a line parallel to the original at the offset distance
264
+ offset_pline = od_line.parallel_offset(offset_distance, "left")
265
+
266
+ # Get the XY coodinates of the control point
267
+ ctrl_x = offset_pline.centroid.x
268
+ ctrl_y = offset_pline.centroid.y
269
+
270
+ # Calculate the XY coordinates of the Bézier curve
271
+ t = np.array([i * 1 / steps for i in range(0, steps + 1)])
272
+ x_coords = x_o * (1 - t) ** 2 + 2 * (1 - t) * t * ctrl_x + x_d * t**2
273
+ y_coords = y_o * (1 - t) ** 2 + 2 * (1 - t) * t * ctrl_y + y_d * t**2
274
+
275
+ return (x_coords, y_coords)
276
+
277
+
278
+ def get_edge_width(flow, min_flow, max_flow):
279
+ """
280
+ This function calculates the width of the curves
281
+ according to the magnitude of the flow.
282
+ """
283
+
284
+ return MIN_LW + np.power(flow - min_flow, 0.57) * (
285
+ MAX_LW - MIN_LW
286
+ ) / np.power(max_flow - min_flow, 0.57)
287
+
288
+
289
+ def get_edges(nodes, edges, region_code, comm_purpose):
290
+ """
291
+ Get the graph's edges for the selected Region and commuting purpose
292
+ """
293
+
294
+ # Filter the edges by Region and commuting purpose
295
+ filt_edges = filter_edges(edges, region_code, comm_purpose).copy()
296
+
297
+ # Aggregate the flows by Region of origin and destination
298
+ if comm_purpose == "Totale":
299
+ filt_edges = (
300
+ filt_edges.groupby(["reg_o", "reg_d"])
301
+ .agg(
302
+ motivo=("motivo", "first"),
303
+ interno=("interno", "first"),
304
+ flussi=("flussi", "sum"),
305
+ )
306
+ .reset_index()
307
+ )
308
+
309
+ # Assign Region names
310
+ filt_edges.loc[:,"den_reg_o"] = filt_edges["reg_o"].map(ITA_REGIONS)
311
+ filt_edges.loc[:,"den_reg_d"] = filt_edges["reg_d"].map(ITA_REGIONS)
312
+
313
+ # Add xy coordinates of origin
314
+ filt_edges = filt_edges.merge(
315
+ nodes.add_suffix("_o"), left_on="reg_o", right_on="cod_reg_o"
316
+ )
317
+
318
+ # Add xy coordinates of destination
319
+ filt_edges = filt_edges.merge(
320
+ nodes.add_suffix("_d"), left_on="reg_d", right_on="cod_reg_d"
321
+ )
322
+
323
+ # Get the Bézier curve
324
+ filt_edges["curve"] = filt_edges.apply(
325
+ lambda row: get_bezier_curve(
326
+ row["x_o"], row["y_o"], row["x_d"], row["y_d"]
327
+ ),
328
+ axis=1,
329
+ )
330
+
331
+ # Get the minimum/maximum flow
332
+ min_flow = filt_edges["flussi"].min()
333
+ max_flow = filt_edges["flussi"].max()
334
+
335
+ # Calculate the curve width
336
+ filt_edges["width"] = filt_edges.apply(
337
+ lambda row: get_edge_width(
338
+ row["flussi"],
339
+ min_flow,
340
+ max_flow,
341
+ ),
342
+ axis=1,
343
+ )
344
+
345
+ # Assigns the color to the incoming/outgoing edges
346
+ filt_edges["color"] = np.where(
347
+ filt_edges["reg_d"] == region_code, INCOMING_COLOR, OUTGOING_COLOR
348
+ )
349
+
350
+ filt_edges = filt_edges.sort_values(by="flussi")
351
+
352
+ return filt_edges
353
+
354
+
355
+ def get_flow_map(nodes, edges, region_admin_bounds, region_code, comm_purpose):
356
+ """
357
+ Returns a Graph showing incoming and outgoing commuting flows
358
+ for the selected Region and commuting purpose.
359
+ """
360
+
361
+ def hook(plot, element):
362
+ """
363
+ Custom hook for disabling x/y tick lines/labels
364
+ """
365
+ plot.state.xaxis.major_tick_line_color = None
366
+ plot.state.xaxis.minor_tick_line_color = None
367
+ plot.state.xaxis.major_label_text_font_size = "0pt"
368
+ plot.state.yaxis.major_tick_line_color = None
369
+ plot.state.yaxis.minor_tick_line_color = None
370
+ plot.state.yaxis.major_label_text_font_size = "0pt"
371
+
372
+ # Define a custom Hover tool
373
+ flow_map_hover = HoverTool(
374
+ tooltips=[
375
+ ("Origin", "@den_reg_o"),
376
+ ("Destination", "@den_reg_d"),
377
+ ("Commuters", "@flussi"),
378
+ ]
379
+ )
380
+
381
+ # Get the Nodes of the selected Region and commuting purpose
382
+ region_graph_nodes = get_nodes(nodes, edges, region_code, comm_purpose)
383
+
384
+ # Get the Edges of the selected Region and commuting purpose
385
+ region_graph_edges = get_edges(nodes, edges, region_code, comm_purpose)
386
+
387
+ # Get the list of Bézier curves
388
+ curves = region_graph_edges["curve"].to_list()
389
+
390
+ # Get the administrative boundary of the selected Region
391
+ region_admin_bound = region_admin_bounds[
392
+ (region_admin_bounds["cod_reg"] == region_code)
393
+ ].to_dict("records")
394
+
395
+ # Draw the administrative boundary using hv.Path
396
+ region_admin_bound_path = hv.Path(region_admin_bound)
397
+ region_admin_bound_path.opts(color=ACCENT, line_width=1.0)
398
+
399
+ # Build a Graph from Edges, Nodes and Bézier curves
400
+ region_flow_graph = hv.Graph(
401
+ (region_graph_edges.drop("curve", axis=1), region_graph_nodes, curves)
402
+ )
403
+
404
+ # Additional plot options
405
+ region_flow_graph.opts(
406
+ title="Incoming and outgoing commuting flows",
407
+ xlabel="",
408
+ ylabel="",
409
+ node_color="white",
410
+ node_hover_fill_color="magenta",
411
+ node_line_color=ACCENT,
412
+ node_size="size",
413
+ node_marker="marker",
414
+ edge_color="color",
415
+ edge_hover_line_color="magenta",
416
+ edge_line_width="width",
417
+ inspection_policy="edges",
418
+ tools=[flow_map_hover],
419
+ hooks=[hook],
420
+ frame_height=500,
421
+ )
422
+
423
+ # Compose the flow map
424
+ flow_map = (
425
+ hv.element.tiles.CartoLight()
426
+ * region_admin_bound_path
427
+ * region_flow_graph
428
+ )
429
+
430
+ return flow_map
431
+
432
+
433
+ # Load the edges as a Dataframe
434
+ @pn.cache
435
+ def get_edges_df():
436
+ return pd.read_json(
437
+ "https://raw.githubusercontent.com/ivandorte/panel-commuting-istat/main/data/edges.json",
438
+ orient="split",
439
+ dtype=EDGES_DTYPES,
440
+ )
441
+ edges_df = get_edges_df()
442
+
443
+ # Load the nodes as a Dataframe
444
+ @pn.cache
445
+ def get_nodes_df():
446
+ return pd.read_json(
447
+ "https://raw.githubusercontent.com/ivandorte/panel-commuting-istat/main/data/nodes.json",
448
+ orient="split",
449
+ dtype=NODES_DTYPES,
450
+ )
451
+
452
+ nodes_df = get_nodes_df()
453
+
454
+ # Load the italian regions as a Dataframe
455
+ def get_region_admin_bounds_df():
456
+ return pd.read_json(
457
+ "https://raw.githubusercontent.com/ivandorte/panel-commuting-istat/main/data/italian_regions.json",
458
+ orient="split",
459
+ dtype=ITA_REGIONS_DTYPES,
460
+ )
461
+ region_admin_bounds_df = get_region_admin_bounds_df()
462
+
463
+ # Region selector
464
+ region_options = dict(map(reversed, ITA_REGIONS.items()))
465
+ region_options = dict(sorted(region_options.items()))
466
+
467
+ region_select = pn.widgets.Select(
468
+ name="Region:",
469
+ options=region_options,
470
+ sizing_mode="stretch_width",
471
+ )
472
+
473
+ # Toggle buttons to select the commuting purpose
474
+ purpose_select = pn.widgets.ToggleGroup(
475
+ name="",
476
+ options=COMMUTING_PURPOSE,
477
+ behavior="radio",
478
+ sizing_mode="stretch_width",
479
+ button_type="primary", button_style="outline"
480
+ )
481
+
482
+ # Description pane
483
+ descr_pane = pn.pane.HTML(DASH_DESCR, styles={"text-align": "left"})
484
+
485
+ # Numeric indicator for incoming flows
486
+ incoming_numind_bind = pn.bind(
487
+ get_incoming_numind,
488
+ edges=edges_df,
489
+ region_code=region_select,
490
+ comm_purpose=purpose_select,
491
+ )
492
+
493
+ # Numeric indicator for outgoing flows
494
+ outgoing_numind_bind = pn.bind(
495
+ get_outgoing_numind,
496
+ edges=edges_df,
497
+ region_code=region_select,
498
+ comm_purpose=purpose_select,
499
+ )
500
+
501
+ # Numeric indicator for internal flows
502
+ internal_numind_bind = pn.bind(
503
+ get_internal_numind,
504
+ edges=edges_df,
505
+ region_code=region_select,
506
+ comm_purpose=purpose_select,
507
+ )
508
+
509
+ # Flow map
510
+ flowmap_bind = pn.bind(
511
+ get_flow_map,
512
+ nodes=nodes_df,
513
+ edges=edges_df,
514
+ region_admin_bounds=region_admin_bounds_df,
515
+ region_code=region_select,
516
+ comm_purpose=purpose_select,
517
+ )
518
+
519
+ # Compose the layout
520
+ layout = pn.Row(
521
+ pn.Column(
522
+ region_select,
523
+ purpose_select,
524
+ pn.Row(incoming_numind_bind, outgoing_numind_bind),
525
+ internal_numind_bind,
526
+ descr_pane,
527
+ width=350,
528
+ ),
529
+ flowmap_bind,
530
+ )
531
+
532
+ pn.template.FastListTemplate(
533
+ site="",
534
+ logo="https://raw.githubusercontent.com/ivandorte/panel-commuting-istat/main/icons/home_work.svg",
535
+ title=DASH_TITLE,
536
+ theme="default",
537
+ theme_toggle=False,
538
+ accent=ACCENT,
539
+ neutral_color="white",
540
+ main=[layout],
541
+ main_max_width="1000px",
542
+ ).servable()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ holoviews
2
+ numpy
3
+ pandas
4
+ panel
5
+ shapely