library(tidyverse)
library(plotly)
library(RColorBrewer)
library(here)
library(data.table)
library(highcharter)
library(lubridate)
library(sf)
library(geojsonsf)
library(jsonlite)
library(deltamapr) #devtools::install_github("InteragencyEcologicalProgram/deltamapr")
Plotly vs. Highchart: A Comparison of Interactive Plots
Load necessary libraries
Introduction
If you are creating R Markdown (html) or Shiny apps with your data and want to take advantage of those formats, you should consider using interactive plots to enable end-users to explore your data.
I use the R library Plotly a lot, because I can simply take an exisiting ggplot and convert it to plotly with a single line. But I recently have been exploring other interactive Javascript based libraries, such as Highchart. You can use both Plotly and Highchart with other programming languages such as Python, but we’ll play with R today.
You can read more about the Plotly and Highcharter R libraries here: Plotly & Highcharter. For even more visualization libraries, check out the list here: Shiny extensions.
Download FMWT data
I used to run the FMWT survey at CDFW and before I left that position, made the data more readily accessable in R. We’ll use this dataset for this tutorial.
#url for file download
<- "https://filelib.wildlife.ca.gov/Public/TownetFallMidwaterTrawl/FMWT%20Data/FMWT%201967-2023%20Catch%20Matrix_updated.zip"
url
#file name for the file we want to grab from the zip file
<- "FMWT 1967-2023 Catch Matrix_updated_tidy.csv"
fn
#Now download the catch data. It's an csv file in a zipped folder so first we have to download the zip folder and extract the file
<- tempfile()
temp download.file(url, temp)
<- fread(unzip(temp, files = fn))
FMWT unlink(temp)
#we'll rename some columns and select a few fish species to reduce the dataset a bit
#rename
<- FMWT %>%
FMWT rename(Date = SampleDate, Survey = SurveyNumber, Station = StationCode, Latitude = StationLat, Longitude = StationLong) %>%
#subset just a few fish and variables
select(Year, Date, Survey, Station, Latitude, Longitude, Species, Catch) %>%
#reformat date
mutate(Date = ymd(Date),
Station = as.character(Station),
Longitude = as.numeric(Longitude)) %>%
filter(Species %in% c("American Shad", "Delta Smelt", "Longfin Smelt", "Striped Bass age-0", "Threadfin Shad", "Splittail", "Northern Anchovy"))
#We'll also grab a table of Index values
<- fread("https://filelib.wildlife.ca.gov/Public/TownetFallMidwaterTrawl/FMWT%20Data/FMWTindices.csv")
FMWT.index
#also import a table of abiotic variables
<- fread("https://raw.githubusercontent.com/jamesryanwhite/Rtutorials/refs/heads/main/R%20Interactive%20charts/Sample.csv") %>%
abiotic filter(SurveyNumber %in% 3:6L, #just currently sampled period
== "MWTR") %>% #just fish trawls, not zooplankton
MethodCode mutate(Date = mdy_hms(SampleDate),
Month = case_when(SurveyNumber == 3L ~ "Sept",
== 4L ~ "Oct",
SurveyNumber == 5L ~ "Nov",
SurveyNumber == 6L ~ "Dec"),
SurveyNumber Station = as.character(StationCode),
Salinity = round((0.36966/(((ConductivityTop*0.001)^-1.07)-0.00074))*1.28156,2)) %>% #convert uS/cm to ppt
select(Date, Month, Station, Secchi, Salinity) %>%
arrange(Date, Month, Station)
Scatter/Line Plots
For Plotly, first we create a ggplot then apply the function ggplotly() to our ggplot object to make it interactive.
#make index table into long format
<- FMWT.index %>%
FMWT.index.long pivot_longer(., cols = `Threadfin Shad`:Splittail, names_to = "Species", values_to = "Index") %>%
filter(Year > 1980L) #just to remove data gaps for aesthetic reasons
#plot
<- FMWT.index.long %>%
lplot ggplot( aes(x = Year, y = Index, group = Species, color = Species)) +
geom_line() +
scale_color_brewer(palette = "Dark2") +
theme(legend.position = "none") +
theme_minimal() +
ggtitle("FMWT Fish Index Values")
#apply the plotly function ggplot
ggplotly(lplot)
Now let’s make the same plot with Highcharter. It has a slightly different syntax than ggplot, but nothing too complicated.
#define color palette
<- brewer.pal(7, "Dark2")
cols
hchart(FMWT.index.long, type = "line", hcaes(x = Year, y = Index, group = Species),
marker = list(enabled = FALSE)) %>% #remove line points
hc_title(text = "FMWT Fish Index Values") %>%
hc_colors(cols)
Boxplots
Plotly version:
#create sum of annual fish species catch
<- FMWT %>%
a.catch group_by(Year, Species) %>%
summarize(Annual.Catch = sum(Catch)) %>%
ungroup()
<- a.catch %>%
bplot ggplot(., aes(fill = Species, y = Annual.Catch, x = Species)) +
geom_boxplot(alpha = 0.5) +
geom_jitter(alpha = 0.1, width = 0.15) +
scale_fill_brewer(palette = "Dark2") +
theme_minimal() +
xlab("Year") +
ylab("Annual Catch")
ggplotly(bplot)
Highcharter version:
<- data_to_boxplot(
dat data = a.catch,
variable = Annual.Catch,
Species,group_var = Species,
add_outliers = FALSE,
fillColor = cols,
color = "black")
highchart() %>%
hc_xAxis(type = "category") %>%
hc_add_series_list(dat) %>%
hc_xAxis(title = list(text = "Species"))%>%
hc_yAxis(title = list(text = "Summed Annual Catch"))%>%
hc_legend(enabled = TRUE)
Heatmap
Plotly version:
#create monthly table of Secchi for 2022
<- abiotic %>%
m.abiotic filter(year(Date) == 2022) %>%
mutate(Month = factor(Month, levels = c("Sept", "Oct", "Nov", "Dec"))) # set proper order of x axis
<- m.abiotic %>%
heatmap ggplot(., aes(x = Month, y = Station)) +
geom_tile(aes(fill = Secchi)) +
scale_fill_distiller(palette = "Spectral", direction = -1, na.value = "grey40") +
theme_minimal() +
theme(axis.text.y = element_text(size = 7)) +
xlab("Month") +
ylab("Station") +
labs(fill = "Secchi depth (cm)")
ggplotly(heatmap)
Highcharter version:
<- brewer.pal(10, "Spectral") #define color palette
stops <- rev(stops)
stops
#tooltip
<- JS("function(){
tt return this.series.xAxis.categories[this.point.x] + ' ' + this.series.yAxis.categories[this.point.y] + ': ' +
Highcharts.numberFormat(this.point.value, 2);
}")
hchart(
m.abiotic,type = "heatmap",
hcaes(x = Month, y = Station, value = Secchi)) %>%
hc_colorAxis(
stops = color_stops(10, colors = stops)) %>%
hc_yAxis(
title = list(text = "Station"),
reversed = FALSE,
tickLength = 0,
gridLineWidth = 0,
minorGridLineWidth = 0,
labels = list(style = list(fontSize = "9px"))
%>%
) hc_tooltip(
formatter = tt
%>%
) hc_legend(
layout = "vertical",
verticalAlign = "top",
align = "right",
valueDecimals = 0,
title = list(
text = "Secchi Depth (cm)",
style = list(
textDecoration = "underline")))
Choropleth Map
Plotly version:
#assign stations to regions for geospatial data
#fmwt
<- data.frame("Station" = as.character(c(101:113, 201:211, 301:339, 340:341, 401:408, 409:419, 501:513, 515:519, 601:604, 605:609, 514, 801:802, 803:815, 904:906, 908:912, 902, 915:916, 918, 917, 913:914, 907, 924, 919, 923, 920:922, 925, 901, 701:711, 717, 71:73, 724, 735:736, 70, 712, 713, 715:716, 721, 723, 719, 799, 794:797, 903)),
mapidf "Region" = c(rep("SAN FRANCISCO BAY",24), rep("SAN PABLO BAY",39), rep("NAPA RIVER",2), rep("SUISUN BAY",8), rep("GRIZZLY BAY",33), rep("MONTEZUMA SLOUGH", 5), rep("BROAD SLOUGH",3), rep("SAN JOAQUIN RIVER",21), "SHEEP SLOUGH", rep("OLD RIVER",3), "VICTORIA CANAL", rep("MIDDLE RIVER", 2), "LITTLE CONNECTION SLOUGH", "POTATO SLOUGH", "LITTLE POTATO SLOUGH", "NORTH MOKELUMNE RIVER", rep("SOUTH MOKELUMNE RIVER", 3), "SYCAMORE SLOUGH", "FALSE RIVER", rep("SACRAMENTO RIVER", 18), "GEORGIANA SLOUGH", "STEAMBOAT SLOUGH", rep("CACHE SLOUGH",4), rep("SACTO. R DEEP WATER SH CHAN", 7), "MOKELUMNE RIVER"))
<- filter(mapidf, !is.na(Region))
mapidf
#join with catch and abiotic data, just use 2022
<- FMWT %>%
map.data filter(Year == 2022 & Survey == 6L) %>%
left_join(., mapidf, by = "Station") %>%
left_join(., m.abiotic, by = c("Date", "Station"))
#create a df of average Dec monthly salinity by region to use for choropleth scaling
<- map.data %>%
avg group_by(Month, Region) %>%
summarize(Salinity = round(mean(Salinity), 1)) %>%
ungroup() %>%
filter(Month == "Dec")
#merge data with shapefile
<- deltamapr::WW_Delta %>%
spdf filter(HNAME %in% unique(avg$Region)) %>%
right_join(. , avg, by= c("HNAME" = "Region"))
#read other water bodies w/o stations
<- deltamapr::WW_Delta
other
#plot choropleth
<- ggplot(
map # define main data source
data = spdf
+
) #draw non-sampled water bodies transparent
geom_sf(
data = other,
fill = "transparent",
color = "transparent"
+
) # add main fill aesthetic
# use thin black stroke for region borders
geom_sf(
mapping = aes(
fill = Salinity
),color = "grey40",
size = 0.001
+
) # use the predefined color scale
scale_fill_distiller(palette = "Spectral", direction = -1, na.value = "white") +
# add titles
labs(x = NULL,
y = NULL,
title = "2022 Dec Average Water Salinity") +
theme_minimal()
ggplotly(map)
Highcharter version:
# Ensure correct coordinate reference system (CRS) is WGS84 (EPSG:4326)
<- st_transform(spdf, crs = 4326)
spdf
# Convert sf object to GeoJSON format
<- geojsonsf::sf_geojson(spdf)
geojson_data
# Parse the GeoJSON into a list (Highcharts requires this format)
<- jsonlite::fromJSON(geojson_data, simplifyVector = FALSE)
geojson_list
# Prepare data for Highcharter
<- spdf %>% select(HNAME, Salinity) %>% st_drop_geometry()
region_data
# Create Highcharter choropleth map
<- highchart(type = "map") %>%
map.h #salinity choropleth layer
hc_add_series_map(
map = geojson_list, # Use the parsed GeoJSON
df = region_data,
joinBy = "HNAME",
value = "Salinity",
name = "Salinity (ppt)",
tooltip = list(valueDecimals = 2)) %>%
hc_colorAxis(stops = color_stops(10, colors = stops)) %>%
hc_legend(
layout = "horizontal",
verticalAlign = "bottom",
align = "center",
valueDecimals = 0,
title = list(
text = "Salinity (ppt)",
style = list(
textDecoration = "underline"))) %>%
#title
hc_title(text = "2022 Dec Average Water Salinity") %>%
#make zoomable/moveable
hc_mapNavigation(enabled = TRUE)
map.h