Make point pattern spookier
If I show you the following graphic, what do you think this is?

Take a moment and think about it before reading any further.
…
…
…
Anyway, I’ll begin to reveal the answer now.
So, as I was hunting (pun-intended) for a dataset for my data viz class to illustrate how to make maps, I stumbled upon a TidyTuesday gem: Haunted places in the United States (week 41, 2023).
# A tibble: 10,864 Ă— 10
city country description location state state_abbrev longitude
<chr> <chr> <chr> <chr> <chr> <chr> <dbl>
1 Ada United… "Ada witch… Ada Cem… Mich… MI -85.5
2 Addison United… "A little … North A… Mich… MI -84.4
3 Adrian United… "If you ta… Ghost T… Mich… MI -84.0
4 Adrian United… "In the 19… Siena H… Mich… MI -84.0
5 Albion United… "Kappa Del… Albion … Mich… MI -84.7
6 Albion United… "A mysteri… Riversi… Mich… MI -84.8
7 Algoma T… United… "On a wind… Hell's … Mich… MI NA
8 Algonac United… "Morrow Ro… Morrow … Mich… MI -82.6
9 Allegan United… "People re… Elks Lo… Mich… MI -85.8
10 Allegan United… "Various g… The Gri… Mich… MI -85.9
# ℹ 10,854 more rows
# ℹ 3 more variables: latitude <dbl>, city_longitude <dbl>,
# city_latitude <dbl>
Nothing complicated here. The data contain locations of haunted spots across the country. (And sorry, Alaska and Hawaii, I’m omitting you for convenience.)
In fancy spatial data terms, this is a case of point pattern data. All we have here are 2D coordinates (longitude and latitude) over space. The focus is: Where are the points?
Visualizing 2D spatial point pattern data is as simple as making a scatterplot… because it’s literally just a scatterplot… wearing a fancy geographic trench coat. Strip away the geography, and we’re just plotting dots on a grid (hence the term dot maps or bubble maps). The only difference is that the base layer is a map instead of a blank canvas.
A common goal of point pattern analysis to understand how the density of events varies across space. Thus, we can use basically any 2D density estimation viz for this (e.g., contours, heatmaps, etc.)
Back to the haunted places data, the map I originally wanted to make (for pedagogical purpose) is the following:
(Note: I’m going old-school by making maps with geom_polygon() and changing projections with the superseded coord_map(). I’m fully aware the gold standard these days is geom_sf() and coord_sf(), and I’m going to teach those in my class. But for this post, I’m not gonna go there.)
ggplot() +
geom_polygon(data = map_data("state"),
aes(long, lat, group = group),
fill = "gray75", color = "gray20", alpha = 0.1) +
geom_point(data = haunted_places,
aes(city_longitude, city_latitude),
color = "white", alpha = 0.1, size = 0.3) +
coord_map("albers", lat0 = 39, lat1 = 45, xlim = c(-118, -75)) +
theme_void() +
theme(plot.background = element_rect(fill = "black"),
plot.margin = margin(0, 0, 0, 0))

My first version of this figure did not include state borders, so it looks like this:
haunted_places |>
ggplot(aes(city_longitude, city_latitude)) +
geom_point(color = "white", alpha = 0.1, size = 0.3) +
coord_map("albers", lat0 = 39, lat1 = 45, xlim = c(-118, -75)) +
theme_void() +
theme(plot.background = element_rect(fill = "black"),
plot.margin = margin(0, 0, 0, 0))

I thought this looks cool and wanted to make it more data-artsy. Since this is about haunted places, my goal was to make it more “ghostly”. After a few hours of screwing around with density estimation and contours, I ended up with this:
haunted_places |>
ggplot(aes(city_longitude, city_latitude, alpha = after_stat(level))) +
geom_density_2d_filled(bins = 50, color = NA, show.legend = FALSE) +
scale_fill_manual(values = colorRampPalette(c("#1a1a24", "#4a5568", "#cbd5e1", "white"))(50)) +
scale_alpha_manual(values = seq(0.4, 0.8, length.out = 50)) +
theme_void() +
theme(plot.background = element_rect(fill = "black"),
panel.background = element_rect(fill = "black"))

Here, I chose a foggy monochromatic color gradient (ranging from ghost lavender to full-intensity white) to give it a spooky vibe, with high values (in white) denoting heavily haunted areas. I also messed around with transparency so the contours look more like eerie spirits emerging from the mist.
Kinda dig it.
Bonus: Here’s what it looks like when I changed the map projection to Albers.
haunted_places |>
ggplot(aes(city_longitude, city_latitude, alpha = after_stat(level))) +
geom_density_2d_filled(bins = 50, color = NA, show.legend = FALSE) +
scale_fill_manual(values = colorRampPalette(c("#1a1a24", "#4a5568", "#cbd5e1", "white"))(50)) +
scale_alpha_manual(values = seq(0.4, 0.8, length.out = 50)) +
coord_map("albers", lat0 = 39, lat1 = 45) +
theme_void() +
theme(plot.background = element_rect(fill = "black"),
panel.background = element_rect(fill = "black"))

Looking closely, it really looks like an ultrasound picture. Perhaps this could be called “Baby Ghost”?
Hopefully this will be accepted into the JSM 2026 Data Art Show.
For attribution, please cite this work as
Nguyen (2026, June 6). The Q: Data art: Haunted. Retrieved from https://qntkhvn.netlify.app/posts/2026-06-06-data-art-haunted/
BibTeX citation
@misc{nguyen2026data,
author = {Nguyen, Quang},
title = {The Q: Data art: Haunted},
url = {https://qntkhvn.netlify.app/posts/2026-06-06-data-art-haunted/},
year = {2026}
}