Skip to contents

This vignette demonstrates how to capture and visualize NetLogo simulations at specific time steps using logolink and ggplot2. You’ll learn how to extract agent positions, render them as publication-ready figures, and create animations showing simulation dynamics over time.

We’ll work with the Wolf Sheep Simple model, a classic predator-prey simulation that ships with NetLogo. By the end, you’ll have both static plots and an animated GIF showing your simulation evolving over time.

We’re assuming you have NetLogo 7.0.1 or above installed and are comfortable with R programming.

Getting Started

First, let’s load the packages we’ll need:

If any of these are missing, install them with:

install.packages(
  c(
    "cli",
    "curl",
    "dplyr",
    "ggplot2",
    "ggimage",
    "ggtext",
    "magick",
    "magrittr",
    "ragg",
    "remotes",
    "stringr",
    "tidyr"
  )
)

And then run:

remotes::install_github("danielvartan/logolink")

Now let’s find our model. The find_netlogo_home() function locates your NetLogo installation, and from there we can navigate to the model file:

model_path <-
  find_netlogo_home() |>
  file.path(
    "models",
    "IABM Textbook",
    "chapter 4",
    "Wolf Sheep Simple 5.nlogox"
  )

We’ll also need the turtle shapes to make our plots look nice. The get_netlogo_shape() function grabs SVG image files from the LogoShapes project:

sheep_shape <- get_netlogo_shape("sheep")
wolf_shape <- get_netlogo_shape("wolf")

Running the Simulation

Here’s where things get interesting. We want to capture the position of every sheep, wolf, and patch at regular intervals. Let’s set up an experiment that takes snapshots every 100 ticks:

setup_file <- create_experiment(
  name = "Wolf Sheep Simple Model Analysis",
  repetitions = 1,
  sequential_run_order = TRUE,
  run_metrics_every_step = FALSE,
  setup = "setup",
  go = "go",
  time_limit = 500,
  run_metrics_condition = 'ticks mod 100 = 0',
  metrics = c(
    '[xcor] of sheep',
    '[ycor] of sheep',
    '[xcor] of wolves',
    '[ycor] of wolves',
    '[pxcor] of patches',
    '[pycor] of patches',
    '[pcolor] of patches'
  ),
  constants = list(
    "number-of-sheep" = 100,
    "number-of-wolves" = 15,
    "movement-cost" = 0.5,
    "grass-regrowth-rate" = 0.3,
    "energy-gain-from-grass" = 2,
    "energy-gain-from-sheep" = 5
  )
)

The run_metrics_condition = 'ticks mod 100 = 0' is the key here. It tells NetLogo to only record data when the tick count is divisible by 100. With time_limit = 500, we get 6 snapshots: steps 0, 100, 200, 300, 400, and 500.

Now let’s run it using run_experiment():

results <-
  model_path |>
  run_experiment(
    setup_file = setup_file,
    output = c("table", "lists")
  )

The results come back as a list. The lists element has all our agent data:

results |>
  extract2("lists") |>
  glimpse()
#> Rows: 7,350
#> Columns: 16
#> $ run_number             <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
#> $ number_of_sheep        <dbl> 100, 100, 100, 100, 100, 100, 100, 100, 100,…
#> $ number_of_wolves       <dbl> 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,…
#> $ movement_cost          <dbl> 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,…
#> $ grass_regrowth_rate    <dbl> 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3,…
#> $ energy_gain_from_grass <dbl> 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,…
#> $ energy_gain_from_sheep <dbl> 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,…
#> $ step                   <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
#> $ index                  <chr> "0", "1", "2", "3", "4", "5", "6", "7", "8",…
#> $ pcolor_of_patches      <dbl> 56.35257, 55.37902, 55.10799, 56.12433,…
#> $ pxcor_of_patches       <dbl> -12, 12, 7, -16, 6, -11, 11, 8, 5, -7, -13,…
#> $ pycor_of_patches       <dbl> -6, 11, 10, 15, -14, 13, 13, 6, -9, 8, -5,…
#> $ xcor_of_sheep          <dbl> -12.6365795, 16.9131184, -8.5579481,…
#> $ xcor_of_wolves         <dbl> -17.368182, -11.239707, -13.813702, …
#> $ ycor_of_sheep          <dbl> 17.44700318, 14.04946398, 12.60102781, …
#> $ ycor_of_wolves         <dbl> 16.403987, -15.657477, -1.630277,…

Preparing the Data

NetLogo uses its own color coding system, so we need to convert those values to hex colors that ggplot2 understands:

plot_data <-
  results |>
  extract2("lists") |>
  mutate(
    across(
      .cols = matches("^pcolor_of_patches|^color_of_"),
      .fns = parse_netlogo_color
    )
  )

Building the Plot

Let’s create a function that renders the world at any given step. It draws patches as a raster background, then overlays sheep and wolf icons at their coordinates:

plot_netlogo_world <- function(
  data,
  run_number = 1,
  step = 0,
  step_label = TRUE
) {
  data <-
    data |>
    filter(
      run_number == .env$run_number,
      step == .env$step
    )

  plot <-
    data |>
    ggplot(
      aes(
        x = pxcor_of_patches,
        y = pycor_of_patches,
        fill = pcolor_of_patches
      )
    ) +
    geom_raster() +
    coord_fixed(expand = FALSE) +
    geom_image(
      data = data |> drop_na(xcor_of_sheep),
      mapping = aes(
        x = xcor_of_sheep,
        y = ycor_of_sheep,
        image = sheep_shape
      ),
      size = 0.04
    ) +
    geom_image(
      data = data |> drop_na(xcor_of_wolves),
      mapping = aes(
        x = xcor_of_wolves,
        y = ycor_of_wolves,
        image = wolf_shape
      ),
      size = 0.055,
      color = parse_netlogo_color(31)
    ) +
    scale_fill_identity(na.value = parse_netlogo_color(7.5)) +
    theme_void() +
    theme(legend.position = "none")

  if (isTRUE(step_label)) {
    plot +
      labs(title = paste0("Step: **", step, "**")) +
      theme(
        plot.title.position = "plot",
        plot.title = element_markdown(size = 20, margin = margin(b = 10)),
        plot.background = element_rect(fill = "white", color = NA),
        plot.margin = margin(1.5, 1.5, 1.5, 1.5, "line")
      )
  } else {
    plot
  }
}

Let’s see what the initial state looks like:

plot_netlogo_world(plot_data)

Creating an Animation

Static plots are nice, but an animation really brings the simulation to life. We’ll use the magick package to stitch our snapshots together.

First, let’s see what steps we have:

steps <-
  plot_data |>
  pull(step) |>
  unique()
steps
#> [1]   0 100 200 300 400 500

Now we’ll generate a PNG image file for each step:

files <- character()

cli_progress_bar("Generating frames", total = length(steps))

for (i in steps) {
  i_plot <- plot_netlogo_world(plot_data, step = i, step_label = TRUE)

  i_file <- tempfile(pattern = paste0("step-", i, "-"), fileext = ".png")

  ggsave(
    filename = i_file,
    plot = i_plot,
    device = agg_png,
    width = 7,
    height = 7.4,
    units = "in",
    dpi = 96
  )

  files <- append(files, i_file)
  cli_progress_update()
}

cli_progress_done()

Finally, let’s combine them into a GIF:

animation <-
  files |>
  lapply(image_read) |>
  image_join() |>
  image_animate(fps = 1)

To save it:

animation |> image_write("netlogo-world-animation.gif")

And here’s the result:

animation

Wrapping up

You now have the tools to visualize any NetLogo simulation. The approach is straightforward: extract agent coordinates at the time steps you care about, convert NetLogo colors to hex, and plot with ggplot2.

Feel free to adapt this for your own models, just change the metrics to capture whatever agent properties you need. One caveat: animations can get memory-intensive if you’re capturing many steps or have lots of agents, so start small and scale up as needed.