Seasonal plots in the light of the gestalt principles.
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.
“Elements that are close together are perceived as being related.”
“Elements that share visual characteristics like colour, size, or shape are perceived as belonging to the same group.”
“Elements that are enclosed within a boundary, such as a border or a shaded region, are perceived as a group.”
Additional data points are added from a fourth group, mapped to a white colour aesthetic. These data appear to have a random pattern.
“The mind distinguishes between objects in the foreground (figure) and the background (ground).”
“Elements that move or change together are perceived as related.”
Prägnanz is a German word that means “pithiness” or “concise and meaningful”.



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)
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)
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)
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)
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)
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)
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)
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.
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)
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}
}