2 Y axes problem

OK, here is a first complete pass at drawing the graph, assuming you start with separate tables for depth and temperature, as in this post:

# combine depth and temperature tables
depth |> 
  mutate(measure = 'depth') |> 
  bind_rows(
    temp |> 
      mutate(measure = 'temp')
  ) -> depth_plus_temp

depth_plus_temp

# reformat into longer table
depth_plus_temp |> 
  pivot_longer(min:max, names_to = 'extreme') -> depth_plus_temp_long

depth_plus_temp_long
# normalize depth and temperature data to make them comparable
depth_plus_temp_long |> 
  # perform the following calculations separately for depth and temperature
  group_by(measure) |> 
  mutate(
    # negate depth values
    value = if_else(measure == 'depth', -value, value),
    # create new column with 1 if date is before Oct 9 and NA otherwise
    pre_oct9 = case_when(Date < as.Date('2023-10-09') ~ 1),
    # create new column with value if date is before Oct 9 and NA otherwise
    pre_oct9_values = value * pre_oct9, 
    # create column with max of values that occur before Oct 9
    pre_oct9_max = max(pre_oct9_values, na.rm = T),
    # create column with min of all values
    oct_min = min(value),
    # normalize values relative to min (0) and pre-Oct 9 max (1)
    norm_value = (value - oct_min) / (pre_oct9_max - oct_min)
  ) -> norm_depth_plus_temp_long
  
# extract the min and pre-Oct 9 max for depth and temperature
norm_depth_plus_temp_long |> 
  summarise(across(c(pre_oct9_max, oct_min), unique)) -> min_max_table
  
min_max_table
# create function that converts normalized value of temperature to Celsius
rescale_temp <- 
  function(temp){
    min_max_table |> 
      filter(measure == 'temp') |> 
      # recover unnormalized value
      mutate(rescaled = temp * (pre_oct9_max - oct_min) + oct_min) |> 
      pull(rescaled)
  }

# vectorize rescale_temp so it can be used with ggplot()
rescale_temp <- Vectorize(rescale_temp)
# create function that converts normalized value of depth to original depth
rescale_depth <- 
  function(depth){
    min_max_table |> 
      filter(measure == 'depth') |> 
      # recover unnormalized value
      mutate(rescaled = depth * (pre_oct9_max - oct_min) + oct_min) |> 
      # return depth to positive value
      mutate(rescaled = -rescaled) |> 
      pull(rescaled)
  }

# vectorize rescale_depth so it can be used with ggplot()
rescale_depth <- Vectorize(rescale_depth)
# create labels that combine extreme and measure and order them
norm_depth_plus_temp_long |> 
  ungroup() |> 
  # create column to combine extreme and measure labels
  unite(extreme_measure, c(extreme, measure), sep = ' ', remove = F) |> 
  # set order of curve labels for color legend
  mutate(
    extreme_measure = 
      fct_relevel(
        extreme_measure, 
        c('max temp', 'min temp', 'min depth', 'max depth')          
      )
  ) -> norm_depth_plus_temp_long_w_labels
# plot
norm_depth_plus_temp_long_w_labels |> 
  ggplot() +
  geom_line(aes(Date, norm_value, color = extreme_measure)) +
  scale_y_continuous(
    name = 'Temperature (°C)',
    labels = rescale_temp,
    breaks = seq(0, 1, by = 0.2),
    sec.axis = 
      dup_axis(
        name = "Depth (m)",
        labels = rescale_depth,
        breaks = seq(0, 1, by = 0.2),
        )
  ) +
  scale_color_discrete(name = NULL) +
  theme_minimal()
1 Like

My truly honest advice to you. Stop using ggplot where 2 Y axis are needed and jump over plotly for R.
I spent weeks and weeks and weeks tryting to get the secondary Y axis right - failed. Waste of time. Honeslty.

1 Like

From the looks of it you're working with acoustic or satellite telemetry data, which is awesome! I too work with telemetry data.

Secondly, since you're likely wanting to make static plots ggplot2 will do, you could move over to plotly as suggested, however, you may spend more time trying to learn how to do this in plotly than use ggplot. Plotly is great especially for interactive (html plots) but it isn't always necessary.

Lastly, my biggest suggestion is to two-fold, either make separate plots and stack them on top of themselves (one for temp and one for depth), as you've noticed trying to do second axis in ggplot is frustratingly difficult and for a good reason. Hadley initial set forth to make this either not possible or difficult as second axis make confusing and not always the most informative graphics.

OR

You can do the following which is often what I end up doing...you have 3 variables, time, temp and depth that all can be plotted on a single axis and still be informative. I would take the mean depth and temp for each time bin, from the raw detections file...you haven't posted that, instead of you've posted the min and max for depth and temp for each time bin which is fine. You can achieve this by doing the following which is similar to what @dromano was having you do earlier.

Sharks1 |> 
  group_by(time_bin) |> 
  summarise(
     mean_temp = mean(temp, na.rm = T), 
     mean_depth = mean(depth, na.rm = T)
  ) |> 
  ungroup()

From there you can plot all three with the following:

ggplot(data = dat, aes(x = time_bin, y = mean_temp, 
                       colour = depth)) +
  geom_point()

You can add in the error bars for the SEM for each time bin which is helpful.

Otherwise @dromano later responses also works quite nicely, even though you loose what depth and/or temp the animal is at.

Could you say more? The primary and secondary y-axis labels preserve the depth and temp, so I'm not sure what you mean.

1 Like

Since you might have dual measurements at each time point, look at heatmaps (Figure 4) that Raby et al. 2019 made that may give you a better idea of the habitat/space/temperature the fish is using. You can make the same thing in ggplot, he made them I think somehow in base (no idea how lol) but definitely can make them in ggplot. To see example of modeling/plotting with two measurements see the following plot. In that the daily mean smr and temp are being plotted but the same idea works for temp and depth data similarly to what your data. That plot was at one point part of this paper.

By rescaling each temp and depth you start to loose the meaning of each unit/measurement. If I'm looking at the figure, I don't know what 0.2 is on the temp scale, is that hot is that cold, I don't know and same goes for depth. I like the solution, you just start to loose the meaning of the raw data, which is also why I suggest just plotting them separately.

I can't get your code to work right now with the posted temp and depth dataframes (the end result graph is a flat line with the y axis being NaN), which is clearly something on my end.

The rescale_*() functions are there precisely to return the depth and temp to the original meter and Celsius values, so the units are restored after normalizing to make the measurement visually comparable. In other words, the meaning of the data is not lost.

I can try to add a plot to illustrate, but the code is tailored to the original data used (and described) by @Argyris_Venidis, but which they have not shared. This is also why you will not be able to run the code yourself.

1 Like

Thanks for clarifying both issues...there is a plot earlier where rescale_*() isn't used that in combination with breaks = seq(0, 1, 0.2) in last post gave me the impression that we loose the meaning of each measurement. It's hard to see this when it isn't reproducible/plot not present. I apologize for not fully understanding the provided resolution as it rectifies the OP issue and the issue I raised with loosing the meaning of the measurements.

If one is to model any of this data (e.g., GAMMs), which I strongly suggest, you likely won't want to represent the data on the same graph/duel axis. As much as I like @dromano solution I still am in the notion that single axis graphs for this type of data makes the most sense, especially considering that water temperature and depth use are likely seasonally corelated with one another (e.g., summer - deeper water = cold temperatures).

OK, here is a plot that uses @Argyris_Venidis's most recent data, which is a partial version of an intermediate result from the code I posted. (A full reprex that can be copied all at once is at the bottom of this post.)

data stored in depth_plus_temp_long (click to open)
structure(list(Date = structure(c(1696428000, 1696428000, 1696449600, 
1696449600, 1696471200, 1696471200, 1696492800, 1696492800, 1696514400, 
1696514400, 1696536000, 1696536000, 1696557600, 1696557600, 1696579200, 
1696579200, 1696600800, 1696600800, 1696622400, 1696622400, 1696644000, 
1696644000, 1696665600, 1696665600, 1696687200, 1696687200, 1696708800, 
1696708800, 1696730400, 1696730400, 1696752000, 1696752000, 1696773600, 
1696773600, 1696795200, 1696795200, 1696816800, 1696816800, 1696838400, 
1696838400, 1696860000, 1696860000, 1696881600, 1696881600, 1696903200, 
1696903200, 1696924800, 1696924800, 1696946400, 1696946400, 1696471200, 
1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 
1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 
1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 
1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 
1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 
1696471200, 1696492800, 1696492800, 1696492800, 1696492800, 1696492800, 
1696492800, 1696492800, 1696492800, 1696492800, 1696492800, 1696492800, 
1696492800, 1696492800, 1696492800, 1696492800, 1696492800, 1696492800, 
1696492800), tzone = "", class = c("POSIXct", "POSIXt")), measure = c("depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"temp", "temp", "temp", "temp", "temp", "temp", "temp", "temp", 
"temp", "temp", "temp", "temp", "temp", "temp", "temp", "temp", 
"temp", "temp", "temp", "temp", "temp", "temp", "temp", "temp", 
"temp", "temp", "temp", "temp", "temp", "temp", "temp", "temp", 
"temp", "temp", "temp", "temp", "temp", "temp", "temp", "temp", 
"temp", "temp", "temp", "temp", "temp", "temp", "temp", "temp", 
"temp", "temp"), extreme = c("min", "max", "min", "max", "min", 
"max", "min", "max", "min", "max", "min", "max", "min", "max", 
"min", "max", "min", "max", "min", "max", "min", "max", "min", 
"max", "min", "max", "min", "max", "min", "max", "min", "max", 
"min", "max", "min", "max", "min", "max", "min", "max", "min", 
"max", "min", "max", "min", "max", "min", "max", "min", "max", 
"min", "max", "min", "max", "min", "max", "min", "max", "min", 
"max", "min", "max", "min", "max", "min", "max", "min", "max", 
"min", "max", "min", "max", "min", "max", "min", "max", "min", 
"max", "min", "max", "min", "max", "min", "max", "min", "max", 
"min", "max", "min", "max", "min", "max", "min", "max", "min", 
"max", "min", "max", "min", "max"), value = c(0.75, 5, 4.5, 4.5, 
3, 4, 4, 6, 6, 6, 4, 6, 5, 6, 5, 20, 20.5, 22.5, 20, 21, 16, 
26, 15.5, 23.5, 23, 26, 19, 26, 23.5, 25.5, 18, 27, 24, 26, 17, 
27, 14, 25, 12, 26, 25, 26, 17.5, 26.5, 17, 26, 16, 27, 16.5, 
26.5, 17.9, 18.1, 17.9, 17.9, 17.9, 17.9, 17.9, 17.9, 17.9, 17.9, 
17.7, 18.1, 17.7, 17.9, 17.7, 17.7, 17.7, 17.7, 17.7, 17.7, 18.1, 
18.1, 17.9, 17.9, 18.1, 18.1, 17.9, 17.9, 18.1, 18.1, 17.9, 17.9, 
18.1, 18.3, 18.1, 17.9, 18.1, 18.1, 18.1, 18.1, 18.1, 18.1, 17.7, 
18.3, 17.7, 17.9, 17.7, 17.7, 17.7, 17.7)), class = c("tbl_df", 
"tbl", "data.frame"), row.names = c(NA, -100L)) ->  depth_plus_temp_long
depth_plus_temp_long

library(tidyverse)

# normalize depth and temperature data to make them comparable
depth_plus_temp_long |> 
  # perform the following calculations separately for depth and temperature
  group_by(measure) |> 
  mutate(
    # negate depth values
    value = if_else(measure == 'depth', -value, value),
    # create new column with 1 if date is before Oct 9 and NA otherwise
    pre_oct9 = case_when(Date < as.Date('2023-10-09') ~ 1),
    # create new column with value if date is before Oct 9 and NA otherwise
    pre_oct9_values = value * pre_oct9, 
    # create column with max of values that occur before Oct 9
    pre_oct9_max = max(pre_oct9_values, na.rm = T),
    # create column with min of all values
    oct_min = min(value),
    # normalize values relative to min (0) and pre-Oct 9 max (1)
    norm_value = (value - oct_min) / (pre_oct9_max - oct_min)
  ) -> norm_depth_plus_temp_long
# extract the min and pre-Oct 9 max for depth and temperature
norm_depth_plus_temp_long |> 
  summarise(across(c(pre_oct9_max, oct_min), unique)) -> min_max_table
  
min_max_table
#> # A tibble: 2 × 3
#>   measure pre_oct9_max oct_min
#>   <chr>          <dbl>   <dbl>
#> 1 depth          -0.75   -27  
#> 2 temp           18.3     17.7
# create function that converts normalized value of temperature to Celsius
rescale_temp <- 
  function(temp){
    min_max_table |> 
      filter(measure == 'temp') |> 
      mutate(rescaled = temp * (pre_oct9_max - oct_min) + oct_min) |> 
      pull(rescaled)
  }

# vectorize rescale_temp so it can be used with ggplot()
rescale_temp <- Vectorize(rescale_temp)
# create function that converts normalized value of depth to original depth
rescale_depth <- 
  function(depth){
    min_max_table |> 
      filter(measure == 'depth') |> 
      mutate(rescaled = depth * (pre_oct9_max - oct_min) + oct_min) |> 
      mutate(rescaled = -rescaled) |> 
      pull(rescaled)
  }

# vectorize  rescale_depth so it can be used with ggplot()
rescale_depth <- Vectorize(rescale_depth)
# create labels that combine extreme and measure and order them
norm_depth_plus_temp_long |> 
ungroup() |> 
  # create column to combine extreme and measure labels
  unite(extreme_measure, c(extreme, measure), sep = ' ', remove = F) |> 
  # set order of curve labels for color legend
  mutate(
    extreme_measure = 
      fct_relevel(
        extreme_measure, 
        c('max temp', 'min temp', 'min depth', 'max depth')          
      )
  ) -> norm_depth_plus_temp_long_w_labels
# plot
norm_depth_plus_temp_long_w_labels |> 
ggplot() +
  geom_line(aes(Date, norm_value, color = extreme_measure)) +
  scale_y_continuous(
    name = 'Temperature (°C)',
    labels = rescale_temp,
    breaks = seq(0, 1, by = 0.2),
    sec.axis = 
      dup_axis(
        name = "Depth (m)",
        labels = rescale_depth,
        breaks = seq(0, 1, by = 0.2),
        )
  ) +
  scale_color_discrete(name = NULL) +
  theme_minimal()

Created on 2024-04-02 with reprex v2.0.2

full reprex (click to open)
structure(list(Date = structure(c(1696428000, 1696428000, 1696449600, 
1696449600, 1696471200, 1696471200, 1696492800, 1696492800, 1696514400, 
1696514400, 1696536000, 1696536000, 1696557600, 1696557600, 1696579200, 
1696579200, 1696600800, 1696600800, 1696622400, 1696622400, 1696644000, 
1696644000, 1696665600, 1696665600, 1696687200, 1696687200, 1696708800, 
1696708800, 1696730400, 1696730400, 1696752000, 1696752000, 1696773600, 
1696773600, 1696795200, 1696795200, 1696816800, 1696816800, 1696838400, 
1696838400, 1696860000, 1696860000, 1696881600, 1696881600, 1696903200, 
1696903200, 1696924800, 1696924800, 1696946400, 1696946400, 1696471200, 
1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 
1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 
1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 
1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 
1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 1696471200, 
1696471200, 1696492800, 1696492800, 1696492800, 1696492800, 1696492800, 
1696492800, 1696492800, 1696492800, 1696492800, 1696492800, 1696492800, 
1696492800, 1696492800, 1696492800, 1696492800, 1696492800, 1696492800, 
1696492800), tzone = "", class = c("POSIXct", "POSIXt")), measure = c("depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"depth", "depth", "depth", "depth", "depth", "depth", "depth", 
"temp", "temp", "temp", "temp", "temp", "temp", "temp", "temp", 
"temp", "temp", "temp", "temp", "temp", "temp", "temp", "temp", 
"temp", "temp", "temp", "temp", "temp", "temp", "temp", "temp", 
"temp", "temp", "temp", "temp", "temp", "temp", "temp", "temp", 
"temp", "temp", "temp", "temp", "temp", "temp", "temp", "temp", 
"temp", "temp", "temp", "temp", "temp", "temp", "temp", "temp", 
"temp", "temp"), extreme = c("min", "max", "min", "max", "min", 
"max", "min", "max", "min", "max", "min", "max", "min", "max", 
"min", "max", "min", "max", "min", "max", "min", "max", "min", 
"max", "min", "max", "min", "max", "min", "max", "min", "max", 
"min", "max", "min", "max", "min", "max", "min", "max", "min", 
"max", "min", "max", "min", "max", "min", "max", "min", "max", 
"min", "max", "min", "max", "min", "max", "min", "max", "min", 
"max", "min", "max", "min", "max", "min", "max", "min", "max", 
"min", "max", "min", "max", "min", "max", "min", "max", "min", 
"max", "min", "max", "min", "max", "min", "max", "min", "max", 
"min", "max", "min", "max", "min", "max", "min", "max", "min", 
"max", "min", "max", "min", "max"), value = c(0.75, 5, 4.5, 4.5, 
3, 4, 4, 6, 6, 6, 4, 6, 5, 6, 5, 20, 20.5, 22.5, 20, 21, 16, 
26, 15.5, 23.5, 23, 26, 19, 26, 23.5, 25.5, 18, 27, 24, 26, 17, 
27, 14, 25, 12, 26, 25, 26, 17.5, 26.5, 17, 26, 16, 27, 16.5, 
26.5, 17.9, 18.1, 17.9, 17.9, 17.9, 17.9, 17.9, 17.9, 17.9, 17.9, 
17.7, 18.1, 17.7, 17.9, 17.7, 17.7, 17.7, 17.7, 17.7, 17.7, 18.1, 
18.1, 17.9, 17.9, 18.1, 18.1, 17.9, 17.9, 18.1, 18.1, 17.9, 17.9, 
18.1, 18.3, 18.1, 17.9, 18.1, 18.1, 18.1, 18.1, 18.1, 18.1, 17.7, 
18.3, 17.7, 17.9, 17.7, 17.7, 17.7, 17.7)), class = c("tbl_df", 
"tbl", "data.frame"), row.names = c(NA, -100L)) ->  depth_plus_temp_long

library(tidyverse)

# normalize depth and temperature data to make them comparable
depth_plus_temp_long |> 
  # perform the following calculations separately for depth and temperature
  group_by(measure) |> 
  mutate(
    # negate depth values
    value = if_else(measure == 'depth', -value, value),
    # create new column with 1 if date is before Oct 9 and NA otherwise
    pre_oct9 = case_when(Date < as.Date('2023-10-09') ~ 1),
    # create new column with value if date is before Oct 9 and NA otherwise
    pre_oct9_values = value * pre_oct9, 
    # create column with max of values that occur before Oct 9
    pre_oct9_max = max(pre_oct9_values, na.rm = T),
    # create column with min of all values
    oct_min = min(value),
    # normalize values relative to min (0) and pre-Oct 9 max (1)
    norm_value = (value - oct_min) / (pre_oct9_max - oct_min)
  ) -> norm_depth_plus_temp_long
  
# extract the min and pre-Oct 9 max for depth and temperature
norm_depth_plus_temp_long |> 
  summarise(across(c(pre_oct9_max, oct_min), unique)) -> min_max_table
  
min_max_table
#> # A tibble: 2 × 3
#>   measure pre_oct9_max oct_min
#>   <chr>          <dbl>   <dbl>
#> 1 depth          -0.75   -27  
#> 2 temp           18.3     17.7

# create function that converts normalized value of temperature to Celsius
rescale_temp <- 
  function(temp){
    min_max_table |> 
      filter(measure == 'temp') |> 
      mutate(rescaled = temp * (pre_oct9_max - oct_min) + oct_min) |> 
      pull(rescaled)
  }

# vectorize rescale_temp so it can be used with ggplot()
rescale_temp <- Vectorize(rescale_temp)

# create function that converts normalized value of depth to original depth
rescale_depth <- 
  function(depth){
    min_max_table |> 
      filter(measure == 'depth') |> 
      mutate(rescaled = depth * (pre_oct9_max - oct_min) + oct_min) |> 
      mutate(rescaled = -rescaled) |> 
      pull(rescaled)
  }

# vectorize  rescale_depth so it can be used with ggplot()
rescale_depth <- Vectorize(rescale_depth)

# create labels that combine extreme and measure and order them
norm_depth_plus_temp_long |> 
ungroup() |> 
  # create column to combine extreme and measure labels
  unite(extreme_measure, c(extreme, measure), sep = ' ', remove = F) |> 
  # set order of curve labels for color legend
  mutate(
    extreme_measure = 
      fct_relevel(
        extreme_measure, 
        c('max temp', 'min temp', 'min depth', 'max depth')          
      )
  ) -> norm_depth_plus_temp_long_w_labels

# plot
norm_depth_plus_temp_long_w_labels |> 
ggplot() +
  geom_line(aes(Date, norm_value, color = extreme_measure)) +
  scale_y_continuous(
    name = 'Temperature (°C)',
    labels = rescale_temp,
    breaks = seq(0, 1, by = 0.2),
    sec.axis = 
      dup_axis(
        name = "Depth (m)",
        labels = rescale_depth,
        breaks = seq(0, 1, by = 0.2),
        )
  ) +
  scale_color_discrete(name = NULL) +
  theme_minimal()

Created on 2024-04-02 with reprex v2.0.2

1 Like

Thanks for the reprex as that clarifies things for me.

1 Like