cjc0013 commited on
Commit
948ff7f
·
verified ·
1 Parent(s): 2ff2a78

Fix drone map playback with Gradio slider plus tested play controls

Browse files
README.md CHANGED
@@ -15,6 +15,6 @@ python_version: 3.11
15
 
16
  This private preview maps reported drone sightings from news stories over official covered military and civilian areas.
17
 
18
- Use the built-in time slider and play button in the map to watch red dots appear across time, then match the `event_id` on the map to the row data directly below it.
19
 
20
  Each point is tied to one public event row and a linked source list. This release tracks reported drone sightings and does not prove intent, threat, wrongdoing, or a verified security breach.
 
15
 
16
  This private preview maps reported drone sightings from news stories over official covered military and civilian areas.
17
 
18
+ Use the time slider plus the Play and Pause controls to watch red dots appear across time, then match the `event_id` on the map to the row data directly below it.
19
 
20
  Each point is tied to one public event row and a linked source list. This release tracks reported drone sightings and does not prove intent, threat, wrongdoing, or a verified security breach.
dataset_bundle/public_release_manifest.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "public_version": "drone-sightings-slice-2026-04-v1-smoke",
3
  "title": "Drone Sightings Near Covered U.S. Military and Civilian Areas",
4
- "release_date": "2026-04-20T18:04:25.903466+00:00",
5
  "source_run_name": "drone_sightings_smoke_20260420",
6
  "slice_description": "A small, review-oriented slice of U.S. news-reported drone sightings mapped against official covered-area registries.",
7
  "source_window": {
 
1
  {
2
  "public_version": "drone-sightings-slice-2026-04-v1-smoke",
3
  "title": "Drone Sightings Near Covered U.S. Military and Civilian Areas",
4
+ "release_date": "2026-04-20T18:15:50.018651+00:00",
5
  "source_run_name": "drone_sightings_smoke_20260420",
6
  "slice_description": "A small, review-oriented slice of U.S. news-reported drone sightings mapped against official covered-area registries.",
7
  "source_window": {
public_copy.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
  "title": "Drone Sightings Near Covered U.S. Military and Civilian Areas",
3
  "dataset_bundle_prefix": "dataset_bundle",
4
- "landing_markdown": "# Drone Sightings Near Covered U.S. Military and Civilian Areas\n\nThis private preview maps reported drone sightings from news stories over official covered military and civilian areas.\n\nUse the built-in time slider and play button in the map to watch red dots appear across time, then match the `event_id` on the map to the row data directly below it.\n\nEach point is tied to one public event row and a linked source list. This release tracks reported drone sightings and does not prove intent, threat, wrongdoing, or a verified security breach."
5
  }
 
1
  {
2
  "title": "Drone Sightings Near Covered U.S. Military and Civilian Areas",
3
  "dataset_bundle_prefix": "dataset_bundle",
4
+ "landing_markdown": "# Drone Sightings Near Covered U.S. Military and Civilian Areas\n\nThis private preview maps reported drone sightings from news stories over official covered military and civilian areas.\n\nUse the time slider plus the Play and Pause controls to watch red dots appear across time, then match the `event_id` on the map to the row data directly below it.\n\nEach point is tied to one public event row and a linked source list. This release tracks reported drone sightings and does not prove intent, threat, wrongdoing, or a verified security breach."
5
  }
public_space_app.py CHANGED
@@ -29,6 +29,30 @@ def load_release_data(public_copy_path: Path) -> dict:
29
  }
30
 
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  def _point_table(events: pd.DataFrame) -> pd.DataFrame:
33
  ordered = events.copy()
34
  ordered["event_date"] = ordered["event_date"].astype(str)
@@ -52,12 +76,9 @@ def _point_table(events: pd.DataFrame) -> pd.DataFrame:
52
  ]
53
 
54
 
55
- def _build_map(events: pd.DataFrame):
56
  if events.empty:
57
  return px.scatter_mapbox(pd.DataFrame({"lat": [], "lon": []}), lat="lat", lon="lon", zoom=3, height=560)
58
- frame_values = sorted(str(value) for value in events["event_date"].astype(str).unique())
59
- events = events.copy()
60
- events["animation_frame"] = events["event_date"].astype(str)
61
  fig = px.scatter_mapbox(
62
  events,
63
  lat="lat",
@@ -76,30 +97,16 @@ def _build_map(events: pd.DataFrame):
76
  "lon": False,
77
  },
78
  custom_data=["event_id"],
79
- animation_frame="animation_frame",
80
  zoom=3,
81
  height=560,
82
  )
83
  fig.update_layout(
84
  mapbox_style="open-street-map",
85
  margin={"l": 0, "r": 0, "t": 40, "b": 0},
86
- title="Reported drone sightings over time",
87
  showlegend=False,
88
  )
89
  fig.update_traces(marker={"size": 12, "opacity": 0.85})
90
- if frame_values:
91
- fig.update_layout(
92
- updatemenus=[
93
- {
94
- "type": "buttons",
95
- "showactive": False,
96
- "buttons": [
97
- {"label": "Play", "method": "animate", "args": [None, {"frame": {"duration": 600, "redraw": True}, "fromcurrent": True}]},
98
- {"label": "Pause", "method": "animate", "args": [[None], {"frame": {"duration": 0, "redraw": False}, "mode": "immediate"}]},
99
- ],
100
- }
101
- ]
102
- )
103
  return fig
104
 
105
 
@@ -144,30 +151,149 @@ def _detail_markdown(event_sources: pd.DataFrame, events: pd.DataFrame, event_id
144
  return "\n".join(lines)
145
 
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  def build_app(public_copy_path: str | Path):
148
  public_copy_path = Path(public_copy_path)
149
  payload = json.loads(public_copy_path.read_text(encoding="utf-8"))
150
  data = load_release_data(public_copy_path)
151
  events = data["events"]
152
  event_sources = data["event_sources"]
153
- point_table = _point_table(events)
 
154
 
155
  with gr.Blocks(title=payload["title"]) as app:
156
  gr.Markdown(payload["landing_markdown"])
 
157
  plot = gr.Plot(label="Drone sightings over time")
158
- gr.Markdown("## Point data")
159
- gr.Markdown("Every row below is one plotted point. The `event_id` shown on hover in the map matches the `event_id` in the table and detail panel.")
 
 
 
 
 
 
 
 
 
 
160
  table = gr.Dataframe(label="Point data used to draw the map", interactive=False)
161
- detail = gr.Markdown("Select a row in the point-data table to inspect the exact public data and source URLs used for that plotted point.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
- def _table_detail(evt: gr.SelectData):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  if not evt or evt.index is None:
165
- return "Select a row in the point-data table to inspect the exact public data and source URLs used for that plotted point."
166
  row_index = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
167
- if row_index >= len(point_table):
168
  return "No event details available."
169
- return _detail_markdown(event_sources, events, str(point_table.iloc[row_index]["event_id"]))
 
170
 
171
- table.select(_table_detail, outputs=detail)
172
- app.load(lambda: (_build_map(events), point_table), outputs=[plot, table])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  return app
 
29
  }
30
 
31
 
32
+ def _frame_dates(events: pd.DataFrame) -> list[str]:
33
+ if events.empty:
34
+ return []
35
+ return sorted(str(value) for value in events["event_date"].astype(str).unique())
36
+
37
+
38
+ def _frame_index(value, max_index: int) -> int:
39
+ if max_index <= 0:
40
+ return 0
41
+ try:
42
+ numeric = int(float(value))
43
+ except (TypeError, ValueError):
44
+ numeric = 0
45
+ return max(0, min(max_index, numeric))
46
+
47
+
48
+ def _visible_events(events: pd.DataFrame, frame_dates: list[str], frame_index: int) -> pd.DataFrame:
49
+ if events.empty or not frame_dates:
50
+ return events.iloc[0:0].copy()
51
+ frame_value = frame_dates[_frame_index(frame_index, len(frame_dates) - 1)]
52
+ visible = events[events["event_date"].astype(str) <= frame_value].copy()
53
+ return visible.sort_values(["event_date", "event_id", "headline"], ascending=[True, True, True]).reset_index(drop=True)
54
+
55
+
56
  def _point_table(events: pd.DataFrame) -> pd.DataFrame:
57
  ordered = events.copy()
58
  ordered["event_date"] = ordered["event_date"].astype(str)
 
76
  ]
77
 
78
 
79
+ def _build_map(events: pd.DataFrame, frame_label: str):
80
  if events.empty:
81
  return px.scatter_mapbox(pd.DataFrame({"lat": [], "lon": []}), lat="lat", lon="lon", zoom=3, height=560)
 
 
 
82
  fig = px.scatter_mapbox(
83
  events,
84
  lat="lat",
 
97
  "lon": False,
98
  },
99
  custom_data=["event_id"],
 
100
  zoom=3,
101
  height=560,
102
  )
103
  fig.update_layout(
104
  mapbox_style="open-street-map",
105
  margin={"l": 0, "r": 0, "t": 40, "b": 0},
106
+ title=f"Reported drone sightings through {frame_label}",
107
  showlegend=False,
108
  )
109
  fig.update_traces(marker={"size": 12, "opacity": 0.85})
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  return fig
111
 
112
 
 
151
  return "\n".join(lines)
152
 
153
 
154
+ def _detail_placeholder(frame_label: str) -> str:
155
+ return (
156
+ "Select a row in the point-data table to inspect the exact public data and source URLs used for a plotted point. "
157
+ f"The map and table currently show all events through `{frame_label}`."
158
+ )
159
+
160
+
161
+ def _frame_payload(events: pd.DataFrame, event_sources: pd.DataFrame, frame_dates: list[str], frame_index: int):
162
+ if not frame_dates:
163
+ empty_table = _point_table(events.iloc[0:0].copy())
164
+ return (
165
+ _build_map(events.iloc[0:0].copy(), "no data"),
166
+ empty_table,
167
+ "No dated events available.",
168
+ "## Point data",
169
+ "Time slider unavailable because there are no dated events in this release.",
170
+ empty_table.to_dict("records"),
171
+ "No event details available.",
172
+ )
173
+ visible = _visible_events(events, frame_dates, frame_index)
174
+ point_table = _point_table(visible)
175
+ frame_label = frame_dates[_frame_index(frame_index, len(frame_dates) - 1)]
176
+ return (
177
+ _build_map(visible, frame_label),
178
+ point_table,
179
+ f"### Showing events through `{frame_label}`",
180
+ "## Point data",
181
+ "Every row below is a point currently visible on the map. The `event_id` shown on hover matches the `event_id` in the table and detail panel.",
182
+ point_table.to_dict("records"),
183
+ _detail_placeholder(frame_label),
184
+ )
185
+
186
+
187
  def build_app(public_copy_path: str | Path):
188
  public_copy_path = Path(public_copy_path)
189
  payload = json.loads(public_copy_path.read_text(encoding="utf-8"))
190
  data = load_release_data(public_copy_path)
191
  events = data["events"]
192
  event_sources = data["event_sources"]
193
+ frame_dates = _frame_dates(events)
194
+ max_frame_index = max(0, len(frame_dates) - 1)
195
 
196
  with gr.Blocks(title=payload["title"]) as app:
197
  gr.Markdown(payload["landing_markdown"])
198
+ date_markdown = gr.Markdown()
199
  plot = gr.Plot(label="Drone sightings over time")
200
+ with gr.Row():
201
+ play_button = gr.Button("Play")
202
+ pause_button = gr.Button("Pause")
203
+ slider = gr.Slider(
204
+ minimum=0,
205
+ maximum=max_frame_index,
206
+ step=1,
207
+ value=max_frame_index,
208
+ label="Time slider",
209
+ )
210
+ point_heading = gr.Markdown("## Point data")
211
+ point_caption = gr.Markdown("Every row below is a point currently visible on the map.")
212
  table = gr.Dataframe(label="Point data used to draw the map", interactive=False)
213
+ table_rows = gr.State([])
214
+ detail = gr.Markdown("Select a row in the point-data table to inspect the exact public data and source URLs used for a plotted point.")
215
+ timer = gr.Timer(value=1.0, active=False)
216
+
217
+ def _render_frame(frame_value):
218
+ return _frame_payload(events, event_sources, frame_dates, _frame_index(frame_value, max_frame_index))
219
+
220
+ def _start_playback(frame_value):
221
+ next_index = _frame_index(frame_value, max_frame_index)
222
+ if next_index >= max_frame_index and frame_dates:
223
+ next_index = 0
224
+ plot_value, table_value, date_value, heading_value, caption_value, rows_value, detail_value = _render_frame(next_index)
225
+ return (
226
+ gr.Timer(active=True),
227
+ next_index,
228
+ plot_value,
229
+ table_value,
230
+ date_value,
231
+ heading_value,
232
+ caption_value,
233
+ rows_value,
234
+ detail_value,
235
+ )
236
 
237
+ def _pause_playback():
238
+ return gr.Timer(active=False)
239
+
240
+ def _advance_frame(frame_value):
241
+ if not frame_dates:
242
+ plot_value, table_value, date_value, heading_value, caption_value, rows_value, detail_value = _render_frame(0)
243
+ return (
244
+ gr.Timer(active=False),
245
+ 0,
246
+ plot_value,
247
+ table_value,
248
+ date_value,
249
+ heading_value,
250
+ caption_value,
251
+ rows_value,
252
+ detail_value,
253
+ )
254
+ current_index = _frame_index(frame_value, max_frame_index)
255
+ next_index = min(max_frame_index, current_index + 1)
256
+ plot_value, table_value, date_value, heading_value, caption_value, rows_value, detail_value = _render_frame(next_index)
257
+ return (
258
+ gr.Timer(active=(next_index < max_frame_index)),
259
+ next_index,
260
+ plot_value,
261
+ table_value,
262
+ date_value,
263
+ heading_value,
264
+ caption_value,
265
+ rows_value,
266
+ detail_value,
267
+ )
268
+
269
+ def _table_detail(current_rows, evt: gr.SelectData):
270
  if not evt or evt.index is None:
271
+ return "Select a row in the point-data table to inspect the exact public data and source URLs used for a plotted point."
272
  row_index = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
273
+ if row_index >= len(current_rows):
274
  return "No event details available."
275
+ event_id = str(current_rows[row_index]["event_id"])
276
+ return _detail_markdown(event_sources, events, event_id)
277
 
278
+ slider.change(
279
+ _render_frame,
280
+ inputs=slider,
281
+ outputs=[plot, table, date_markdown, point_heading, point_caption, table_rows, detail],
282
+ )
283
+ play_button.click(
284
+ _start_playback,
285
+ inputs=slider,
286
+ outputs=[timer, slider, plot, table, date_markdown, point_heading, point_caption, table_rows, detail],
287
+ )
288
+ pause_button.click(_pause_playback, outputs=timer)
289
+ timer.tick(
290
+ _advance_frame,
291
+ inputs=slider,
292
+ outputs=[timer, slider, plot, table, date_markdown, point_heading, point_caption, table_rows, detail],
293
+ )
294
+ table.select(_table_detail, inputs=table_rows, outputs=detail)
295
+ app.load(
296
+ lambda: _render_frame(max_frame_index),
297
+ outputs=[plot, table, date_markdown, point_heading, point_caption, table_rows, detail],
298
+ )
299
  return app