Wonderful Wednesday November 2025 (69)

Seasonal plots Gestalt principles Wonderful Wednesdays

Seasonal plots in the light of the gestalt principles.

PSI VIS SIG https://www.psiweb.org/sigs-special-interest-groups/visualisation
12-10-2025

Seasonal plots

Time for something different! Can you create a seasonally-themed image?

The Challenge:

A description of the challenge can also be found here.
A recording of the session can be found here.

Visualisation

Gestalt Principle #1: Proximity

“Elements that are close together are perceived as being related.”

Mapping variables to X and Y dimensions in a scatter plot provide insights into related groups of data points.

link to code

Gestalt Principle #2: Similarity

“Elements that share visual characteristics like colour, size, or shape are perceived as belonging to the same group.”

The data are grouped into categories, with each group mapped to a different shape/colour.

The three points appearing close together belong to the same group, separated from the other groups.

link to code

Gestalt Principle #3: Enclosure

“Elements that are enclosed within a boundary, such as a border or a shaded region, are perceived as a group.”

The graph is annotated to emphasis the two groups by use of enclosure.

link to code

Additional data points are added from a fourth group, mapped to a white colour aesthetic. These data appear to have a random pattern.

link to code

Gestalt Principle #4: Figure–Ground

“The mind distinguishes between objects in the foreground (figure) and the background (ground).”

Filling the circular enclosures separates the original data points from the unstructured data.

link to code

Gestalt Principle #5 : Common Fate

“Elements that move or change together are perceived as related.”

Data mapped to white symbols form a time series, and animation is used to show evolution over time.

link to code

Gestalt Principle #6: Prägnanz

Prägnanz is a German word that means “pithiness” or “concise and meaningful”.

We can apply this principle by removing unnecessary graph elements.

We also add a title with a clear message!

link to code

Christmas customs

interactive version

usage instructions

link to prompt

Festive plot generator

link to code

Christmas scene

link to story and code

Code

step 1 to snowman

library(tidyverse)
library(ggforce)
library(gganimate)

# --- Snowman setup ---
r_bottom <- 1.8
r_middle <- 1.6
r_head <- 1.1
overlap <- 0.2

y_bottom <- 0
y_middle <- y_bottom + r_bottom - overlap
y_head <- y_middle + r_middle + r_head - overlap

snowman_parts <- tibble(
  x = 0, y = c(y_middle, y_head),
  r = c( r_middle, r_head)
)

# Head features: eyes and slightly lowered nose
head_x <- 0; head_y <- y_head; head_r <- r_head
features_head <- tibble(
  x = c(head_x - 0.2*head_r, head_x + 0.2*head_r, head_x),  # eyes and nose centered
  y = c(head_y + 0.18*head_r, head_y + 0.18*head_r, head_y - 0.1*head_r),  # nose slightly lower
  size = c(3, 3, 4),  # nose larger
  col = c("black","black","orange"),
  shape = c(1,1,24)  # eyes, eyes, nose
)

# Buttons
buttons <- tibble(
  x = c(0, 0, 0),
  y = c(y_middle + 0.25*r_middle, y_middle, y_middle - 0.25*r_middle),
  size = c(r_middle, r_middle, r_middle)  # increased size
)

# --- Snowflakes ---
n_flakes <- 200
n_frames <- 150
fall_speed <- 0.08

snowflakes <- tibble(
  flake = 1:n_flakes,
  x = runif(n_flakes, -5, 5),
  y_start = runif(n_flakes, 0, 10)
)

# Compute snowflake positions for every frame
y_min <- 0
y_max <- 10
range_y <- y_max - y_min

snowflakes_anim <- snowflakes %>%
  crossing(frame = 1:n_frames) %>%
  mutate(y = y_start - fall_speed * frame,
         y = y_min + (y - y_min) %% range_y)  # smooth wrapping

p <- ggplot() +
 
  # Features (eyes and slightly lowered nose)
  geom_point(data=features_head, aes(x=x, y=y),
             color=features_head$col,
             fill=features_head$col,
             size=features_head$size,
             shape=features_head$shape) +
  
  # Buttons
  geom_point(data=buttons, aes(x=x, y=y), color="black", size=buttons$size) +
  

  coord_fixed(xlim=c(-5,5), ylim=c(0,10)) +
  theme(legend.position="none",
        axis.line = element_line(color = "black", linewidth = 0.4),
        panel.grid.major = element_line(color = "#969696", linewidth = 0.3),
        panel.grid.minor = element_blank(),   # remove minor gridlines
        panel.background = element_rect(fill = "#f0f0f0", color = NA)
  )

ggsave(p, filename = "temp00.png", width = 16, height = 16)

Back to blog

step 2 to snowman

library(tidyverse)
library(ggforce)
library(gganimate)

# --- Snowman setup ---
r_bottom <- 1.8
r_middle <- 1.6
r_head <- 1.1
overlap <- 0.2
eye_size <- 8
y_bottom <- 0
y_middle <- y_bottom + r_bottom - overlap
y_head <- y_middle + r_middle + r_head - overlap

snowman_parts <- tibble(
  x = 0, y = c(y_middle, y_head),
  r = c( r_middle, r_head)
)

# Head features: eyes and slightly lowered nose
head_x <- 0; head_y <- y_head; head_r <- r_head
features_head <- tibble(
  x = c(head_x - 0.2*head_r, head_x + 0.2*head_r, head_x),  # eyes and nose centered
  y = c(head_y + 0.18*head_r, head_y + 0.18*head_r, head_y - 0.1*head_r),  # nose slightly lower
  size = c(3, 3, 4),  # nose larger
  col = c("black","black","orange"),
  shape = c(1,1,24)  # eyes, eyes, nose
)

# Buttons
buttons <- tibble(
  x = c(0, 0, 0),
  y = c(y_middle + 0.25*r_middle, y_middle, y_middle - 0.25*r_middle),
  size = c(r_middle, r_middle, r_middle)  # increased size
)

# --- Snowflakes ---
n_flakes <- 200
n_frames <- 150
fall_speed <- 0.08

snowflakes <- tibble(
  flake = 1:n_flakes,
  x = runif(n_flakes, -5, 5),
  y_start = runif(n_flakes, 0, 10)
)

p <- ggplot() +

  # Features (eyes and slightly lowered nose)
  geom_point(data=features_head, aes(x=x, y=y),
             color="black",
             fill="black",
             size=eye_size,
             shape=1) +
  
  # Buttons
  geom_point(data=buttons, aes(x=x, y=y), color="black", size=eye_size, shape=1) +
  
  scale_x_continuous(limits=c(-0.4, 0.4)) +
  scale_y_continuous(limits=c(0, 7)) +
  theme_classic(base_size = 24) +
  theme(legend.position="none",
        axis.line = element_line(color = "black", linewidth = 0.4),
        panel.grid.major = element_line(color = "#969696", linewidth = 0.3),
        panel.grid.minor = element_blank(),   # remove minor gridlines
        panel.background = element_rect(fill = "#f0f0f0", color = NA)
  )

ggsave(p, filename = "temp01.png", width = 16, height = 16)

Back to blog

step 3 to snowman

library(tidyverse)
library(ggforce)
library(gganimate)

# --- Snowman setup ---
r_bottom <- 1.8
r_middle <- 1.6
r_head <- 1.1
overlap <- 0.2

y_bottom <- 0
y_middle <- y_bottom + r_bottom - overlap
y_head <- y_middle + r_middle + r_head - overlap

snowman_parts <- tibble(
  x = 0, y = c(y_middle, y_head),
  r = c( r_middle, r_head)
)

# Head features: eyes and slightly lowered nose
head_x <- 0; head_y <- y_head; head_r <- r_head
features_head <- tibble(
  x = c(head_x - 0.2*head_r, head_x + 0.2*head_r, head_x),  # eyes and nose centered
  y = c(head_y + 0.18*head_r, head_y + 0.18*head_r, head_y - 0.1*head_r),  # nose slightly lower
  size = c(3, 3, 4),  # nose larger
  col = c("black","black","orange"),
  shape = c(1,1,24)  # eyes, eyes, nose
)

# Buttons
buttons <- tibble(
  x = c(0, 0, 0),
  y = c(y_middle + 0.25*r_middle, y_middle, y_middle - 0.25*r_middle),
  size = c(r_middle, r_middle, r_middle)  # increased size
)

p <- ggplot() +
  # Background
  # annotate("rect", xmin = -5, xmax = 5, ymin = -5, ymax = 10, fill = "#2b3b4f") +
  
  # Snowman body
  geom_circle(data = snowman_parts, aes(x0=x, y0=y, r=r),
              # fill="white",
              color="gray30", size=0.4) +
  # Features (eyes and slightly lowered nose)
  geom_point(data=features_head, aes(x=x, y=y),
             color=features_head$col,
             fill=features_head$col,
             size=features_head$size,
             shape=features_head$shape) +
  
  # Buttons
  geom_point(data=buttons, aes(x=x, y=y), color="black", size=buttons$size) +
  
  # Snowflakes
  # geom_point(data=snowflakes_anim, aes(x=x, y=y), color="white", size=1.5, alpha=0.8) +
  
  # coord_fixed(xlim=c(-5,5), ylim=c(0,10)) +
  scale_x_continuous(limits=c(-2, 2)) +
  theme_classic(base_size = 24) +
  theme(legend.position="none",
        axis.line = element_line(color = "black", linewidth = 0.4),
        panel.grid.major = element_line(color = "darkgray", linewidth = 0.3),
        panel.grid.minor = element_blank(),   # remove minor gridlines
        panel.background = element_rect(fill = "grey85", color = NA)
  )

ggsave(p, filename = "sm02.png", width = 16, height = 16)

Back to blog

step 4 to snowman

library(tidyverse)
library(ggforce)
library(gganimate)

# --- Snowman setup ---
r_bottom <- 1.8
r_middle <- 1.6
r_head <- 1.1
overlap <- 0.2
eye_size <- 8
y_bottom <- 0
y_middle <- y_bottom + r_bottom - overlap
y_head <- y_middle + r_middle + r_head - overlap

snowman_parts <- tibble(
  x = 0, y = c(y_middle, y_head),
  r = c( r_middle, r_head)
)

# Head features: eyes and slightly lowered nose
features_head <- tibble(
  x = c(head_x - 0.2*head_r, head_x + 0.2*head_r, head_x),  # eyes and nose centered
  y = c(head_y + 0.18*head_r, head_y + 0.18*head_r, head_y - 0.1*head_r),  # nose slightly lower
  size = c(eye_size, eye_size, nose_size),  # nose larger
  col = c("black","black","orange"),
  shape = c(1,1,24)  # eyes, eyes, nose
)

# Buttons
buttons <- tibble(
  x = c(0, 0, 0),
  y = c(y_middle + 0.25*r_middle, y_middle, y_middle - 0.25*r_middle),
  size = c(button_size, button_size, button_size)   
)

# --- Snowflakes ---
n_flakes <- 200
n_frames <- 150
fall_speed <- 0.08

snowflakes <- tibble(
  flake = 1:n_flakes,
  x = runif(n_flakes, -5, 5),
  y_start = runif(n_flakes, 0, 10)
)



p <- ggplot() +
  # Background
  # annotate("rect", xmin = -5, xmax = 5, ymin = -5, ymax = 10, fill = "#2b3b4f") +
  
  # Snowman body
  geom_circle(data = snowman_parts, aes(x0=x, y0=y, r=r),
              fill=NULL,
              color="gray30", size=0.4) +

  geom_point(data=features_head, aes(x=x, y=y),
             color=features_head$col,
             fill=features_head$col,
             size=features_head$size,
             shape=features_head$shape) +
  
  # Buttons
  geom_point(data=buttons, aes(x=x, y=y), color="black", size=eye_size) +
  
  # Snowflakes
  # Snowflakes
  geom_point(data=snowflakes, aes(x=x, y=y_start), color="white", size=1.5, alpha=0.8) +
  # 
  scale_x_continuous(limits=c(-5, 5)) +
  scale_y_continuous(limits=c(0, 10)) +
  theme_classic(base_size = 24) +
  theme(legend.position="none",
        axis.line = element_line(color = "black", linewidth = 0.4),
        panel.grid.major = element_line(color = "#969696", linewidth = 0.3),
        panel.grid.minor = element_blank(),   # remove minor gridlines
        panel.background = element_rect(fill = "#d9d9d9", color = NA)
  )

ggsave(p, filename = "temp03.png", width = 16, height = 16)

Back to blog

step 5 to snowman

library(tidyverse)
library(ggforce)
library(gganimate)

# --- Snowman setup ---
r_bottom <- 1.8
r_middle <- 1.6
r_head <- 1.1
overlap <- 0.2
eye_size <- 8
y_bottom <- 0
y_middle <- y_bottom + r_bottom - overlap
y_head <- y_middle + r_middle + r_head - overlap

snowman_parts <- tibble(
  x = 0, y = c(y_middle, y_head),
  r = c( r_middle, r_head)
)

# Head features: eyes and slightly lowered nose
features_head <- tibble(
  x = c(head_x - 0.2*head_r, head_x + 0.2*head_r, head_x),  # eyes and nose centered
  y = c(head_y + 0.18*head_r, head_y + 0.18*head_r, head_y - 0.1*head_r),  # nose slightly lower
  size = c(eye_size, eye_size, nose_size),  # nose larger
  col = c("black","black","orange"),
  shape = c(1,1,24)  # eyes, eyes, nose
)

# Buttons
buttons <- tibble(
  x = c(0, 0, 0),
  y = c(y_middle + 0.25*r_middle, y_middle, y_middle - 0.25*r_middle),
  size = c(button_size, button_size, button_size)   
)

# --- Snowflakes ---
n_flakes <- 200
n_frames <- 150
fall_speed <- 0.08

snowflakes <- tibble(
  flake = 1:n_flakes,
  x = runif(n_flakes, -5, 5),
  y_start = runif(n_flakes, 0, 10)
)



p <- ggplot() +

  # Snowman body
  geom_circle(data = snowman_parts, aes(x0=x, y0=y, r=r),
              fill="white",
              color="gray30", size=0.4) +

  geom_point(data=features_head, aes(x=x, y=y),
             color=features_head$col,
             fill=features_head$col,
             size=features_head$size,
             shape=features_head$shape) +
  
  # Buttons
  geom_point(data=buttons, aes(x=x, y=y), color="black", size=eye_size) +
  
  # Snowflakes
  # Snowflakes
  geom_point(data=snowflakes, aes(x=x, y=y_start), color="white", size=1.5, alpha=0.8) +
  # 
  scale_x_continuous(limits=c(-5, 5)) +
  scale_y_continuous(limits=c(0, 10)) +
  theme_classic(base_size = 24) +
  theme(legend.position="none",
        axis.line = element_line(color = "black", linewidth = 0.4),
        panel.grid.major = element_line(color = "#969696", linewidth = 0.3),
        panel.grid.minor = element_blank(),   # remove minor gridlines
        panel.background = element_rect(fill = "#d9d9d9", color = NA)
  )

ggsave(p, filename = "temp04.png", width = 16, height = 16)

Back to blog

step 6 to snowman

library(tidyverse)
library(ggforce)
library(gganimate)

# --- Snowman setup ---

r_bottom <- 1.8
r_middle <- 1.6
r_head <- 1.1
overlap <- 0.2
eye_size <- 6
button_size <- 6
nose_size <- 6
y_bottom <- 0
y_middle <- y_bottom + r_bottom - overlap
y_head <- y_middle + r_middle + r_head - overlap

snowman_parts <- tibble(
  x = 0, y = c(y_middle, y_head),
  r = c( r_middle, r_head)
)

# Head features: eyes and slightly lowered nose
features_head <- tibble(
  x = c(head_x - 0.2*head_r, head_x + 0.2*head_r, head_x),  # eyes and nose centered
  y = c(head_y + 0.19*head_r, head_y + 0.19*head_r, head_y - 0.1*head_r),  # nose slightly lower
  size = c(eye_size, eye_size, nose_size),  # nose larger
  col = c("black","black","orange"),
  shape = c(1,1,24)  # eyes, eyes, nose
)

# Buttons
buttons <- tibble(
  x = c(0, 0, 0),
  y = c(y_middle + 0.25*r_middle, y_middle, y_middle - 0.25*r_middle),
  size = c(button_size, button_size, button_size)   
)

# --- Snowflakes ---
n_flakes <- 200
n_frames <- 150
fall_speed <- 0.08

snowflakes <- tibble(
  flake = 1:n_flakes,
  x = runif(n_flakes, -5, 5),
  y_start = runif(n_flakes, 0, 10)
)

snowflakes_anim <- snowflakes %>%
  crossing(frame = 1:n_frames) %>%
  mutate(y = y_start - fall_speed * frame,
         y = y_min + (y - y_min) %% range_y)  # smooth wrapping

p <- ggplot() +

  # Snowman body
  geom_circle(data = snowman_parts, aes(x0=x, y0=y, r=r),
              fill="white",
              color="gray30", size=0.4) +
 
    geom_point(data=features_head, aes(x=x, y=y),
             color=features_head$col,
             fill=features_head$col,
             size=features_head$size,
             shape=features_head$shape) +
  
  # Buttons
  geom_point(data=buttons, aes(x=x, y=y), color="black", size=eye_size) +
  
  # Snowflakes
  # Snowflakes
  geom_point(data=snowflakes_anim, aes(x=x, y=y), color="white", size=1.5, alpha=0.8) +
  # 
  scale_x_continuous(limits=c(-5, 5)) +
  scale_y_continuous(limits=c(0, 10)) +
  theme_classic(base_size = 24) +
  theme(legend.position="none",
        axis.line = element_line(color = "black", linewidth = 0.4),
        panel.grid.major = element_line(color = "#969696", linewidth = 0.3),
        panel.grid.minor = element_blank(),   # remove minor gridlines
        panel.background = element_rect(fill = "#d9d9d9", color = NA)
  ) +
  
  # Animation
  transition_manual(frame)

anim_out <- animate(p, nframes=n_frames, fps=30, width=700, height=700, renderer = gifski_renderer(loop = TRUE))

anim_save("temp05.gif", anim_out)

Back to blog

step 7 to final snowman

library(tidyverse)
library(ggforce)
library(gganimate)

# --- Snowman setup ---

r_bottom <- 1.8
r_middle <- 1.6
r_head <- 1.1
overlap <- 0.2

eye_size <- 6
button_size <- 6
nose_size <- 6
hat_width1 <- 1.0
hat_width2 <- 0.8
hat_height <- 0.7

y_bottom <- 0
y_middle <- y_bottom + r_bottom - overlap
y_head <- y_middle + r_middle + r_head - overlap

snowman_parts <- tibble(
  x = 0, y = c(y_middle, y_head),
  r = c( r_middle, r_head)
)

# Head features: eyes and slightly lowered nose
features_head <- tibble(
  x = c(head_x - 0.2*head_r, head_x + 0.2*head_r, head_x),  # eyes and nose centered
  y = c(head_y + 0.19*head_r, head_y + 0.19*head_r, head_y - 0.1*head_r),  # nose slightly lower
  size = c(eye_size, eye_size, nose_size),  # nose larger
  col = c("black","black","orange"),
  shape = c(1,1,24)  # eyes, eyes, nose
)

# Buttons
buttons <- tibble(
  x = c(0, 0, 0),
  y = c(y_middle + 0.25*r_middle, y_middle, y_middle - 0.25*r_middle),
  size = c(button_size, button_size, button_size)   
)

# --- Snowflakes ---
n_flakes <- 200
n_frames <- 150
fall_speed <- 0.08

snowflakes <- tibble(
  flake = 1:n_flakes,
  x = runif(n_flakes, -5, 5),
  y_start = runif(n_flakes, 0, 10)
)

snowflakes_anim <- snowflakes %>%
  crossing(frame = 1:n_frames) %>%
  mutate(y = y_start - fall_speed * frame,
         y = y_min + (y - y_min) %% range_y)  # smooth wrapping

p <- ggplot() +
 
  # Snowman body
  geom_circle(data = snowman_parts, aes(x0=x, y0=y, r=r),
              fill="white",
              color="gray30", size=0.4) +
  # 
  # Arms
  # geom_segment(data = arms, aes(x=x, y=y, xend=xend, yend=yend),
  #              size=1.2, color="saddlebrown", lineend="round") +
  
  # Hat
  geom_rect(aes(xmin=-hat_width2, xmax=hat_width2,
                ymin=head_y+head_r, ymax=head_y+head_r+hat_height),
            fill="black") +
  geom_rect(aes(xmin=-hat_width1, xmax=hat_width1,
                ymin=head_y+head_r-hat_brim_height/2, ymax=head_y+head_r+hat_brim_height/2),
            fill="black") +
  geom_rect(aes(xmin=-hat_width2, xmax=hat_width2,
                ymin=head_y+head_r+hat_brim_height/2, ymax=head_y+head_r+hat_brim_height/2+hat_height/10),
            fill="red") +
  
  geom_point(data=features_head, aes(x=x, y=y),
             color=features_head$col,
             fill=features_head$col,
             size=features_head$size,
             shape=features_head$shape) +
  
  # Buttons
  geom_point(data=buttons, aes(x=x, y=y), color="black", size=eye_size) +
  
  # Snowflakes
  geom_point(data=snowflakes_anim, aes(x=x, y=y), color="white", size=1.5, alpha=0.8) +
  # 
  scale_x_continuous(limits=c(-5, 5)) +
  scale_y_continuous(limits=c(0, 10)) +
  
  theme_void() +
  theme(legend.position="none",
         panel.background = element_rect(fill = "#d9d9d9", color = NA)
  ) +
  
  # Animation
  transition_manual(frame)

p <- p +
  annotate(
    "text",
    x = 0, y = 8.5,
    label = "MERRY CHRISTMAS!",
    size = 15,
    fontface = "bold",
    family = "sans",
    colour = "black"
  )


anim_out <- animate(p, nframes=n_frames, fps=30, width=700, height=700, renderer = gifski_renderer(loop = TRUE))

anim_save("temp06.gif", anim_out)

Back to blog

Christmas customs prompting

I (Thomas) used Claude.ai, Sonnet 4.5. to generate this graph. Claude’s response is christmas-knowledge-graph_prototype.html. A lot of editing and tweaking eventually led to christmas-knowledge-graph_improved.html. Because I overran the limits for an individual conversation, I had to restart the thread. I usually use this to ask Claude to check the code for any “clutter” that might have accumulated during previous iterations.

Initial prompt

I would like to generate an interactive knowledge graph. The theme is popular Christmas traditions around the world, their historical origins, relationships with each other, and other contemporary customs. Interactive features could include * links to online information when the cursor hovers of a node * options to customise the graph: selecting node categories (persons, places, traditions, etc.), changing edge distance, etc. Please * add suggestions for content and visualisation (bullet point list, minimal details) * sketch a strategy for implementation, including programming language(s) and packages best suited for the task, and a bullet-point outline of the essential tasks involved in building the knowledge graph

Prompt initiating the follow-up thread

I would like to generate an interactive knowledge graph. […, as above] Attached is a draft version. Please check the code for robustness and efficiency. Remove any parts that are obsolete and suggest improvements.

Back to blog

Festive plot generator

library(shiny)
library(ggplot2)

# Include your create_festive_plot function
create_festive_plot <- function(to = "Recipient",
                                from = "Sender",
                                message = "Happy Holidays!",
                                image_type = c("christmas gift", "christmas tree", "snowman")) {
  
  image_type <- match.arg(image_type)
  
  p <- ggplot() + coord_fixed() + theme_void()
  
  if (image_type == "christmas gift") {
    p <- p + xlim(0, 10) + ylim(0, 12)
  } else {
    p <- p + xlim(0, 10) + ylim(0, 12)
  }
  
  if (image_type == "christmas gift") {
    p <- p +
      geom_rect(aes(xmin = 2, xmax = 8, ymin = 2, ymax = 6),
                fill = "#D32F2F", color = "black") +
      geom_rect(aes(xmin = 1.5, xmax = 8.5, ymin = 6, ymax = 7),
                fill = "#C62828", color = "black") +
      geom_rect(aes(xmin = 4.5, xmax = 5.5, ymin = 2, ymax = 7),
                fill = "#FFD700") +
      geom_rect(aes(xmin = 1.5, xmax = 8.5, ymin = 5.5, ymax = 6.5),
                fill = "#FFD700") +
      geom_curve(aes(x = 5, y = 7, xend = 3.5, yend = 8.5),
                 curvature = -0.5, color = "#FFD700", linewidth = 1.5) +
      geom_curve(aes(x = 5, y = 7, xend = 6.5, yend = 8.5),
                 curvature = 0.5, color = "#FFD700", linewidth = 1.5)
    
  } else if (image_type == "christmas tree") {
    p <- p +
      geom_polygon(aes(x = c(5, 2, 8), y = c(9, 5, 5)), fill = "#2E7D32") +
      geom_polygon(aes(x = c(5, 2.5, 7.5), y = c(7, 3, 3)), fill = "#388E3C") +
      geom_rect(aes(xmin = 4.5, xmax = 5.5, ymin = 2, ymax = 3), fill = "#6D4C41") +
      geom_point(aes(x = 5, y = 9), shape = 8, size = 6, color = "#FFD700")
    
  } else if (image_type == "snowman") {
    p <- p +
      geom_point(aes(x = 5, y = 3), size = 40, color = "black", stroke = 1, shape = 21, fill = "white") +
      geom_point(aes(x = 5, y = 5), size = 30, color = "black", stroke = 1, shape = 21, fill = "white") +
      geom_point(aes(x = 5, y = 6.8), size = 20, color = "black", stroke = 1, shape = 21, fill = "white") +
      geom_point(aes(x = 4.7, y = 7), size = 1.5) +
      geom_point(aes(x = 5.3, y = 7), size = 1.5) +
      geom_segment(aes(x = 5, y = 6.8, xend = 5.4, yend = 6.7), color = "orange", linewidth = 1.5) +
      geom_segment(aes(x = 4, y = 5.5, xend = 3, yend = 6.5), color = "brown", linewidth = 1.5) +
      geom_segment(aes(x = 6, y = 5.5, xend = 7, yend = 6.5), color = "brown", linewidth = 1.5)
  }
  
  p <- p +
    annotate("text", x = 5, y = 11, label = message,
             size = 6, fontface = "bold", color = "#2E7D32") +
    annotate("text", x = 5, y = 10.3, label = paste("To:", to),
             size = 5, color = "#1565C0") +
    annotate("text", x = 5, y = 9.7, label = paste("From:", from),
             size = 5, color = "#1565C0")
  
  return(p)
}

# ---- Shiny App ----
ui <- fluidPage(
  titlePanel("Festive Plot Generator"),
  
  sidebarLayout(
    sidebarPanel(
      textInput("to", "To:", value = "Recipient"),
      textInput("from", "From:", value = "Sender"),
      textInput("message", "Message:", value = "Happy Holidays!"),
      selectInput("image_type", "Select Image:",
                  choices = c("christmas gift", "christmas tree", "snowman"),
                  selected = "christmas gift")
    ),
    
    mainPanel(
      plotOutput("festivePlot", height = "600px")
    )
  )
)

server <- function(input, output) {
  output$festivePlot <- renderPlot({
    create_festive_plot(to = input$to,
                        from = input$from,
                        message = input$message,
                        image_type = input$image_type)
  })
}

shinyApp(ui = ui, server = server)

Back to blog

Citation

For attribution, please cite this work as

SIG (2025, Dec. 10). VIS-SIG Blog: Wonderful Wednesday November 2025 (69). Retrieved from https://graphicsprinciples.github.io/posts/2025-12-10-wonderful-wednesday-december-2025/

BibTeX citation

@misc{sig2025wonderful,
  author = {SIG, PSI VIS},
  title = {VIS-SIG Blog: Wonderful Wednesday November 2025 (69)},
  url = {https://graphicsprinciples.github.io/posts/2025-12-10-wonderful-wednesday-december-2025/},
  year = {2025}
}