{"spec_id":"dot-matrix-proportional","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' dot-matrix-proportional: Dot Matrix Chart for Proportional Counts\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 90/100 | Created: 2026-09-05\n\nlibrary(ggplot2)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens ------------------------------------------------------------\nTHEME     <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG   <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nINK       <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT  <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nINK_MUTED <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\n\n# Imprint palette — semantic exception applied: sentiment maps to\n# positive/negative/neutral rather than ordinal position (see\n# default-style-guide.md \"Semantic exception\").\nCOLOR_AGREE     <- \"#009E73\" # Imprint position 1 — positive sentiment\nCOLOR_DISAGREE  <- \"#AE3030\" # Imprint semantic anchor — negative sentiment\nCOLOR_UNDECIDED <- INK_MUTED # muted anchor — neutral / other\n\n# --- Data ---------------------------------------------------------------------\n# \"47 out of 100 respondents agreed\" — a 10x10 dot matrix survey result.\nn_cols <- 10\nn_rows <- 10\ntotal <- n_cols * n_rows\n\ncounts <- c(Agree = 47, Disagree = 33, Undecided = 20)\nstopifnot(sum(counts) == total)\n\ncategory <- factor(rep(names(counts), times = counts), levels = names(counts))\n\ndots <- tibble::tibble(\n  index        = seq_len(total),\n  col          = ((index - 1) %% n_cols) + 1,\n  row_from_top = ((index - 1) %/% n_cols) + 1,\n  row          = n_rows - row_from_top + 1,\n  category     = category\n)\n\n# Manual legend geometry — placed in the same coordinate space as the grid.\n# Built by hand (rather than ggplot2's automatic legend) because the\n# automatic legend competes with coord_fixed() for width and silently\n# overflows the fixed canvas instead of reflowing.\nlegend_x_swatch <- n_cols + 1.6\nlegend_x_label <- legend_x_swatch + 1.0\nlegend_x_max <- legend_x_label + 5.8\nlegend_y <- c(7, 5, 3)\n\nlegend_df <- tibble::tibble(\n  category = factor(names(counts), levels = names(counts)),\n  y = legend_y,\n  label = sprintf(\"%s — %d\", names(counts), counts)\n)\n\n# --- Plot -----------------------------------------------------------------\ntitle <- \"Customer Satisfaction Survey · dot-matrix-proportional · r · ggplot2 · anyplot.ai\"\ntitle_fontsize <- max(round(12 * min(1, 67 / nchar(title))), 8)\n\np <- ggplot() +\n  geom_point(\n    data = dots, aes(x = col, y = row, color = category),\n    size = 8, shape = 16\n  ) +\n  geom_point(\n    data = legend_df, aes(x = legend_x_swatch, y = y, color = category),\n    size = 8, shape = 16\n  ) +\n  geom_text(\n    data = legend_df,\n    aes(x = legend_x_label, y = y, label = label),\n    hjust = 0, size = 4.8, color = INK_SOFT\n  ) +\n  annotate(\n    \"text\",\n    x = legend_x_swatch, y = 9, label = \"Responses (n = 100)\",\n    hjust = 0, size = 4.8, color = INK, fontface = \"bold\"\n  ) +\n  scale_color_manual(\n    values = c(\n      Agree = COLOR_AGREE,\n      Disagree = COLOR_DISAGREE,\n      Undecided = COLOR_UNDECIDED\n    ),\n    guide = \"none\"\n  ) +\n  coord_fixed(\n    ratio = 1,\n    xlim = c(0.3, legend_x_max),\n    ylim = c(0.3, n_rows + 0.7),\n    expand = FALSE\n  ) +\n  labs(title = title) +\n  theme_void(base_size = 8) +\n  theme(\n    plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background = element_rect(fill = PAGE_BG, color = NA),\n    plot.title = element_text(\n      color = INK,\n      size = title_fontsize,\n      hjust = 0.5,\n      margin = margin(b = 16)\n    ),\n    plot.margin = margin(t = 20, r = 20, b = 20, l = 20)\n  )\n\n# --- Save -------------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot = p,\n  device = ragg::agg_png,\n  width = 8,\n  height = 4.5,\n  units = \"in\",\n  dpi = 400\n)\n"}