iNaturalist Visualization: What introduced species are in my place?

Since you all were as excited about my last visualization about milkweed bug misidentifications as I was, I thought I’d share one I put together about invasive species.

This one takes a place_id and plots the introduced species as bubbles sized by the number of research-grade observations in that place. The colors correspond to the iconic taxon.

Bubble plots are a fun way to communicate biodiversity data, in my opinion, and can help you compare between sites at a glance.

The code for this visualization is here. It doesn’t handle huge numbers of observations super well, both in terms of speed and visualizations, so I recommend places with <50,000 total verifiable observations.


Hmm . I wonder how the number of observations compares to the amount of organism present. Are the biggest bubbles most common or most fun to photograph?


Very cool, thanks for sharing @alexis18! Might be good to change “invasive” to “introduced”, since that’s the term iNat uses for establishment means.


Thanks for that catch! I’ve updated the code, and the site should update shortly.


There’s almost certainly a bias towards observing the more charismatic taxa. For example Rosa rugosa is definitely not the most abundant nonnative plant in Acadia National Park, but it makes pretty flowers so it grabs folks’ attention.


Yes, and if you plotted them by month, you can see changes in the bias as things start and stop flowering or other activity


Do you mind explaining how to use this? I’m not very computer savvy. I get the impression I need R?


Of course!

So to run this code you need to have R and Rstudio installed (,
If you don’t want to mess with downloading, you can also run code in your browser here:

  1. Copy and paste the code into the scripting pane (upper left)
  2. If this is your first time running the code, take out the first # at the beginning of the first three lines to install the packages
  3. In line ten ( place_id <- 744 ) replace the number (744) with the place_id for the place you’re interested in
  4. In line eleven ( place_name <- “PWC”) replace the text (“PWC”) with the place name corresponding with that place_id
  5. Option 1: press ‘Source’ in the upper right corner of the scripting pane (upper left quarter of the screen). Option 2: is you can use the keyboard shortcut Ctrl+Alt+R on windows or Cmd+Option+R on mac

The plot will then be generated (it can take a while depending on the number of observations in the area you picked). It will save to the current folder (usually your documents folder, you can check where it will save by using the command getwd() in the console - lower left) and you can open it as a picture.


I’ve been trying to work through all the error messages I got, but I really don’t understand them. I think it is not installing the packages in the first three lines, but I don’t know why. I guess this just isn’t for me, which is a shame because it looks cool.

hm. Try copying the code from here, I uncommented the first few lines directly:

install.packages("tidyverse", "devtools", "ggraph", "packcircles")

place_id <- 744
place_name <- "PWC"

iconic_name <- NULL
name <- NULL
common <- NULL

api <- paste("curl -X GET --header 'Accept: application/json' ''", place_id, "&quality_grade=research&per_page=200&order=desc&order_by=created_at'", sep = "")
my_ip <- straighten(api) %>% 
dat <- content(my_ip[[1]](), as="parsed")
for(i in 1:length(dat$results)){
   iconic_name <- c(iconic_name, dat$results[[i]]$taxon$iconic_taxon_name)
   name <- c(name,dat$results[[i]]$taxon$name)
     common <- c(common, NA)}else{
     common <- c(common, dat$results[[i]]$taxon$preferred_common_name)}
if (dat$total_results > 200){
for (i in 2:floor(dat$total_results/200)){
  api <- paste("curl -X GET --header 'Accept: application/json' ''", place_id, "&quality_grade=research&page=", i, "&per_page=200&order=desc&order_by=created_at'", sep = "")
  my_ip <- straighten(api) %>% 
  dat <- content(my_ip[[1]](), as="parsed")
  for(j in 1:length(dat$results)){
    iconic_name <- c(iconic_name, dat$results[[j]]$taxon$iconic_taxon_name)
    name <- c(name,dat$results[[j]]$taxon$name)
      common <- c(common, NA)}else{
      common <- c(common, dat$results[[j]]$taxon$preferred_common_name)}

dat2 <- dat
dat2 <- data.frame(iconic_name,name,common)

dat2 %>% 
  group_by(common, iconic_name) %>%
  summarise(n = n()) -> data
names(data) <- c("group", "iconic", "value")

packing <- circleProgressiveLayout(data$value, sizetype='area')
data <- cbind(data, packing) <- circleLayoutVertices(packing, npoints=50)$group <- rep(data$iconic, each = 51)

# Make the plot
ggplot() + 
  # Make the bubbles
  geom_polygon(data =, aes(x, y, group = id, fill=as.factor(group)), colour = "black", alpha = 0.6) +
  # Add text in the center of each bubble + control its size
  geom_text(data = data, aes(x, y, size=value, label = group)) +
  scale_size_continuous(range = c(1,10), guide = F) +
  # General theme:
  theme_void() + 
  theme() +
  coord_equal() + 
  ggtitle(paste("Introduced Species of", place_name)) +
  scale_fill_brewer(palette="Set3", direction=-1) + 
  theme(plot.title = element_text(size = 40, face = "bold")) + 
  labs(fill = "Iconic Taxon") + 
  theme(legend.text=element_text(size=20), legend.title = element_text(size=40))+
  theme(plot.title = element_text(hjust = 0.5))

ggsave(paste("introduced_species_of_", gsub(" ", "_", place_name), ".png", sep = ""), height = 25, width = 25)
1 Like

Woah! That is a lot! Thanks!

I think that would definitely be a big factor. In my area we are up to our ears in robins, yet multiple ranks above them in observation numbers are great blue herons, which you are generally considered lucky to see. So of course the robins are ignored and the herons are sought out and rigorously photographed.

1 Like

i wonder if this could be made a little more efficient if it hit the observation species count endpoint (ex. instead of pulling in all observations? (i suppose the downside of using the species count endpoint is that it doesn’t provide taxa more granular than species and also can miss taxa less granular than species if there are observations for child taxa. tradeoffs…)


Yep, this is a good point. And the children/subspecies taxa was the thing I was going for most (but there is definitely a speed tradeoff and your proposed method will perform WAY better with large places).


ok. fair enough. not to steal your thunder or idea, but i made a webpage that makes a similar packed circles visualization (using D3.js) using the response from the Observation Species Counts endpoint. this might be useful for folks who are having a hard time running R.


usage examples:

  1. introduced species (research grade observations) at Acadia National Park:
  2. tiwane’s observed leaf taxa for CNC SF Bay Area 2020, grouped by iconic taxon:
  3. alexis18’s observed leaf taxa for 2019, with more observed iconic taxa closer to the center:

Not thunder stealing at all! This is great! @arboretum_amy hopefully this will work for you!


Oh I love it! Thanks @pisum


So far the most visually pleasing one I have made is the plants of Tucson, AZ


people :two_hearts::cactus::cactus::cactus:


Charismatic megaflora ;)

1 Like