Visualizing Longitudinal Trajectories with geom_kodom_line

geom_kodom_line() draws one horizontal lane per subject, with time on the x-axis and value encoded as a color gradient along each path. It is a direct ggplot2 layer, so it composes freely with facet_wrap(), scale_*(), theme(), and any other ggplot2 building block.

Sample data

All examples use a simulated HbA1c dataset: 25 patients each with 6–12 irregular visits over 24 months. Each patient also has a fixed age covariate used later to demonstrate independent aesthetic mappings.

set.seed(42)
n_subjects <- 25
n_obs_per <- sample(6:12, n_subjects, replace = TRUE)

df <- do.call(rbind, lapply(seq_len(n_subjects), function(i) {
  n <- n_obs_per[i]
  base <- rnorm(1, mean = 7.5, sd = 1.2)
  trend <- rnorm(1, mean = -0.02, sd = 0.01)
  time <- sort(runif(n, 0, 24))
  value <- base + trend * time + rnorm(n, sd = 0.4)
  data.frame(
    subject_id = sprintf("P%03d", i),
    visit_month = time,
    relative_month = time - min(time),
    hba1c = pmax(4, value),
    arm = ifelse(i <= 12, "Treatment", "Control"),
    age = sample(30:75, 1),
    stringsAsFactors = FALSE
  )
}))

1. Basic usage

Map x to time, id to the subject identifier, and colour to the measurement value. The stat assigns each unique id a lane position on the y-axis automatically.

ggplot(df, aes(x = visit_month, id = subject_id, colour = hba1c)) +
  geom_kodom_line() +
  scale_colour_kodom() +
  labs(x = "Visit (months)", y = "", colour = "HbA1c (%)") +
  theme_kodom()

The color along each path is a smooth gradient interpolated between consecutive measurement points, not a step function. Teal = lower HbA1c, red = higher.


2. Lane ordering with sort_by

By default lanes follow first-appearance order of subject IDs. Use sort_by to re-order by a summary of the colour variable.

# Top lanes = highest mean HbA1c, bottom = lowest
ggplot(df, aes(x = visit_month, id = subject_id, colour = hba1c)) +
  geom_kodom_line(sort_by = "mean") +
  scale_colour_kodom() +
  labs(x = "Visit (months)", y = "", title = 'sort_by = "mean"') +
  theme_kodom(legend_position = "top")

Available sort_by values:

Value Lane order
"none" First appearance in data (default)
"mean" Descending mean of colour
"mean_asc" Ascending mean of colour
"first" Descending value at earliest timepoint
"last" Descending value at latest timepoint

Often visits are counted by relative differences with the initial visit for every patients marked at time \(0\). This can be achieved by simple transformation of the data.

ggplot(df, aes(x = relative_month, id = subject_id, colour = hba1c)) +
  geom_kodom_line(sort_by = "first") +
  scale_colour_kodom() +
  labs(x = "Visit (months)", y = "", title = 'sort_by = "mean"') +
  theme_kodom(legend_position = "top")


3. Limiting subjects with n_max

For large cohorts, n_max takes a random sample of subjects to keep the plot readable. Here we show only 12 of the 25 patients.

ggplot(df, aes(x = visit_month, id = subject_id, colour = hba1c)) +
  geom_kodom_line(sort_by = "mean", n_max = 12) +
  scale_colour_kodom() +
  labs(x = "Visit (months)", y = "", title = "12 randomly sampled subjects") +
  theme_kodom()


4. Controlling points

show_points = TRUE (the default) places a dot at every observation. This makes irregular visit times visible at a glance — critical for sparse clinical data.

p_base <- ggplot(df, aes(x = visit_month, id = subject_id, colour = hba1c)) +
  scale_colour_kodom() +
  labs(x = "Visit (months)", y = "") +
  theme_kodom()

p_base + geom_kodom_line(show_points = TRUE) +
  labs(title = "show_points = TRUE (default)")

p_base + geom_kodom_line(show_points = FALSE) +
  labs(title = "show_points = FALSE")

You can also suppress points by passing shape = NA or size = 0 as fixed aesthetics — useful when you want to keep the parameter API consistent across geom calls:

p_base + geom_kodom_line(shape = NA) +
  labs(title = "shape = NA — same effect as show_points = FALSE")


5. Customising point appearance

shape, size, stroke, and fill control the observation markers. These follow standard ggplot2 conventions.

# shape 21 = filled circle — fill controls interior, colour controls border
ggplot(
  df,
  aes(x = visit_month, id = subject_id, colour = hba1c, fill = hba1c)
) +
  geom_kodom_line(shape = 21, size = 2.5, stroke = 0.6) +
  scale_colour_kodom() +
  scale_fill_kodom() +
  labs(x = "Visit (months)", y = "", title = "shape = 21 with fill + colour") +
  theme_kodom()

# shape 23 = filled diamond
ggplot(
  df,
  aes(x = visit_month, id = subject_id, colour = hba1c, fill = hba1c)
) +
  geom_kodom_line(shape = 23, size = 2.5) +
  scale_colour_kodom() +
  scale_fill_kodom() +
  labs(x = "Visit (months)", y = "", title = "shape = 23 (diamond)") +
  theme_kodom()


6. Controlling line appearance

linewidth sets the path width. alpha controls transparency for both line and points.

ggplot(df, aes(x = visit_month, id = subject_id, colour = hba1c)) +
  geom_kodom_line(linewidth = 3, alpha = 0.5) +
  scale_colour_kodom() +
  labs(x = "Visit (months)", y = "", title = "linewidth = 3, alpha = 0.5") +
  theme_kodom()


7. Discrete color bands with scale_colour_kodom(discretize = TRUE)

Continuous gradients can wash out when values cluster tightly. discretize = TRUE switches to solid color bands, and color_breaks sets the clinical thresholds.

ggplot(df, aes(x = visit_month, id = subject_id, colour = hba1c)) +
  geom_kodom_line(sort_by = "mean") +
  scale_colour_kodom(
    discretize   = TRUE,
    color_breaks = c(5.7, 6.5, 8),
    name         = "HbA1c (%)"
  ) +
  labs(x = "Visit (months)", y = "", title = "Discrete clinical bands") +
  theme_kodom()


8. Independent size and linewidth — encoding patient covariates

size controls only the observation markers; linewidth controls only the connecting path. They are independently routed to GeomPoint and GeomSegment respectively, so mapping one has no effect on the other.

Use this to encode a fixed patient-level covariate (here, age) as point size while keeping the path width constant:

ggplot(df, aes(
  x = visit_month, id = subject_id,
  colour = hba1c, size = age
)) +
  geom_kodom_line(sort_by = "mean", linewidth = 0.4) +
  scale_colour_kodom() +
  scale_size_continuous(range = c(1, 4), name = "Age") +
  labs(
    x = "Visit (months)", y = "",
    title = "Point size = patient age, path width fixed"
  ) +
  theme_kodom()

alpha works the same way — here it encodes age as opacity, making older patients’ trajectories more prominent:

ggplot(df, aes(
  x = visit_month, id = subject_id,
  colour = hba1c, alpha = age
)) +
  geom_kodom_line(sort_by = "mean") +
  scale_colour_kodom() +
  scale_alpha_continuous(range = c(0.2, 1), name = "Age") +
  labs(
    x = "Visit (months)", y = "",
    title = "Opacity = patient age"
  ) +
  theme_kodom()

Both can be combined — size and alpha each independently driven by a covariate:

ggplot(df, aes(
  x = visit_month, id = subject_id,
  colour = hba1c, size = age, alpha = age
)) +
  geom_kodom_line(sort_by = "mean", linewidth = 0.3) +
  scale_colour_kodom() +
  scale_size_continuous(range = c(1, 4), name = "Age") +
  scale_alpha_continuous(range = c(0.25, 1), guide = "none") +
  labs(
    x = "Visit (months)", y = "",
    title = "Size + opacity both encode age"
  ) +
  theme_kodom()


10. Faceting by a grouping variable

Because geom_kodom_line() is a standard ggplot2 layer, facet_wrap() and facet_grid() work out of the box. Lane ordering is computed independently within each facet panel.

ggplot(df, aes(x = visit_month, id = subject_id, colour = hba1c)) +
  geom_kodom_line(sort_by = "mean") +
  scale_colour_kodom(color_breaks = c(5.7, 6.5, 8)) +
  facet_wrap(~arm) +
  labs(x = "Visit (months)", y = "", title = "Treatment vs. Control") +
  theme_kodom()


11. Y-axis subject labels

The y-axis shows integer lane numbers by default. Use scale_y_continuous() to replace them with subject IDs, or theme() to suppress them entirely.

# Suppress y labels for large cohorts — usually the right choice
ggplot(df, aes(x = visit_month, id = subject_id, colour = hba1c)) +
  geom_kodom_line(sort_by = "mean") +
  scale_colour_kodom() +
  labs(x = "Visit (months)", y = "") +
  theme_kodom() +
  theme(axis.text.y = element_blank())


12. Full palette reference

# View the three anchor colors and any interpolated expansion
scales::show_col(kodom_colors(7))

kodom_colors() returns the three anchors (teal #008D98, gold #FFCC3D, red #D7433B). Passing n > 3 interpolates between them via colorRampPalette.