Basic Scatter Plot — Pygal

A fundamental 2D scatter plot that displays the relationship between two numeric variables by plotting points on a Cartesian coordinate system. This visualization is essential for exploring correlations, identifying patterns, detecting outliers, and understanding the distribution of paired data points.

Basic Scatter Plot rendered with Pygal

Python source (Pygal)

""" anyplot.ai
scatter-basic: Basic Scatter Plot
Library: pygal 3.1.3 | Python 3.13.14
Quality: 87/100 | Updated: 2026-06-25
"""

import os
import sys


# Script filename shadows the installed `pygal` package when run as `python pygal.py`;
# dropping the script directory from sys.path lets the real package resolve.
sys.path.pop(0)

import numpy as np
import pygal
from pygal.style import Style


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint palette — canonical order; semantic positions used for pass/fail split
# First series = brand green (#009E73), fail = matte red (#AE3030)
IMPRINT = (
    "#009E73",  # position 1: brand green → Passing
    "#AE3030",  # position 5: matte red (semantic anchor for bad/fail) → Below 70%
    "#C475FD",
    "#4467A3",
    "#BD8233",
    "#2ABCCD",
    "#954477",
    "#99B314",
)

# Data — study hours vs exam scores, moderate positive correlation
np.random.seed(42)
n = 180
study_hours = np.random.uniform(1.5, 13.5, n)
exam_scores = study_hours * 4.8 + np.random.normal(0, 6.5, n) + 26
exam_scores = np.clip(exam_scores, 20, 100)

# Visual hierarchy: split at conventional 70% passing threshold to guide the
# viewer's eye and convey the "hours → outcome" narrative beyond a raw cloud.
PASSING = 70.0
above = [(float(h), float(s)) for h, s in zip(study_hours, exam_scores, strict=True) if s >= PASSING]
below = [(float(h), float(s)) for h, s in zip(study_hours, exam_scores, strict=True) if s < PASSING]

font = "DejaVu Sans, Helvetica, Arial, sans-serif"

custom_style = Style(
    background=PAGE_BG,
    plot_background=PAGE_BG,
    foreground=INK_SOFT,
    foreground_strong=INK,
    foreground_subtle=INK_MUTED,
    colors=IMPRINT,
    font_family=font,
    title_font_family=font,
    label_font_family=font,
    major_label_font_family=font,
    legend_font_family=font,
    tooltip_font_family=font,
    title_font_size=66,
    label_font_size=56,
    major_label_font_size=44,
    legend_font_size=44,
    tooltip_font_size=36,
    value_font_size=36,
    opacity=0.72,
    opacity_hover=0.95,
    stroke_opacity=1,
    stroke_opacity_hover=1,
)

chart = pygal.XY(
    width=3200,
    height=1800,
    style=custom_style,
    title="scatter-basic · python · pygal · anyplot.ai",
    x_title="Study Hours per Week (hrs)",
    y_title="Exam Score (%)",
    stroke=False,
    dots_size=12,
    show_legend=True,
    legend_at_bottom=True,
    legend_at_bottom_columns=2,
    legend_box_size=32,
    show_x_guides=True,
    show_y_guides=True,
    x_value_formatter=lambda v: f"{v:.0f}",
    value_formatter=lambda v: f"{v:.0f}%",
    range=(15, 100),
    xrange=(1, 14),
    x_labels_major_count=7,
    y_labels_major_count=9,
    margin=60,
    print_values=False,
    js=[],
)

chart.add("Passing (≥ 70%)", above)
chart.add("Below 70%", below)

chart.render_to_png(f"plot-{THEME}.png")
with open(f"plot-{THEME}.html", "wb") as f:
    f.write(chart.render())

Part of Basic Scatter Plot on anyplot.ai.

Other implementations