Parler Metadata in Leaflet
Interactive cluster map and timeline of GPS data
Background
Recently heard about the Parler web scrape that happened a few days ago. It was awesome to read about the archiving work that @donk_enby led (link to her Twitter page). More info on Parler here.
Found the processed metadata on Github yesterday evening. Thanks to zumbov2 on Github for his repo and data export. Got to work after I had the data. Thought it would be cool to see in Leaflet, combined with leaftime for timelines in Leaflet.
The raw data source I used had 64,399 records with GPS coordinates from 2011–08–11 to 2021–01–10. Had creation date, latitude, and longitude fields to work with. The count I read across multiple new articles was 68K, but there are ~5% of records with missing or bad coordinates in the data. Checked full data that Kyle Mcdonald posted and obtained same record count after filtering out for records with long/lat at origin (0,0).
Note that the data represents only content that was publicly available, as pointed out by donk_enby on her Vice article and this Wired article.
The first map is a cluster map focused on Washington, D.C. on January 6, 2021. Second is a timeline that looks at the data from November 1, 2020 onward (U.S. election occurred on Tuesday, November 3). Feel free to interact with the maps as you like. This was a great opportunity for me to learn more about Leaflet. Code included below maps.
Other cool projects
- Patr10tic made an interactive map of Parler videos called Y’all Qaeda. Story on it here. The tool allows you to watch videos according to their GPS location.
- u/_Xeet_ posted an animated heatmap of the GPS metadata on January 6 in DC on r/dataisbeautiful here.
- mstrlaw on Github also made an interactive map of Parler videos (with a better UI) here. This project is taking it to the next level.
- ProPublica recently published an interactive timeline with over 500 videos here.
- JohnM recently shared metadata and over 700 videos from the US Capitol on January 6 on Kaggle here, part of the 1,000 videos that Tommy Carstensen shared about on his website.
Maps
Code
knitr::opts_chunk$set(echo = TRUE)
library(tidyverse)
library(lubridate)
library(leaflet)
library(htmltools)
library(htmlwidgets)
library(sf)
library(geojsonio)
library(leaftime)parler_all <- read_csv("https://raw.githubusercontent.com/zumbov2/parler-video-metadata/main/parler_vids_geolocation_time.csv")
dt2 <- parler_all %>%
mutate(CreateDate = ymd_hms(CreateDate)) %>%
mutate(start = as_date(CreateDate), end = as_date(CreateDate)) %>%
filter(start >= as_date("2020-11-01")) %>%
mutate(start = ymd_hm(paste(start,"00:01 AM")),
end = ymd_hm(paste(end,"11:59 PM"))
)
#dt2 parler video data time lapse
dt2_geo <- geojsonio::geojson_json(dt2)
#on 2021-6-1
dt2_jan6 <- dt2 %>%
filter(date(CreateDate) == "2021-01-06")
# filter(lat > 38.6 & lat < 39.2)
tag.map.title_white <- tags$style(HTML("
.leaflet-control.map-title-white {
position: sticky !important;
left: 25%;
right: 50%;
text-align: center;
font-weight: bold;
font-size: 18px;
color: WhiteSmoke;
}
"))
title <- tags$div(
tag.map.title_white, HTML("Parler Metadata on Jan 6, 2021")
)
tag.map.title_black <- tags$style(HTML("
.leaflet-control.map-title-black {
position: sticky !important;
left: 25%;
right: 50%;
text-align: center;
font-weight: bold;
font-size: 18px;
color: black;
}
"))
title2 <- tags$div(
tag.map.title_black, HTML("Parler Metadata over Time")
)# Leaflet map1
leaflet(dt2_jan6) %>%
addControl(title, position = "topright", className="map-title-white") %>%
setView(lng = -77.025, lat = 38.892, zoom = 12) %>%
addProviderTiles(providers$Esri.NatGeoWorldMap) %>%
addCircleMarkers(
color = "red", radius=7,
stroke = FALSE, fillOpacity = 0.7,
popup = ~paste(as.character(CreateDate),"at (", as.character(lat), as.character(lon),")"),
labelOptions = labelOptions(style = list("font-size" = "8px")),
clusterOptions = markerClusterOptions(), clusterId = "quakesCluster"
) %>%
addMiniMap(
tiles = providers$Esri.WorldStreetMap,
toggleDisplay = TRUE) %>%
addEasyButton(easyButton(
states = list(
easyButtonState(
stateName="unfrozen-markers",
icon="ion-toggle",
title="Freeze Clusters",
onClick = JS("
function(btn, map) {
var clusterManager =
map.layerManager.getLayer('cluster', 'quakesCluster');
clusterManager.freezeAtZoom();
btn.state('frozen-markers');
}")
),
easyButtonState(
stateName="frozen-markers",
icon="ion-toggle-filled",
title="UnFreeze Clusters",
onClick = JS("
function(btn, map) {
var clusterManager =
map.layerManager.getLayer('cluster', 'quakesCluster');
clusterManager.unfreeze();
btn.state('unfrozen-markers');
}")))))#Leaflet map2
leaflet(dt2_geo, options = leafletOptions(zoomControl = TRUE,
dragging = TRUE,
preferCanvas = FALSE)) %>%
addTiles() %>%
addControl(title2, position = "topright", className="map-title-black") %>%
setView(lng = -77.025, lat = 38.892, zoom = 6) %>%
addTimeline(
width = "40%",
sliderOpts = sliderOptions(
formatOutput = htmlwidgets::JS("function(StartDate) {return new Date(StartDate).toDateString()}"),
# steps=length(unique(dt2$start)),
position = "bottomright",
duration = 130000,
showTicks = TRUE,
enablePlayback = TRUE,
enableKeyboardControls = TRUE,
waitToUpdateMap = FALSE),
timelineOpts = timelineOptions(
styleOptions = styleOptions(
color = "red",
fillOpacity = 0.5,
radius = 2))
) %>%
addEasyButton(easyButton(icon="fa-crosshairs", title="Locate Me",
onClick=JS("function(btn, map){ map.locate({setView: true}); }"))) %>%
addEasyButton(easyButton(icon="fa-globe", title="Zoom to Level 1",
onClick=JS("function(btn, map){ map.setZoom(1); }")))