""" Generate the W3L4 EmotionMap process infographic. Wide horizontal layout, in the style of Gordon's reference image: five numbered steps (dark circles, amber+dark caps headlines, flat geometric icons, captions) plus a sixth amber-outlined call-out box on the right for the Porter & Ten Brinke factor. Outputs PNG and SVG into the repo root so both can be dropped into EngageLMS / Word / slide decks. British English, no emojis. Run with: uv run python scratch/build_process_infographic.py """ from __future__ import annotations from pathlib import Path import matplotlib.pyplot as plt import matplotlib.patches as mpatches from matplotlib.patches import FancyBboxPatch, Circle, Polygon, Rectangle REPO_ROOT = Path(__file__).resolve().parent.parent # Palette aligned with the EngageLMS template + verdict bands DARK = "#3C373C" AMBER = "#FFAA2D" GREEN = "#228B54" ORANGE = "#C85A28" SKIN = "#e0a78a" GREY = "#dcdce0" LIGHT = "#fafafa" # ---------- Icons (each draws into the supplied axes at (cx, cy)) ------ def icon_watch(ax, cx, cy): """Monitor with play triangle + a small face.""" # Monitor body ax.add_patch(FancyBboxPatch( (cx - 0.62, cy - 0.32), 0.95, 0.65, boxstyle="round,pad=0.02", ec=DARK, fc="white", lw=2.2, )) # Stand ax.add_patch(Rectangle((cx - 0.18, cy - 0.40), 0.20, 0.08, color=DARK)) # Play triangle ax.add_patch(Polygon( [[cx - 0.27, cy - 0.10], [cx - 0.27, cy + 0.20], [cx - 0.02, cy + 0.05]], color=AMBER, )) # Face beside monitor (Ekman headshot stand-in) ax.add_patch(Circle((cx + 0.55, cy + 0.02), 0.20, fc=SKIN, ec=DARK, lw=1.8)) # Eyes ax.add_patch(Circle((cx + 0.48, cy + 0.06), 0.025, color=DARK)) ax.add_patch(Circle((cx + 0.62, cy + 0.06), 0.025, color=DARK)) def icon_phone(ax, cx, cy): """Phone outline with a wireframe face on the screen.""" # Phone body ax.add_patch(FancyBboxPatch( (cx - 0.30, cy - 0.45), 0.60, 0.95, boxstyle="round,pad=0.04", ec=DARK, fc=DARK, lw=1.5, )) # Screen ax.add_patch(Rectangle( (cx - 0.24, cy - 0.36), 0.48, 0.78, color="white", )) # Speaker dot ax.add_patch(Circle((cx, cy + 0.46), 0.018, color="white")) # Wireframe face on screen ax.add_patch(Circle((cx, cy + 0.04), 0.18, ec=DARK, fc="none", lw=1.4)) # Eyes + nose + mouth (geometric) ax.plot([cx - 0.10, cx - 0.04], [cy + 0.10, cy + 0.10], color=DARK, lw=1.4) ax.plot([cx + 0.04, cx + 0.10], [cy + 0.10, cy + 0.10], color=DARK, lw=1.4) ax.plot([cx, cx], [cy + 0.05, cy - 0.04], color=DARK, lw=1.4) ax.plot([cx - 0.07, cx + 0.07], [cy - 0.10, cy - 0.10], color=DARK, lw=1.4) # Mesh hint ax.plot([cx - 0.18, cx + 0.18], [cy + 0.04, cy + 0.04], color=DARK, lw=0.6, alpha=0.4) ax.plot([cx, cx], [cy + 0.22, cy - 0.14], color=DARK, lw=0.6, alpha=0.4) def icon_webcam(ax, cx, cy): """Webcam lens with a face inside.""" # Outer lens ax.add_patch(Circle((cx, cy), 0.45, fc=DARK, ec=DARK, lw=1)) # Mid ring ax.add_patch(Circle((cx, cy), 0.38, fc="white", ec=DARK, lw=1)) # Face inside ax.add_patch(Circle((cx, cy - 0.02), 0.22, fc=SKIN, ec=DARK, lw=1.5)) # Hair cap ax.add_patch(mpatches.Wedge((cx, cy - 0.02), 0.22, 30, 150, fc=DARK, ec=DARK)) # Eyes ax.add_patch(Circle((cx - 0.07, cy - 0.02), 0.022, color=DARK)) ax.add_patch(Circle((cx + 0.07, cy - 0.02), 0.022, color=DARK)) # Mouth (small line) ax.plot([cx - 0.05, cx + 0.05], [cy - 0.12, cy - 0.12], color=DARK, lw=1.4) # Tripod hint ax.add_patch(Rectangle((cx - 0.06, cy - 0.55), 0.12, 0.10, color=DARK)) def icon_verdict(ax, cx, cy): """Monitor showing the AGREES / DISAGREES bars.""" # Monitor body ax.add_patch(FancyBboxPatch( (cx - 0.62, cy - 0.32), 1.25, 0.70, boxstyle="round,pad=0.02", ec=DARK, fc="white", lw=2.2, )) # Stand ax.add_patch(Rectangle((cx - 0.10, cy - 0.40), 0.20, 0.08, color=DARK)) # AGREES bar ax.text(cx - 0.52, cy + 0.16, "✓", color=GREEN, fontsize=12, weight="bold", va="center") ax.text(cx - 0.40, cy + 0.16, "AGREES", color=DARK, fontsize=8, weight="bold", va="center") ax.add_patch(Rectangle((cx - 0.05, cy + 0.10), 0.55, 0.12, color=GREEN)) ax.add_patch(Rectangle((cx + 0.50, cy + 0.10), 0.08, 0.12, color=GREY)) # DISAGREES bar ax.text(cx - 0.52, cy - 0.10, "✗", color=ORANGE, fontsize=12, weight="bold", va="center") ax.text(cx - 0.40, cy - 0.10, "DISAGREES", color=DARK, fontsize=8, weight="bold", va="center") ax.add_patch(Rectangle((cx - 0.05, cy - 0.16), 0.25, 0.12, color=ORANGE)) ax.add_patch(Rectangle((cx + 0.20, cy - 0.16), 0.38, 0.12, color=GREY)) def icon_pdfs(ax, cx, cy): """Two stacked PDF pages — one labelled FACE, one WIREFRAME.""" # Face page page_w, page_h = 0.50, 0.70 fx, fy = cx - 0.36, cy - 0.30 ax.add_patch(Rectangle((fx, fy), page_w, page_h, fc="white", ec=DARK, lw=1.8)) # PDF tag ax.add_patch(Rectangle((fx, fy + page_h - 0.16), 0.20, 0.16, color=ORANGE)) ax.text(fx + 0.10, fy + page_h - 0.08, "PDF", color="white", fontsize=7, weight="bold", ha="center", va="center") # Face on page ax.add_patch(Circle((fx + page_w/2, fy + 0.30), 0.13, fc=SKIN, ec=DARK, lw=1)) ax.add_patch(Circle((fx + page_w/2 - 0.04, fy + 0.32), 0.012, color=DARK)) ax.add_patch(Circle((fx + page_w/2 + 0.04, fy + 0.32), 0.012, color=DARK)) ax.text(fx + page_w/2, fy - 0.05, "FACE", color=DARK, fontsize=8, weight="bold", ha="center") # Wireframe page wx, wy = cx + 0.18, cy - 0.30 ax.add_patch(Rectangle((wx, wy), page_w, page_h, fc="white", ec=DARK, lw=1.8)) ax.add_patch(Rectangle((wx, wy + page_h - 0.16), 0.20, 0.16, color=ORANGE)) ax.text(wx + 0.10, wy + page_h - 0.08, "PDF", color="white", fontsize=7, weight="bold", ha="center", va="center") # Wireframe face — circle + crosshairs wcx, wcy = wx + page_w/2, wy + 0.30 ax.add_patch(Circle((wcx, wcy), 0.13, fc="none", ec=DARK, lw=1.2)) ax.plot([wcx - 0.08, wcx - 0.03], [wcy + 0.04, wcy + 0.04], color=DARK, lw=1) ax.plot([wcx + 0.03, wcx + 0.08], [wcy + 0.04, wcy + 0.04], color=DARK, lw=1) ax.plot([wcx, wcx], [wcy + 0.02, wcy - 0.04], color=DARK, lw=1) ax.plot([wcx - 0.05, wcx + 0.05], [wcy - 0.07, wcy - 0.07], color=DARK, lw=1) ax.plot([wcx - 0.13, wcx + 0.13], [wcy, wcy], color=DARK, lw=0.5, alpha=0.4) ax.text(wx + page_w/2, wy - 0.05, "WIREFRAME", color=DARK, fontsize=7, weight="bold", ha="center") # ---------- Sidebar face icons ----------------------------------------- def icon_smiley(ax, cx, cy, r=0.22): ax.add_patch(Circle((cx, cy), r, fc=AMBER, ec=DARK, lw=1.5)) ax.add_patch(Circle((cx - r * 0.35, cy + r * 0.18), r * 0.10, color=DARK)) ax.add_patch(Circle((cx + r * 0.35, cy + r * 0.18), r * 0.10, color=DARK)) # Smile arc — approximate with two short lines ax.plot([cx - r * 0.45, cx, cx + r * 0.45], [cy - r * 0.25, cy - r * 0.45, cy - r * 0.25], color=DARK, lw=1.6, solid_capstyle="round") def icon_wireframe_face(ax, cx, cy, r=0.22): ax.add_patch(Circle((cx, cy), r, fc="none", ec=DARK, lw=1.5)) # Brows ax.plot([cx - r * 0.55, cx - r * 0.20], [cy + r * 0.40, cy + r * 0.30], color=DARK, lw=1) ax.plot([cx + r * 0.20, cx + r * 0.55], [cy + r * 0.30, cy + r * 0.40], color=DARK, lw=1) # Eyes ax.plot([cx - r * 0.50, cx - r * 0.20], [cy + r * 0.05, cy + r * 0.05], color=DARK, lw=1) ax.plot([cx + r * 0.20, cx + r * 0.50], [cy + r * 0.05, cy + r * 0.05], color=DARK, lw=1) # Nose ax.plot([cx, cx], [cy, cy - r * 0.30], color=DARK, lw=1) # Mouth — straight (uncertain) ax.plot([cx - r * 0.30, cx + r * 0.30], [cy - r * 0.55, cy - r * 0.55], color=DARK, lw=1.2) # ---------- Build the figure ------------------------------------------- STEPS = [ { "num": "1", "verb": "WATCH THE", "rest": "FOUNDATION", "icon": icon_watch, "body": ( "A short video on FACS\n" "and why facial lie-\n" "detection is barely\n" "above chance." ), }, { "num": "2", "verb": "OPEN THE", "rest": "APP", "icon": icon_phone, "body": ( "One challenge across\n" "six slots: happy, sad,\n" "fear, disgust, anger,\n" "and surprise." ), }, { "num": "3", "verb": "POSE &", "rest": "SUBMIT", "icon": icon_webcam, "body": ( "For each slot capture\n" "your webcam image,\n" "pick the emotion, and\n" "submit." ), }, { "num": "4", "verb": "READ THE", "rest": "VERDICT", "icon": icon_verdict, "body": ( "The classifier's bar\n" "chart shows whether it\n" "AGREES or DISAGREES\n" "with you." ), }, { "num": "5", "verb": "DOWNLOAD YOUR", "rest": "EMOTIONMAP", "icon": icon_pdfs, "body": ( "Export the six-pose\n" "PDF in FACE format,\n" "or WIREFRAME for a\n" "face-free version." ), }, ] def build(): fig_w, fig_h = 17.0, 6.2 fig, ax = plt.subplots(figsize=(fig_w, fig_h)) ax.set_xlim(0, fig_w) ax.set_ylim(0, fig_h) ax.set_aspect("equal") ax.axis("off") fig.patch.set_facecolor("white") # 5 step columns + 6th sidebar. step_w = 2.55 margin_x = 0.35 base_y = 5.55 # number circle centre icon_y = 3.25 body_y = 1.85 for i, step in enumerate(STEPS): cx = margin_x + step_w / 2 + i * step_w # Number circle ax.add_patch(Circle((cx, base_y), 0.48, fc=DARK, ec=DARK)) ax.text(cx, base_y, step["num"], color="white", fontsize=28, weight="bold", ha="center", va="center") # Headline — two-line: amber verb, then dark rest ax.text(cx, base_y - 0.88, step["verb"], color=AMBER, fontsize=15, weight="bold", ha="center") ax.text(cx, base_y - 1.22, step["rest"], color=DARK, fontsize=15, weight="bold", ha="center") # Icon step["icon"](ax, cx, icon_y) # Body ax.text(cx, body_y, step["body"], color=DARK, fontsize=10, ha="center", va="top", linespacing=1.5) # ---- Sidebar (Porter & Ten Brinke factor) ---- sb_left = margin_x + 5 * step_w + 0.10 sb_w = fig_w - sb_left - margin_x sb_bottom = 0.70 sb_top = 5.95 ax.add_patch(FancyBboxPatch( (sb_left, sb_bottom), sb_w, sb_top - sb_bottom, boxstyle="round,pad=0.05", ec=AMBER, fc="white", lw=2.6, )) sb_cx = sb_left + sb_w / 2 ax.text(sb_cx, sb_top - 0.55, "THE PORTER", color=DARK, fontsize=14, weight="bold", ha="center") ax.text(sb_cx, sb_top - 0.90, "& TEN BRINKE", color=DARK, fontsize=14, weight="bold", ha="center") ax.text(sb_cx, sb_top - 1.25, "FACTOR", color=DARK, fontsize=14, weight="bold", ha="center") # Divider ax.plot([sb_left + 0.35, sb_left + sb_w - 0.35], [sb_top - 1.55, sb_top - 1.55], color=AMBER, lw=1.4) # Body ax.text(sb_cx, sb_top - 1.90, "Research shows that\n" "happiness is easier to\n" "fake than negative\n" "emotions. Does your\n" "EmotionMap match?", color=DARK, fontsize=10, ha="center", va="top", linespacing=1.5) # Faces — smiley + wireframe with question marks face_y = sb_bottom + 0.75 icon_smiley(ax, sb_cx - 0.55, face_y, r=0.32) icon_wireframe_face(ax, sb_cx + 0.50, face_y, r=0.32) ax.text(sb_cx - 0.13, face_y + 0.15, "?", color=DARK, fontsize=16, weight="bold") ax.text(sb_cx + 0.92, face_y + 0.15, "?", color=DARK, fontsize=16, weight="bold") # ---- Footer attribution ---- fig.text( 0.5, 0.015, "Created by Dr. Gordon Wright — A LittleMonkeyLab caper. " "Part of the Goldsmiths MSc in Psychology, Week 3 Part 4.", ha="center", fontsize=8, color="#888", style="italic", ) out_png = REPO_ROOT / "process-infographic.png" out_svg = REPO_ROOT / "process-infographic.svg" fig.savefig(out_png, dpi=200, bbox_inches="tight", facecolor="white") fig.savefig(out_svg, bbox_inches="tight", facecolor="white") plt.close(fig) print(f"Wrote {out_png}") print(f"Wrote {out_svg}") if __name__ == "__main__": build()