Spaces:
Sleeping
Sleeping
| """ | |
| 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() | |