Concentric Puzzles

Rings of pieces radiating from a central shape

Overview

Concentric puzzles feature rings of pieces that radiate outward from a central shape. They combine the elegance of hexagonal geometry with a distinctive radial pattern, creating puzzles that are both beautiful and satisfying to assemble.

Ring Variations

make_conc_rings <- function(r) {
  ggplot() +
    geom_puzzle_conc(
      aes(fill = after_stat(ring)),
      rings = r,
      seed = 42
    ) +
    scale_fill_viridis_c(option = "plasma", guide = "none") +
    coord_fixed() +
    theme_puzzle() +
    labs(title = paste0(r, " rings"))
}

print(make_conc_rings(2) + make_conc_rings(3) + make_conc_rings(4))
Figure 1
# Generate ring previews in a grid
rings <- c(2, 3, 4)
items <- lapply(rings, function(r) {
  generate_puzzle(type = "concentric", grid = c(r), size = c(200), seed = 42, fill_palette = "plasma")
})
render_puzzle_grid(items, ncol = 3, labels = paste(rings, "rings"))
2 rings
3 rings
4 rings
Figure 2

Center Shape Options

The center piece can be either a hexagon or a circle:

# Hexagon center (default)
print(ggplot() +
  geom_puzzle_conc(
    aes(fill = after_stat(ring)),
    rings = 3,
    center_shape = "hexagon",
    seed = 42
  ) +
  scale_fill_viridis_c(option = "magma", guide = "none") +
  coord_fixed() +
  theme_puzzle() +
  labs(title = "Hexagon center"))

# Circle center
print(ggplot() +
  geom_puzzle_conc(
    aes(fill = after_stat(ring)),
    rings = 3,
    center_shape = "circle",
    seed = 42
  ) +
  scale_fill_viridis_c(option = "magma", guide = "none") +
  coord_fixed() +
  theme_puzzle() +
  labs(title = "Circle center"))
Figure 3: Hexagon center
Figure 4: Circle center
# Hexagon center (default)
result_hex <- generate_puzzle(
  type = "concentric",
  grid = c(3),
  size = c(200),
  seed = 42,
  center_shape = "hexagon",
  fill_palette = "magma"
)
render_puzzle_preview(result_hex)

# Circle center
result_circle <- generate_puzzle(
  type = "concentric",
  grid = c(3),
  size = c(200),
  seed = 42,
  center_shape = "circle",
  fill_palette = "magma"
)
render_puzzle_preview(result_circle)
Figure 5: Hexagon center
Figure 6: Hexagon center

Fill by Ring vs Piece ID

# By ring
print(ggplot() +
  geom_puzzle_conc(
    aes(fill = after_stat(ring)),
    rings = 3, seed = 42
  ) +
  scale_fill_viridis_c(option = "viridis", name = "Ring") +
  coord_fixed() +
  theme_puzzle() +
  labs(title = "By ring") +
  theme(legend.position = "bottom"))

# By piece_id
print(ggplot() +
  geom_puzzle_conc(
    aes(fill = after_stat(piece_id)),
    rings = 3, seed = 42
  ) +
  scale_fill_viridis_c(option = "viridis", name = "Piece") +
  coord_fixed() +
  theme_puzzle() +
  labs(title = "By piece_id") +
  theme(legend.position = "bottom"))
Figure 7: Fill by ring
Figure 8: Fill by piece_id

Color Palettes

make_conc_palette <- function(pal) {
  ggplot() +
    geom_puzzle_conc(
      aes(fill = after_stat(ring)),
      rings = 3, seed = 42
    ) +
    scale_fill_viridis_c(option = pal, guide = "none") +
    coord_fixed() +
    theme_puzzle() +
    labs(title = pal)
}

print((make_conc_palette("viridis") + make_conc_palette("magma") + make_conc_palette("plasma")) /
(make_conc_palette("inferno") + make_conc_palette("cividis") + make_conc_palette("turbo")))
Figure 9
# Generate all palette previews and combine into a grid
palettes <- c("viridis", "magma", "plasma", "inferno", "cividis", "turbo")
items <- lapply(palettes, function(pal) {
  generate_puzzle(type = "concentric", grid = c(3), size = c(200), seed = 42, fill_palette = pal)
})
render_puzzle_grid(items, ncol = 3, labels = palettes)
viridis
magma
plasma
inferno
cividis
turbo
Figure 10

Fill Direction

Control the spatial order of color assignment. For concentric puzzles, "reverse" reverses colors within each ring while keeping the center piece unchanged. This is distinct from palette_invert which reverses the dark↔︎light ends of the palette.

make_direction_plot <- function(dir) {
  ggplot() +
    geom_puzzle_conc(
      aes(fill = after_stat(fill_order)),
      rings = 3,
      fill_direction = dir,
      seed = 42
    ) +
    scale_fill_viridis_c(option = "viridis", guide = "none") +
    coord_fixed() +
    theme_puzzle() +
    labs(title = paste0("fill_direction = \"", dir, "\""))
}

print(make_direction_plot("forward") + make_direction_plot("reverse"))
Figure 11
directions <- c("forward", "reverse")
items <- lapply(directions, function(dir) {
  generate_puzzle(type = "concentric", grid = c(3), size = c(200), seed = 42, fill_palette = "viridis", fill_direction = dir)
})
render_puzzle_grid(items, ncol = 2, labels = paste0("fill_direction = \"", directions, "\""))
fill_direction = "forward"
fill_direction = "reverse"
Figure 12

Offset (Separation)

make_conc_offset <- function(off) {
  ggplot() +
    geom_puzzle_conc(
      aes(fill = after_stat(ring)),
      rings = 3,
      offset = off, seed = 42
    ) +
    scale_fill_viridis_c(option = "cividis", guide = "none") +
    coord_fixed() +
    theme_puzzle() +
    labs(title = paste0("offset = ", off))
}

print(make_conc_offset(0) + make_conc_offset(5) + make_conc_offset(10) + make_conc_offset(15))
Figure 13
# Generate offset previews in a grid
offsets <- c(0, 5, 10, 15)
items <- lapply(offsets, function(off) {
  generate_puzzle(type = "concentric", grid = c(3), size = c(200), seed = 42, offset = off, fill_palette = "cividis")
})
render_puzzle_grid(items, ncol = 4, labels = paste("offset =", offsets))
offset = 0
offset = 5
offset = 10
offset = 15
Figure 14

Fusion Groups

Merge pieces together using PILES notation:

# Center merged with ring 1
print(ggplot() +
  geom_puzzle_conc(
    aes(fill = after_stat(ring)),
    rings = 3,
    fusion_groups = "center-ring1",
    seed = 42
  ) +
  scale_fill_viridis_c(option = "plasma", guide = "none") +
  coord_fixed() +
  theme_puzzle() +
  labs(title = "center + ring1"))

# Ring 2 pieces merged
print(ggplot() +
  geom_puzzle_conc(
    aes(fill = after_stat(ring)),
    rings = 3,
    fusion_groups = "ring2",
    seed = 42
  ) +
  scale_fill_viridis_c(option = "plasma", guide = "none") +
  coord_fixed() +
  theme_puzzle() +
  labs(title = "ring2"))
Figure 15: center + ring1
Figure 16: All of ring2
# Center merged with ring 1
result_center_ring1 <- generate_puzzle(
  type = "concentric",
  grid = c(3),
  size = c(200),
  seed = 42,
  fusion_groups = "center-ring1",
  fill_palette = "plasma"
)
render_puzzle_preview(result_center_ring1)

# Ring 2 pieces merged
result_ring2 <- generate_puzzle(
  type = "concentric",
  grid = c(3),
  size = c(200),
  seed = 42,
  fusion_groups = "ring2",
  fill_palette = "plasma"
)
render_puzzle_preview(result_ring2)
Figure 17: center + ring1
Figure 18: center + ring1

Large Concentric Puzzle

ggplot() +
  geom_puzzle_conc(
    aes(fill = after_stat(ring)),
    rings = 5,
    center_shape = "hexagon",
    seed = 456
  ) +
  scale_fill_viridis_c(
    option = "turbo",
    name = "Ring"
  ) +
  coord_fixed() +
  theme_puzzle() +
  theme(
    legend.position = "bottom",
    plot.title = element_text(hjust = 0.5, size = 16)
  ) +
  labs(title = "5-Ring Concentric Puzzle")
Figure 19
result <- generate_puzzle(
  type = "concentric",
  grid = c(5),
  size = c(300),
  seed = 456,
  center_shape = "hexagon",
  fill_palette = "turbo"
)
render_puzzle_preview(result, max_width = "500px")
Figure 20

Parameters Reference

Parameter Type Default Description
rings integer 3 Number of concentric rings
center_shape string “hexagon” Center shape: “hexagon” or “circle”
seed integer random Random seed for reproducibility
offset numeric 0 (mm) Piece separation distance
tabsize numeric 6 (%) Tab size as percentage of edge length
jitter numeric 2 (%) Randomness in tab shape as percentage
min_tab_size numeric 0 (mm) Minimum tab size in millimeters
max_tab_size numeric Inf (mm) Maximum tab size in millimeters
fusion_groups string NULL PILES notation for fusing pieces

Fill Variables

When using ggplot2 with concentric puzzles, you can map these variables:

Variable Description
piece_id Unique piece identifier
ring Which ring the piece belongs to (0 = center)
is_center TRUE if the piece is the center piece

Code Example

# Visualize with ggplot2
ggplot() +
  geom_puzzle_conc(
    aes(fill = after_stat(ring)),
    rings = 4,
    diameter = 280,
    center_shape = "hexagon",
    seed = 42,
    offset = 5
  ) +
  scale_fill_viridis_c(option = "plasma", name = "Ring") +
  coord_fixed() +
  theme_puzzle() +
  labs(title = "4-Ring Concentric Puzzle")

# Generate puzzle
result <- generate_puzzle(
  type = "concentric",
  grid = c(4),           # 4 rings
  size = c(280),         # 280mm diameter
  seed = 42,
  center_shape = "hexagon",
  offset = 5
)

# writeLines(result$svg_content, "concentric_puzzle.svg")

# Preview the puzzle
render_puzzle_preview(result)