Skip to content

Commit 8cda6d8

Browse files
v0.4.3: add L0 parameter to growth output, fix catch curve age range display, and code quality improvements
1 parent eded3e5 commit 8cda6d8

86 files changed

Lines changed: 424 additions & 213 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.Rbuildignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@
1616
^LICENSE$
1717
^NEWS\.md$
1818
^vignettes$
19+
^\.positai$
20+
^\.claude$
21+
^CLAUDE\.md$
22+
^memory$

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,9 @@ README.html
4747

4848
# Rstudio project
4949
*.Rproj
50+
.positai
51+
52+
# Claude Code
53+
.claude/
54+
memory/
55+
CLAUDE.md

DESCRIPTION

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
Package: ggFishPlots
22
Type: Package
33
Title: Visualise and Calculate Life History Parameters for Fisheries Science using 'ggplot2'
4-
Version: 0.4.2
5-
Date: 2026-04-22
4+
Version: 0.4.3
5+
Date: 2026-05-12
66
Authors@R: c(person("Mikko", "Vihtakari", email = "mikko.vihtakari@hi.no",
77
role = c("aut", "cre"),
88
comment = c(affiliation = "Institute of Marine Research",
@@ -20,4 +20,4 @@ Imports: dplyr, tibble, tidyr, ggridges (>= 0.5.0), fishmethods, broom,
2020
Suggests: knitr, rmarkdown
2121
License: GPL-3
2222
Encoding: UTF-8
23-
RoxygenNote: 7.3.3
23+
Config/roxygen2/version: 8.0.0

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ importFrom(stats,lm)
2323
importFrom(stats,na.omit)
2424
importFrom(stats,nls)
2525
importFrom(stats,predict)
26+
importFrom(stats,pt)
27+
importFrom(stats,qt)
2628
importFrom(stats,quantile)
2729
importFrom(stats,residuals)
2830
importFrom(stats,rnorm)
2931
importFrom(stats,setNames)
32+
importFrom(stats,vcov)

R/internal_functions.R

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
#' @param accuracy number to round to; for POSIXct objects, a number of seconds
44
#' @param f rounding function: \code{\link{floor}}, \code{\link{ceiling}} or \code{\link{round}}
55
#' @return Rounded numeric vector
6-
#' @keywords internal
76
#' @author Hadley Wickham
87
#' @export
98
#'
@@ -15,7 +14,6 @@ round_any <- function(x, accuracy, f = round) {
1514
#' @param p probability threshold to unlog from \code{model}
1615
#' @param model a \code{glm} model object
1716
#' @return A data frame
18-
#' @keywords internal
1917
#' @importFrom stats confint
2018
#' @export
2119

R/plot_catchcurve.R

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#' @inheritParams plot_growth
55
#' @param time Split analysis by time? If \code{NULL}, all data are assumed to stem from one time point. Using a character argument giving the name of a time column splits the analysis by unique values in that column and produces a faceted plot.
66
#' @param age.range Defines the age range to be used for Z estimation. If \code{NULL}, all ages are used. If a numeric vector of length 2, the first number defines the minimum age to include and the last number the maximum age. It is also possible to use differing ranges by sex when \code{split.by.sex = TRUE}: use a named list of length two with names referring to \code{female.sex} and \code{male.sex}. Provide a numeric vector of length 2 to each element (first number defining the minimum age to include and the last number the maximum age). See Examples.
7-
#' @details Calculates and plots the basic log-linearized catch curve to estimate instantaneous mortality. See e.g. \href{https://www.fishbase.se/manual/english/FishBaseThe_LENGTH_WEIGHT_Table.htm}{Ogle (2013)}.
7+
#' @details Calculates and plots the basic log-linearized catch curve to estimate instantaneous mortality. See e.g. Ogle (2016) \emph{Introductory Fisheries Analyses with R}, Chapter 11.
88
#' @author Mikko Vihtakari // Institute of Marine Research.
99
#' @import dplyr ggplot2
1010
#' @importFrom scales log_breaks
@@ -210,7 +210,7 @@ plot_catchcurve <- function(
210210
dt %>%
211211
dplyr::filter(.data$include, .data$sex == k) %>%
212212
dplyr::group_by(.data$time) %>%
213-
dplyr::reframe(broom::tidy(lm(log(n) ~ age, data = dplyr::cur_data()), conf.int = TRUE)) %>%
213+
dplyr::reframe(broom::tidy(lm(log(n) ~ age, data = dplyr::pick(everything())), conf.int = TRUE)) %>%
214214
dplyr::mutate(sex = k, .before = 1)
215215
}
216216
}) %>%
@@ -346,13 +346,13 @@ plot_catchcurve <- function(
346346
}
347347

348348
Text <- paste0(
349-
"Instantenous total mortality (Z) estimated using a catch curve and\nage range ",
349+
"Instantaneous total mortality (Z) estimated using a catch curve and\nage range ",
350350
ifelse(
351351
inherits(age.range, "list"),
352352
paste0(
353-
paste(age.range[["female"]], collapse = "-"),
353+
paste(age.range[[female.sex]], collapse = "-"),
354354
" for females and ",
355-
paste(age.range[["male"]], collapse = "-"),
355+
paste(age.range[[male.sex]], collapse = "-"),
356356
" for males.\n\n"
357357
),
358358
paste0(paste(age.range, collapse = "-"), " for both sexes.\n\n")
@@ -501,7 +501,7 @@ plot_catchcurve <- function(
501501
}
502502

503503
Text <- paste0(
504-
"Instantenous total mortality (Z) estimated using a catch curve and\nage range ",
504+
"Instantaneous total mortality (Z) estimated using a catch curve and\nage range ",
505505
paste(age.range, collapse = "-"),
506506
".\n\n",
507507
if (!is.null(time)) {

R/plot_growth.R

Lines changed: 139 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,54 @@
1+
# Debug parameters:
2+
# length = "length"; age = "age"; sex = "sex"; female.sex = "F"; male.sex = "M"; length.unit = "cm"; split.by.sex = FALSE; growth.model = 1; force.zero.group.length = NA; force.zero.group.strength = 10; force.zero.group.cv = 0; show.Linf = TRUE; boxplot = TRUE; return_model = FALSE; return_data = FALSE; base_size = 8; legend.position = "bottom"
3+
4+
# Internal helper: delta-method SE/CI for L0 from fitted growth model
5+
compute_L0_row <- function(fit_model, growth_model_internal, n_obs) {
6+
L0 <- try(as.numeric(predict(fit_model, newdata = data.frame(age = 0))), silent = TRUE)
7+
if (inherits(L0, "try-error") || length(L0) == 0 || !is.finite(L0)) {
8+
return(tibble::tibble(
9+
term = "L0", estimate = NA_real_, std.error = NA_real_,
10+
statistic = NA_real_, p.value = NA_real_, conf.low = NA_real_, conf.high = NA_real_
11+
))
12+
}
13+
14+
se_L0 <- NA_real_; stat_L0 <- NA_real_; pval_L0 <- NA_real_
15+
ci_low <- NA_real_; ci_high <- NA_real_
16+
17+
vcov_mat <- try(vcov(fit_model), silent = TRUE)
18+
if (!inherits(vcov_mat, "try-error") && all(is.finite(vcov_mat))) {
19+
pars <- coef(fit_model)
20+
Sinf <- pars[["Sinf"]]; K <- pars[["K"]]; t0 <- pars[["t0"]]
21+
eKt0 <- exp(K * t0)
22+
23+
grad <- if (growth_model_internal == "vout") {
24+
c(1 - eKt0, -Sinf * t0 * eKt0, -Sinf * K * eKt0)
25+
} else if (growth_model_internal == "gout") {
26+
en <- exp(-eKt0)
27+
c(en, -Sinf * t0 * eKt0 * en, -Sinf * K * eKt0 * en)
28+
} else {
29+
d <- 1 + eKt0
30+
c(1 / d, -Sinf * t0 * eKt0 / d^2, -Sinf * K * eKt0 / d^2)
31+
}
32+
33+
var_L0 <- as.numeric(t(grad) %*% vcov_mat %*% grad)
34+
if (is.finite(var_L0) && var_L0 > 0) {
35+
se_L0 <- sqrt(var_L0)
36+
df <- n_obs - length(pars)
37+
stat_L0 <- L0 / se_L0
38+
pval_L0 <- 2 * pt(-abs(stat_L0), df)
39+
ci_low <- L0 - qt(0.975, df) * se_L0
40+
ci_high <- L0 + qt(0.975, df) * se_L0
41+
}
42+
}
43+
44+
tibble::tibble(
45+
term = "L0", estimate = L0, std.error = se_L0,
46+
statistic = stat_L0, p.value = pval_L0, conf.low = ci_low, conf.high = ci_high
47+
)
48+
}
49+
150
#' @title Plot age-length relationships and growth curves
51+
#' @description Fits and plots a von Bertalanffy, Gompertz, or Logistic growth curve to age-length data. Returns the plot, a text summary, and estimated parameters.
252
#' @param length Character argument giving the name of the length column in \code{dt}
353
#' @param age Character argument giving the name of the age column in \code{dt}
454
#' @param growth.model Integer defining the growth model. 1 = von Bertalanffy, 2 = Gompertz, 3 = Logistic.
@@ -10,13 +60,13 @@
1060
#' @param return_model Logical indicating whether the growth models should be returned together with other output.
1161
#' @param return_data Logical indicating whether the data used to make the growth models should be returned together with other output.
1262
#' @param show.Linf Logical indicating whether Linf values should be shown as dashed vertical lines.
13-
#' @param base_size Base size parameter for ggplot. See \link[ggplot2]{ggtheme}.
63+
#' @param base_size Base size parameter for ggplot. See \link[ggplot2]{theme}.
1464
#' @details Uses the \code{fishmethods::growth} function to calculate the growth curves. Zero group length can be forced to the growth functions using the \code{force.zero.group.*} parameters.
15-
#' @return A list containing the \code{plot}, \code{text} for Rmarkdown and Shiny applications, and estimated parameters (\code{params}).
65+
#' @return A list containing the \code{plot}, \code{text} for Rmarkdown and Shiny applications, estimated parameters (\code{params}), the raw \code{model} object (if \code{return_model = TRUE}), and the prepared \code{data} (if \code{return_data = TRUE}).
1666
#' @author Mikko Vihtakari // Institute of Marine Research.
1767
#' @import dplyr ggplot2
1868
#' @importFrom fishmethods growth
19-
#' @importFrom stats rnorm setNames
69+
#' @importFrom stats pt qt rnorm setNames vcov
2070
#' @examples
2171
#' # Simple plot. Note that a list is returned.
2272
#' data(survey_ghl)
@@ -28,9 +78,6 @@
2878
#' plot_growth(survey_ghl, force.zero.group.length = 10, boxplot = FALSE)$plot
2979
#' }
3080
#' @export
31-
32-
# Debug parameters:
33-
# length = "length"; age = "age"; sex = "sex"; female.sex = "F"; male.sex = "M"; length.unit = "cm"; split.by.sex = FALSE; growth.model = 1; force.zero.group.length = NA; force.zero.group.strength = 10; force.zero.group.cv = 0; show.Linf = TRUE; boxplot = TRUE; return_model = FALSE; return_data = FALSE; base_size = 8; legend.position = "bottom"
3481
plot_growth <- function(
3582
dt,
3683
length = "length",
@@ -68,6 +115,7 @@ plot_growth <- function(
68115
# Add row number
69116

70117
dt$id <- rownames(dt)
118+
orig.nrow <- nrow(dt)
71119

72120
# Fix sex column
73121

@@ -89,8 +137,6 @@ plot_growth <- function(
89137
stop("Either invalid sex column or not enough sex data")
90138
}
91139

92-
orig.nrow <- nrow(dt)
93-
94140
dt <- dt %>%
95141
dplyr::rename("sex" = !!sex) %>%
96142
dplyr::filter(!is.na(sex))
@@ -100,10 +146,6 @@ plot_growth <- function(
100146

101147
# Filter
102148

103-
if (!exists("orig.nrow")) {
104-
orig.nrow <- nrow(dt)
105-
}
106-
107149
dt <- dt %>%
108150
dplyr::rename(
109151
"age" = !!age,
@@ -172,6 +214,8 @@ plot_growth <- function(
172214
################
173215
## The Plot ####
174216

217+
laModpars <- NULL
218+
175219
# Plot sexed data
176220

177221
if (split.by.sex) {
@@ -296,7 +340,7 @@ plot_growth <- function(
296340
silent = TRUE
297341
)
298342

299-
if (any(class(model_tidy) == "try-error")) {
343+
if (inherits(model_tidy, "try-error")) {
300344
laModparsF <- dplyr::bind_cols(
301345
sex = female.sex,
302346
broom::tidy(
@@ -309,6 +353,19 @@ plot_growth <- function(
309353
laModparsF <- dplyr::bind_cols(sex = female.sex, model_tidy)
310354
Fmodels <- laModF
311355
}
356+
357+
L0_row_F <- compute_L0_row(
358+
eval(parse(text = paste0("laModF$", growth.model))),
359+
growth.model,
360+
sum(dt$sex == female.sex)
361+
)
362+
if (!"conf.low" %in% names(laModparsF)) {
363+
L0_row_F <- dplyr::select(L0_row_F, -conf.low, -conf.high)
364+
}
365+
laModparsF <- dplyr::bind_rows(
366+
laModparsF,
367+
dplyr::mutate(L0_row_F, sex = female.sex, .before = 1)
368+
)
312369
}
313370

314371
# Males
@@ -342,7 +399,7 @@ plot_growth <- function(
342399
silent = TRUE
343400
)
344401

345-
if (any(class(model_tidy) == "try-error")) {
402+
if (inherits(model_tidy, "try-error")) {
346403
laModparsM <- dplyr::bind_cols(
347404
sex = male.sex,
348405
broom::tidy(
@@ -355,6 +412,19 @@ plot_growth <- function(
355412
laModparsM <- dplyr::bind_cols(sex = male.sex, model_tidy)
356413
Mmodels <- laModM
357414
}
415+
416+
L0_row_M <- compute_L0_row(
417+
eval(parse(text = paste0("laModM$", growth.model))),
418+
growth.model,
419+
sum(dt$sex == male.sex)
420+
)
421+
if (!"conf.low" %in% names(laModparsM)) {
422+
L0_row_M <- dplyr::select(L0_row_M, -conf.low, -conf.high)
423+
}
424+
laModparsM <- dplyr::bind_rows(
425+
laModparsM,
426+
dplyr::mutate(L0_row_M, sex = male.sex, .before = 1)
427+
)
358428
}
359429

360430
laModpars <- dplyr::bind_rows(laModparsF, laModparsM)
@@ -554,6 +624,35 @@ plot_growth <- function(
554624
} else {
555625
paste0("no CIs")
556626
},
627+
" \n L0 (length at age 0) = ",
628+
round(laModparsF$estimate[4], 1),
629+
" ",
630+
length.unit,
631+
" +/- ",
632+
if ("conf.low" %in% names(laModparsF) && !is.na(laModparsF$conf.low[4])) {
633+
paste0(
634+
round(laModparsF$conf.low[4], 1),
635+
" - ",
636+
round(laModparsF$conf.high[4], 1),
637+
" (95% CIs) and "
638+
)
639+
} else {
640+
paste0("no CIs and ")
641+
},
642+
round(laModparsM$estimate[4], 1),
643+
" ",
644+
length.unit,
645+
" +/- ",
646+
if ("conf.low" %in% names(laModparsM) && !is.na(laModparsM$conf.low[4])) {
647+
paste0(
648+
round(laModparsM$conf.low[4], 1),
649+
" - ",
650+
round(laModparsM$conf.high[4], 1),
651+
" (95% CIs)"
652+
)
653+
} else {
654+
paste0("no CIs")
655+
},
557656
" \n tmax (life span; t0 + 3/K) = ",
558657
round(laModparsF$estimate[3] + 3 / laModparsF$estimate[2], 1),
559658
" and ",
@@ -674,6 +773,16 @@ plot_growth <- function(
674773
laModpars <- dplyr::bind_cols(model_tidy)
675774
}
676775

776+
L0_row <- compute_L0_row(
777+
eval(parse(text = paste0("laMod$", growth.model))),
778+
growth.model,
779+
nrow(dt)
780+
)
781+
if (!"conf.low" %in% names(laModpars)) {
782+
L0_row <- dplyr::select(L0_row, -conf.low, -conf.high)
783+
}
784+
laModpars <- dplyr::bind_rows(laModpars, L0_row)
785+
677786
Plot <-
678787
suppressWarnings({
679788
ggplot(data = dt, aes(x = age, y = length)) +
@@ -767,6 +876,21 @@ plot_growth <- function(
767876
} else {
768877
paste0("no CIs")
769878
},
879+
" \n L0 (length at age 0) = ",
880+
round(laModpars$estimate[4], 1),
881+
" ",
882+
length.unit,
883+
" +/- ",
884+
if ("conf.low" %in% names(laModpars) && !is.na(laModpars$conf.low[4])) {
885+
paste0(
886+
round(laModpars$conf.low[4], 1),
887+
" - ",
888+
round(laModpars$conf.high[4], 1),
889+
" (95% CIs)"
890+
)
891+
} else {
892+
paste0("no CIs")
893+
},
770894
" \n tmax (life span; t0 + 3/K) = ",
771895
round(laModpars$estimate[3] + 3 / laModpars$estimate[2], 1),
772896
" years",
@@ -790,11 +914,7 @@ plot_growth <- function(
790914
list(
791915
plot = Plot,
792916
text = Text,
793-
params = if (exists("laModpars")) {
794-
laModpars
795-
} else {
796-
NULL
797-
},
917+
params = laModpars,
798918
model = if (return_model) {
799919
Models
800920
} else {

0 commit comments

Comments
 (0)