seqcombo for visualizing genetic reassortment

Guangchuang Yu
School of Basic Medical Sciences, Southern Medical University

2026-08-28

Reassortment is an important strategy for influenza A viruses to introduce a HA subtype that is new to human populations, which creates the possibilities of pandemic.

A diagram showed above (Figure 2 of doi:10.1038/srep25549) is widely used to illustrate the reassortment events. While such diagrams are mostly manually draw and edit without software tool to automatically generate. Here, I implemented the hybrid_plot function for producing publication quality figure of reassortment events.

Recommended workflow

For most new projects, the recommended entry point is a table-based workflow:

  1. Prepare a long-format virus-segment table.
  2. Build virus_info with build_virus_info_from_long().
  3. Build flow_info with build_flow_info_from_long() when reassortment edges also come from tables.
  4. Wrap both objects with as_seqcombo_data().
  5. Start with autoplot() and only switch to hybrid_plot() when you need fine-grained control.
workflow_segments <- data.frame(
    id = rep(c("avian_1990", "human_1990", "swine_2000"), each = 8),
    sample_time = rep(c(1990, 1990, 2000), each = 8),
    segment = rep(c("PB2", "PB1", "PA", "HA", "NP", "NA", "M", "NS"), 3),
    color = c(
        rep("purple", 8),
        rep("red", 8),
        c("darkgreen", "darkgreen", "red", "darkgreen",
          "darkgreen", "purple", "red", "purple")
    ),
    host = rep(c("Avian", "Human", "Swine"), each = 8),
    stringsAsFactors = FALSE
)

workflow_flow <- data.frame(
    from = c("avian_1990", "human_1990"),
    to = c("swine_2000", "swine_2000"),
    segment = c("HA", "NA"),
    stringsAsFactors = FALSE
)

workflow_virus <- build_virus_info_from_long(
    workflow_segments,
    x = "sample_time",
    keep = "host"
)
workflow_flow <- build_flow_info_from_long(
    workflow_flow,
    segment = "segment"
)
workflow_virus <- layout_timeline(
    workflow_virus,
    workflow_flow,
    time_col = "x"
)

workflow_data <- as_seqcombo_data(workflow_virus, workflow_flow)
autoplot(workflow_data, v_color = ~host, v_fill = ~host, link_style = "curve")

Basic usage of hybrid_plot()

n <- 8

virus_info <- build_virus_info(
    id = 1:7,
    x = c(rep(1990, 4), rep(2000, 2), 2009),
    y = c(1,2,3,5, 1.5, 3, 4),
    segment_color = list(
        rep('purple', n),
        rep('red', n),
        rep('darkgreen', n),
        rep('lightgreen', n),
        c('darkgreen', 'darkgreen', 'red', 'darkgreen', 'red', 'purple', 'red', 'purple'),
        c('darkgreen', 'darkgreen', 'red', 'darkgreen', 'darkgreen', 'purple', 'red', 'purple'),
        c('darkgreen', 'lightgreen', 'lightgreen', 'darkgreen', 'darkgreen', 'purple', 'red', 'purple'))
)

flow_info <- build_flow_info(from = c(1,2,3,3,4,5,6),
                             to = c(5,5,5,6,7,6,7))
hybrid_plot(virus_info, flow_info)

The hybrid_plot requires two tibble data frame of virus information and genetic flow information.

Users need to provide x and y positions to plot the virus, this make sense for geographically and temporally information are usually available in such phylodynamic study and can be employed to set x or y to provide more information and help interpretation of the reassortment events.

Customizing the appearance

We use hexagon to represent virus. Users can set the virus outer boundary color by v_color and fill the virus by v_fill. Color of line segments that indicate the genetic flow relationship can be specify via l_color parameter.

hybrid_plot(virus_info, flow_info, v_color='firebrick', v_fill='darkgreen', l_color='steelblue')

We usually have more information to present, for example host information and HA subtype etc. and these information can be used to color the virus either by v_color or v_fill

virus_info$Host = c("Avian", "Human", rep("Swine", 4), "Human")
hybrid_plot(virus_info, flow_info, v_color=~Host, v_fill=~Host)

The relative virus size can also be specify if a virus_size column is available in the input virus_info data.

virus_info$virus_size <- c(rep(1, 3), 2, 1, 1, 1.5)
hybrid_plot(virus_info, flow_info, v_color=~Host, v_fill=~Host)

If label and label_position coloumns are available, the virus labels (virus name or other information) will be added automatically.

virus_info$label <- c("Avian", "Human\nH3N2", "Classic\nswine\nH1N1", "Eurasian swine",
                      "North American swine\n triple reassrotant H3N2",
                      "North American swine\n triple reassortant H1N2", "2009 Human H1N1")
virus_info$label_position <- c('left', 'left', 'left', 'below', 'below', 'upper', 'below')
hybrid_plot(virus_info, flow_info, v_color=~Host, v_fill=~Host)

User can use asp to set the aspect ratio of hexagons, asp < 1 for thick/short and asp > 1 for thin/tall.

hybrid_plot(virus_info, flow_info, v_color=~Host, v_fill=~Host, asp=2)

The output of hybrid_plot is a ggplot object and users can use ggplot2 to modify the details.

title <- "Reassortment events in evolution of the 2009 influenza A (H1N1) virus"
caption <- build_segment_caption(c("PB2", "PB1", "PA", "HA", "NP", "NA", "M", "NS"))
color <- c(Avian="purple", Human="red", Swine="darkgreen")

hybrid_plot(virus_info, flow_info, v_color=~Host, v_fill=~Host) +
    labs(caption=caption, title=title) +
    scale_color_manual(values=color) + scale_fill_manual(values=color) +
    scale_x_continuous(breaks=c(1990, 2000, 2009)) +
    xlab(NULL) + ylab(NULL) + theme_minimal() +
    theme(axis.line.y = element_blank(),
          axis.text.y = element_blank(),
          axis.ticks.y = element_blank(),
          panel.grid.minor=element_blank(),
          panel.grid.major.y=element_blank(),
          legend.position = c(.95, .1)
          )

When coloring viruses by their host of origin, the manual scales above can be replaced by the built-in host presets from scale_seqcombo_host():

hybrid_plot(virus_info, flow_info, v_color=~Host, v_fill=~Host) +
    scale_seqcombo_host()

Top-down or bottom-up style is also supported.

x <- virus_info$x
virus_info$x <- virus_info$y
virus_info$y <- x
virus_info$label_position <- c(rep("right", 3), "left", "left", "right", "right")
hybrid_plot(virus_info, flow_info, v_color=~Host, v_fill=~Host) +
    scale_y_reverse() + scale_x_continuous(limits=c(0, 5.5))

When your x-axis already encodes time, you can preserve it and only arrange the y positions automatically. Viruses sharing the same time point (here 1990 and 2000) are spread into evenly spaced slots instead of stacking on top of each other:

virus_time <- build_virus_info(
    id = 1:7,
    x = c(rep(1990, 4), rep(2000, 2), 2009),
    segment_color = virus_info$segment_color,
    label = virus_info$label,
    label_position = rep("upper", 7)
)
virus_time <- set_layout(virus_time, flow_info, preserve_x = TRUE, spread = TRUE)
hybrid_plot(virus_time, flow_info, link_style = "curve")

Building inputs from tables

For workflows that start from long-format segment records, you can build virus_info directly from a virus-segment table:

segment_df <- data.frame(
    id = rep(c("avian", "human"), each = 8),
    segment = rep(c("PB2", "PB1", "PA", "HA", "NP", "NA", "M", "NS"), 2),
    color = c(rep("purple", 8), rep("red", 8)),
    x = rep(c(1990, 2009), each = 8),
    y = rep(c(1, 2), each = 8),
    host = rep(c("Avian", "Human"), each = 8)
)

build_virus_info_from_long(
    segment_df,
    x = "x",
    y = "y",
    keep = "host"
)
         id    x y segment_color virus_size segment_name  host
avian avian 1990 1  purple, ....          1 PB2, PB1.... Avian
human human 2009 2  red, red....          1 PB2, PB1.... Human

You can construct flow_info from long-format reassortment records as well:

flow_long <- data.frame(
    from = c(1, 1, 2, 3, 3, 4),
    to = c(5, 5, 5, 6, 7, 7),
    segment = c("PB2", "HA", "NA", "PB1", "NP", "M"),
    support = c(1, 1, 2, 1, 1, 1)
)

build_flow_info_from_long(
    flow_long,
    segment = "segment",
    weight = "support"
)
  from to segment_name weight
1    1  5      PB2, HA      2
2    2  5           NA      2
3    3  6          PB1      1
4    3  7           NP      1
5    4  7            M      1

Plotting time-course reassortment data

In a typical reassortment study you know when each virus was sampled, but you rarely know where it should sit on the canvas. This section walks through the full path from two plain tables (one row per virus-segment record, one row per reassortment flow) to a publication-ready figure with time on the x-axis. If you only want a quick look, example_seqcombo_data("timeline") returns ready-to-plot demo data.

Step 1: prepare long-format tables

The first table stores segment records. Each row is one virus-segment combination:

tl_segments <- data.frame(
    id = rep(c("avian_1990", "human_1990", "swine_2000", "human_2009"), each = 8),
    sample_time = rep(c(1990, 1990, 2000, 2009), each = 8),
    segment = rep(c("PB2", "PB1", "PA", "HA", "NP", "NA", "M", "NS"), 4),
    color = c(
        rep("purple", 8),
        rep("red", 8),
        c("darkgreen", "darkgreen", "red", "darkgreen",
          "darkgreen", "purple", "red", "purple"),
        c("darkgreen", "lightgreen", "lightgreen", "darkgreen",
          "darkgreen", "purple", "red", "purple")
    ),
    host = rep(c("Avian", "Human", "Swine", "Human"), each = 8),
    stringsAsFactors = FALSE
)
head(tl_segments)
          id sample_time segment  color  host
1 avian_1990        1990     PB2 purple Avian
2 avian_1990        1990     PB1 purple Avian
3 avian_1990        1990      PA purple Avian
4 avian_1990        1990      HA purple Avian
5 avian_1990        1990      NP purple Avian
6 avian_1990        1990      NA purple Avian

The second table stores reassortment flows. from and to refer to virus ids; the optional segment column records which segments were donated:

tl_flows <- data.frame(
    from = c("avian_1990", "human_1990", "swine_2000"),
    to = c("swine_2000", "swine_2000", "human_2009"),
    segment = c("HA", "NA", "PB1"),
    stringsAsFactors = FALSE
)

Step 2: build virus_info and flow_info

At this stage there is no y column yet — only sampling time, which we hand to the x argument as a column name:

tl_virus <- build_virus_info_from_long(tl_segments, x = "sample_time",
                                       keep = "host")

tl_flow <- build_flow_info_from_long(tl_flows, segment = "segment")

tl_virus[, c("id", "x")]
                   id    x
avian_1990 avian_1990 1990
human_1990 human_1990 1990
human_2009 human_2009 2009
swine_2000 swine_2000 2000

Step 3: arrange the timeline

layout_timeline() keeps the time axis untouched (time_col = "x") and fills in sensible y positions based on the flow network:

tl_virus <- layout_timeline(tl_virus, tl_flow, time_col = "x")

Step 4: plot

Wrap everything into a seqcombo_data object and call autoplot():

tl_data <- as_seqcombo_data(tl_virus, tl_flow)

autoplot(tl_data, v_color = ~host, v_fill = ~host, link_style = "curve")

That is the complete workflow: two tables in, one figure out.

Polishing the figure

Host colors work better with an explicit palette. The built-in scale_seqcombo_host() wires both colour and fill aesthetics to a curated host palette, and build_segment_caption() turns segment names into a readable caption:

autoplot(tl_data, v_color = ~host, v_fill = ~host, link_style = "curve") +
    scale_seqcombo_host() +
    labs(title = "Reassortment events leading to the 2009 H1N1 pandemic",
         caption = build_segment_caption(c("PB2", "PB1", "PA", "HA",
                                           "NP", "NA", "M", "NS"))) +
    scale_x_continuous(breaks = c(1990, 2000, 2009)) +
    theme_seqcombo_pub()

Labels can be added by attaching label and label_position columns before building the plot:

tl_labeled <- tl_data$virus_info
tl_labeled$label <- c("Avian H1N1", "Human seasonal H3N2",
                      "Triple-reassortant swine H3N2", "2009 pandemic H1N1")
tl_labeled$label_position <- c("left", "left", "below", "below")

tl_data_labeled <- as_seqcombo_data(tl_labeled, tl_data$flow_info)
autoplot(tl_data_labeled, v_color = ~host, v_fill = ~host,
         link_style = "curve") + theme_seqcombo_pub()

For dense timelines, elbow links often read more clearly than curves:

autoplot(tl_data_labeled, v_color = ~host, v_fill = ~host,
         link_style = "elbow", link_elbow_position = 0.4) +
    theme_seqcombo_pub()

If both input tables carry a grouping variable, facet_by splits the history into panels — useful when several lineages co-circulate:

tl_info_facet <- tl_data$virus_info
tl_info_facet$Lineage <- c("North America", "North America",
                           "North America", "North America")
tl_flow_facet <- tl_data$flow_info
tl_flow_facet$Lineage <- tl_info_facet$Lineage[match(tl_flow_facet$to,
                                                     tl_info_facet$id)]

autoplot(as_seqcombo_data(tl_info_facet, tl_flow_facet),
         v_color = ~host, v_fill = ~host,
         link_style = "curve", facet_by = "Lineage") +
    theme_seqcombo_pub()

Notes

Segment labels inside the glyphs

If your virus_info carries segment names, you can show them directly inside the virus glyphs:

segment_table <- do.call(rbind, lapply(seq_len(nrow(virus_info)), function(i) {
    data.frame(
        id = virus_info$id[i],
        segment = c("PB2", "PB1", "PA", "HA", "NP", "NA", "M", "NS"),
        color = virus_info$segment_color[[i]],
        x = virus_info$x[i],
        y = virus_info$y[i],
        stringsAsFactors = FALSE
    )
}))

virus_labeled <- build_virus_info_from_long(
    segment_table,
    x = "x",
    y = "y",
    segment_order = c("PB2", "PB1", "PA", "HA", "NP", "NA", "M", "NS")
)
virus_labeled$virus_size <- virus_info$virus_size

hybrid_plot(
    virus_labeled,
    flow_info,
    show_segment_label = TRUE,
    segment_text_size = 2.5
)

If you want temporal flows to read more like a timeline, elbow links can make the transitions easier to follow:

hybrid_plot(
    virus_time,
    flow_info,
    link_style = "elbow",
    link_elbow_position = 0.4
)

Flow metadata can also drive line appearance. Here we map support to line width and segment count to transparency:

flow_strength <- build_flow_info_from_long(
    flow_long,
    segment = "segment",
    weight = "support"
)
flow_strength$alpha <- c(0.5, 0.7, 0.9, 0.7, 0.8)

hybrid_plot(
    virus_time,
    flow_strength,
    link_style = "elbow",
    l_width = ~weight,
    l_alpha = ~alpha
)
Warning in geom_segment(mapping = mapping, data = rbind(elbow_data$lead, :
Ignoring empty aesthetics: `alpha` and `linewidth`.
Warning in geom_segment(mapping = mapping, data = elbow_data$tail, arrow =
arrow(length = unit(0.3, : Ignoring empty aesthetics: `alpha` and `linewidth`.

The layout_timeline() helper keeps the temporal axis fixed while arranging the other axis automatically:

virus_timeline <- layout_timeline(virus_time, flow_info, time_col = "x")
hybrid_plot(virus_timeline, flow_info, link_style = "curve")

When virus_info carries a grouping variable, you can facet reassortment histories by group. Flows that cross facets are drawn in each panel, so transitions between lineages remain visible:

virus_time$Lineage <- c("North America", "North America", "North America",
                        "Europe", "North America", "North America", "North America")

hybrid_plot(
    virus_time,
    flow_info,
    link_style = "curve",
    facet_by = "Lineage"
)

For a shorter workflow, you can bundle inputs into a seqcombo_data object, validate them, and call autoplot() directly:

demo_data <- example_seqcombo_data("timeline")
check_seqcombo_data(demo_data)

ggplot2::autoplot(
    demo_data,
    v_color = ~Host,
    v_fill = ~Host,
    link_style = "curve"
) +
    theme_seqcombo_pub() +
    ggplot2::labs(
        title = "Reassortment example",
        caption = build_segment_caption(demo_data$virus_info$segment_name)
    )

example_seqcombo_data() also ships dedicated teaching datasets. Type "long" starts from tidy tables (attached as attr(demo, "long_tables")) and walks through the recommended build-and-arrange pipeline, so you can inspect each intermediate step:

long_demo <- example_seqcombo_data("long")
head(attr(long_demo, "long_tables")$flows)
                 from                  to support
1          avian_1996 swine_eurasian_2002    0.62
2          avian_1996   swine_triple_2008    0.84
3 swine_eurasian_2002   swine_triple_2008    0.58
4   swine_triple_2008  pandemic_h1n1_2009    0.91
ggplot2::autoplot(
    long_demo,
    v_color = ~sampling_host,
    v_fill = ~sampling_host,
    l_width = ~weight,
    link_style = "curve"
) +
    scale_seqcombo_host() +
    theme_seqcombo_pub()
Warning in geom_curve(mapping = mapping, data = flow_params$data, arrow =
arrow(length = unit(0.3, : Ignoring empty aesthetic: `linewidth`.

Type "genotype" provides parents plus reassortants without flows, handy for genotype-only charts through geom_genotype() or autoplot(as_seqcombo_data(virus_info)).

Emoji text and automatic layouts

hybrid_plot(virus_info, flow_info, v_color=~Host, v_fill=~Host,
              parse='emoji', t_size=8, t_color='firebrick') +
    scale_y_reverse()
Warning: Removed 7 rows containing missing values or values outside the scale range
(`geom_text()`).

In case you don’t have xy-coordination information, you can use set_layout function to auto setting the xy position using selected layout function.

virus_info <- set_layout(virus_info, flow_info, layout="layout.kamada.kawai")
hybrid_plot(virus_info, flow_info, parse='emoji', t_size=8, t_color='firebrick')
Warning: Removed 7 rows containing missing values or values outside the scale range
(`geom_text()`).

virus_info <- set_layout(virus_info, flow_info, layout="layout.fruchterman.reingold")
hybrid_plot(virus_info, flow_info, parse='emoji', t_size=8, t_color='firebrick')
Warning: Removed 7 rows containing missing values or values outside the scale range
(`geom_text()`).