Skip to contents

This vignette watches a food web assemble one geom_odum_* layer at a time. Same data, six versions of the plot — each step adds one symbol. Copy the final block, tweak the data, and you have your own diagram.

library(ggplot2)
library(energese)

nodes <- data.frame(
  x    = c(1, 3.5, 6, 8.5),
  y    = 2,
  role = c("Sun", "Grass", "Grazer", "Predator"))

ink <- "#1d2a1d"

1. An empty canvas

Start with ggplot() on the node data, coord_fixed() for equal-scale drawing, and theme_void() for a clean background.

base <- ggplot(nodes, aes(x, y)) +
  coord_fixed(xlim = c(0, 10), ylim = c(0, 4)) +
  theme_void()
base

2. Add the sun (source)

geom_odum_source() — a filled circle. radius controls the size.

p2 <- base +
  geom_odum_source(data = nodes[1, ], radius = 0.35, fill = "#F1C40F")
p2

3. Add the primary producer (grass)

geom_odum_producer() — the Odum “bullet” shape. Autocatalytic units that capture and concentrate emergy.

p3 <- p2 +
  geom_odum_producer(data = nodes[2, ], width = 1.4, height = 1.0,
                      fill = "#2E7D32")
p3

4. Draw the sun→grass flow

Arrows between symbols are just annotate("segment", ...) calls with an arrow() argument — no special geom needed.

p4 <- p3 +
  annotate("segment", x = 1.4, xend = 2.8, y = 2, yend = 2,
           arrow = arrow(length = unit(0.12, "cm")), colour = ink)
p4

5. Add the grazer and its flow

Grazers are geom_odum_consumer() (a hexagon). Then draw the grass→grazer arrow.

p5 <- p4 +
  geom_odum_consumer(data = nodes[3, ], width = 1.3, height = 0.9,
                      fill = "#8B4513") +
  annotate("segment", x = 4.2, xend = 5.35, y = 2, yend = 2,
           arrow = arrow(length = unit(0.12, "cm")), colour = ink)
p5

6. Add the top predator + its flow

Same geom family, different fill. Grazer→predator arrow closes the chain.

p6 <- p5 +
  geom_odum_consumer(data = nodes[4, ], width = 1.3, height = 0.9,
                      fill = "#5D4037") +
  annotate("segment", x = 6.65, xend = 7.85, y = 2, yend = 2,
           arrow = arrow(length = unit(0.12, "cm")), colour = ink)
p6

7. Second-law bookkeeping: heat sinks

Every transformation dissipates heat. Add one geom_odum_heat_sink() under each consumer.

sinks <- data.frame(x = c(3.5, 6, 8.5), y = 0.7)

p7 <- p6 +
  geom_odum_heat_sink(data = sinks, width = 0.55, height = 0.85,
                       fill = "#455a64") +
  annotate("segment", x = 3.5, xend = 3.5, y = 1.5, yend = 1.1,
           arrow = arrow(length = unit(0.12, "cm")), colour = ink) +
  annotate("segment", x = 6, xend = 6, y = 1.55, yend = 1.1,
           arrow = arrow(length = unit(0.12, "cm")), colour = ink) +
  annotate("segment", x = 8.5, xend = 8.5, y = 1.55, yend = 1.1,
           arrow = arrow(length = unit(0.12, "cm")), colour = ink)
p7

8. Labels + final polish

Label each node, tighten the frame, add a title. Now it’s a diagram.

p8 <- p7 +
  geom_text(aes(y = 3.15, label = role), size = 3.2, colour = ink) +
  labs(title = "Solar-driven grazing food web") +
  theme(plot.title = element_text(hjust = 0.5, size = 12, colour = "#176b17"))
p8

What just happened

Every ESL diagram is the same recipe: an x/y node table, one geom_odum_* per node kind, annotate("segment", ...) for the flows, geom_odum_heat_sink() under every transformation, labels via geom_text(). That’s the entire grammar. See the case-studies vignette (vignette("case-studies", package = "energese")) for four worked classical Odum systems built on this same recipe.