Python code examples

Each example is a complete, standalone Matplotlib snippet. The good-chart code highlights the lines that were added or corrected.

Completeness rules

Rules 1-6 check whether the chart contains enough information to be understood on its own.

Rule 1: Clear title

A title should tell the reader what the chart is about before they inspect the marks.

Bad example code
import matplotlib.pyplot as plt
 
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
revenue = [18, 20, 21, 23, 25, 28]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.plot(months, revenue, marker="o")
ax.set_xlabel("Month")
ax.set_ylabel("Revenue (k EUR)")
plt.show()
Rule 1 bad chart example
Good example code
import matplotlib.pyplot as plt
 
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
revenue = [18, 20, 21, 23, 25, 28]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.plot(months, revenue, marker="o")
ax.set_title("Monthly revenue increased from Jan to Jun 2026")
ax.set_xlabel("Month")
ax.set_ylabel("Revenue (k EUR)")
plt.show()
Rule 1 good chart example

Rule 2: Axis labels

Axis labels and units make the measurement unambiguous.

Bad example code
import matplotlib.pyplot as plt
 
ad_spend = [8, 12, 16, 20, 24, 30, 34, 38]
signups = [110, 140, 175, 205, 245, 310, 335, 390]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.scatter(ad_spend, signups)
ax.set_title("Campaign performance")
plt.show()
Rule 2 bad chart example
Good example code
import matplotlib.pyplot as plt
 
ad_spend = [8, 12, 16, 20, 24, 30, 34, 38]
signups = [110, 140, 175, 205, 245, 310, 335, 390]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.scatter(ad_spend, signups)
ax.set_title("Campaign performance")
ax.set_xlabel("Ad spend (k EUR)")
ax.set_ylabel("New signups")
plt.show()
Rule 2 good chart example

Rule 3: Units and scale clarity

Large values should be scaled and labeled so the reader knows what the numbers mean.

Bad example code
import matplotlib.pyplot as plt
 
years = [2022, 2023, 2024, 2025]
revenue = [1250000, 1480000, 1730000, 1910000]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.plot(years, revenue, marker="o")
ax.set_title("Revenue trend")
ax.set_ylabel("Revenue")
plt.show()
Rule 3 bad chart example
Good example code
import matplotlib.pyplot as plt
 
years = [2022, 2023, 2024, 2025]
revenue = [1250000, 1480000, 1730000, 1910000]
revenue_millions = [value / 1_000_000 for value in revenue]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.plot(years, revenue_millions, marker="o")
ax.set_title("Revenue trend")
ax.set_xlabel("Year")
ax.set_ylabel("Revenue (million EUR)")
plt.show()
Rule 3 good chart example

Rule 4: Legend clarity

Multiple series need clear labels so readers can identify each line.

Bad example code
import matplotlib.pyplot as plt
 
years = [2021, 2022, 2023, 2024, 2025]
series = {
    "Basic": [12, 14, 16, 18, 20],
    "Pro": [9, 13, 17, 22, 28],
    "Enterprise": [6, 8, 12, 17, 25],
}
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
for values in series.values():
    ax.plot(years, values, marker="o")
ax.set_title("Subscriptions by plan")
ax.set_ylabel("Subscriptions (k)")
plt.show()
Rule 4 bad chart example
Good example code
import matplotlib.pyplot as plt
 
years = [2021, 2022, 2023, 2024, 2025]
series = {
    "Basic": [12, 14, 16, 18, 20],
    "Pro": [9, 13, 17, 22, 28],
    "Enterprise": [6, 8, 12, 17, 25],
}
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
for label, values in series.items():
    ax.plot(years, values, marker="o", label=label)
ax.legend(loc="center left", bbox_to_anchor=(1.02, 0.5))
ax.set_title("Subscriptions by plan")
ax.set_ylabel("Subscriptions (k)")
plt.show()
Rule 4 good chart example

Rule 5: Annotation context

Important events or takeaways should be visible when they explain the pattern.

Bad example code
import matplotlib.pyplot as plt
 
weeks = [1, 2, 3, 4, 5, 6, 7, 8]
adoption = [12, 15, 18, 23, 34, 39, 43, 46]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.plot(weeks, adoption, marker="o")
ax.set_title("Feature adoption")
ax.set_xlabel("Week")
ax.set_ylabel("Adoption (%)")
plt.show()
Rule 5 bad chart example
Good example code
import matplotlib.pyplot as plt
 
weeks = [1, 2, 3, 4, 5, 6, 7, 8]
adoption = [12, 15, 18, 23, 34, 39, 43, 46]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.plot(weeks, adoption, marker="o")
ax.annotate("Onboarding email launched", xy=(5, 34), xytext=(3.2, 42), arrowprops={"arrowstyle": "->"})
ax.set_title("Feature adoption increased after onboarding email")
ax.set_xlabel("Week")
ax.set_ylabel("Adoption (%)")
plt.show()
Rule 5 good chart example

Rule 6: Uncertainty cues

Estimates should show uncertainty when the uncertainty matters.

Bad example code
import matplotlib.pyplot as plt
 
groups = ["A", "B", "C", "D"]
mean = [52, 57, 61, 55]
error = [4, 7, 3, 6]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.bar(groups, mean)
ax.set_title("Average test result")
ax.set_ylabel("Score")
plt.show()
Rule 6 bad chart example
Good example code
import matplotlib.pyplot as plt
 
groups = ["A", "B", "C", "D"]
mean = [52, 57, 61, 55]
error = [4, 7, 3, 6]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.bar(groups, mean, yerr=error, capsize=6)
ax.set_title("Average test result with uncertainty")
ax.set_ylabel("Score")
plt.show()
Rule 6 good chart example

Readability rules

Rules 7-16 check whether the chart can be read, scanned, and compared without unnecessary effort.

Rule 7: Readable labels

Long labels should not collide or force the reader to decode a crowded axis.

Bad example code
import matplotlib.pyplot as plt
 
segments = [
    "Returning enterprise customers",
    "New small business customers",
    "One-time promotional buyers",
    "Students using education plan",
    "Trial users awaiting onboarding",
]
counts = [180, 145, 96, 125, 72]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.bar(segments, counts)
ax.set_title("Customers by segment")
plt.show()
Rule 7 bad chart example
Good example code
import matplotlib.pyplot as plt
import textwrap
 
segments = [
    "Returning enterprise customers",
    "New small business customers",
    "One-time promotional buyers",
    "Students using education plan",
    "Trial users awaiting onboarding",
]
counts = [180, 145, 96, 125, 72]
labels = [textwrap.fill(s, 22) for s in segments]
pairs = sorted(zip(counts, labels))
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.barh([label for _, label in pairs], [count for count, _ in pairs])
ax.set_title("Customers by segment")
ax.set_xlabel("Customers")
plt.show()
Rule 7 good chart example

Rule 8: Color accessibility

Charts should remain readable for people with color-vision deficiencies.

Bad example code
import matplotlib.pyplot as plt
 
segments = ["Home", "Food", "Auto", "Health", "Shopping"]
spend = [42, 26, 18, 12, 8]
colors = ["#d7191c", "#fdae61", "#ffffbf", "#a6d96a", "#1a9641"]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.bar(segments, spend, color=colors)
ax.set_title("Household spend by category")
ax.set_ylabel("Spend (%)")
plt.show()
Rule 8 bad chart example
Good example code
import matplotlib.pyplot as plt
 
segments = ["Home", "Food", "Auto", "Health", "Shopping"]
spend = [42, 26, 18, 12, 8]
colors = ["#0072B2", "#E69F00", "#56B4E9", "#009E73", "#CC79A7"]
hatches = ["", "//", "\\", "..", "xx"]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
bars = ax.bar(segments, spend, color=colors)
for bar, hatch in zip(bars, hatches):
    bar.set_hatch(hatch)
ax.set_title("Household spend by category")
ax.set_ylabel("Spend (%)")
plt.show()
Rule 8 good chart example

Rule 9: Direct labeling

When lines are easy to label directly, readers should not have to bounce between the plot and legend.

Bad example code
import matplotlib.pyplot as plt
 
years = [2021, 2022, 2023, 2024, 2025]
series = {
    "Basic": [12, 14, 16, 18, 20],
    "Pro": [9, 13, 17, 22, 28],
    "Enterprise": [6, 8, 12, 17, 25],
    "Education": [4, 7, 10, 13, 19],
}
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
for label, values in series.items():
    ax.plot(years, values, marker="o", label=label)
ax.legend()
ax.set_title("Subscriptions by plan")
ax.set_ylabel("Subscriptions (k)")
plt.show()
Rule 9 bad chart example
Good example code
import matplotlib.pyplot as plt
 
years = [2021, 2022, 2023, 2024, 2025]
series = {
    "Basic": [12, 14, 16, 18, 20],
    "Pro": [9, 13, 17, 22, 28],
    "Enterprise": [6, 8, 12, 17, 25],
    "Education": [4, 7, 10, 13, 19],
}
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
for label, values in series.items():
    ax.plot(years, values, marker="o")
    ax.text(years[-1] + 0.05, values[-1], label, va="center")
ax.set_xlim(2021, 2025.9)
ax.set_title("Subscriptions by plan")
ax.set_ylabel("Subscriptions (k)")
plt.show()
Rule 9 good chart example

Rule 10: Avoid chartjunk

Decoration should not compete with the data.

Bad example code
import matplotlib.pyplot as plt
 
regions = ["North", "South", "East", "West"]
profit = [18, 12, 15, 21]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.set_facecolor("#f3d9a5")
ax.bar(regions, profit, edgecolor="black", linewidth=2)
ax.grid(True, axis="both")
ax.set_title("!!! PROFIT !!!")
ax.set_ylabel("Profit (k EUR)")
plt.show()
Rule 10 bad chart example
Good example code
import matplotlib.pyplot as plt
 
regions = ["North", "South", "East", "West"]
profit = [18, 12, 15, 21]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.bar(regions, profit)
ax.grid(True, axis="y")
ax.set_title("Profit by region")
ax.set_ylabel("Profit (k EUR)")
plt.show()
Rule 10 good chart example

Rule 11: Too many categories

Too many categories make comparison slow and crowded.

Bad example code
import matplotlib.pyplot as plt
 
categories = [f"C{i}" for i in range(1, 19)]
values = [34, 28, 26, 22, 20, 18, 16, 12, 11, 10, 9, 8, 7, 6, 5, 5, 4, 3]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.bar(categories, values)
ax.set_title("Requests by category")
ax.set_ylabel("Requests")
plt.show()
Rule 11 bad chart example
Good example code
import matplotlib.pyplot as plt
 
categories = [f"C{i}" for i in range(1, 19)]
values = [34, 28, 26, 22, 20, 18, 16, 12, 11, 10, 9, 8, 7, 6, 5, 5, 4, 3]
top_labels = categories[:7] + ["Other"]
top_values = values[:7] + [sum(values[7:])]
pairs = sorted(zip(top_values, top_labels))
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.barh([label for _, label in pairs], [value for value, _ in pairs])
ax.set_title("Requests by category")
ax.set_xlabel("Requests")
plt.show()
Rule 11 good chart example

Rule 12: Sort categorical bars

Sorted bars make ranking and comparison faster.

Bad example code
import matplotlib.pyplot as plt
 
teams = ["Ops", "Sales", "Support", "Product", "Finance"]
hours = [42, 18, 31, 24, 36]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.barh(teams, hours)
ax.set_title("Average resolution time")
ax.set_xlabel("Hours")
plt.show()
Rule 12 bad chart example
Good example code
import matplotlib.pyplot as plt
 
teams = ["Ops", "Sales", "Support", "Product", "Finance"]
hours = [42, 18, 31, 24, 36]
pairs = sorted(zip(hours, teams))
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.barh([team for _, team in pairs], [hour for hour, _ in pairs])
ax.set_title("Average resolution time")
ax.set_xlabel("Hours")
plt.show()
Rule 12 good chart example

Rule 13: Scatter overplotting

Repeated or dense points should reveal density instead of hiding it.

Bad example code
import matplotlib.pyplot as plt
import numpy as np
 
rng = np.random.default_rng(42)
centers = np.array([[2, 2], [3, 3], [4, 2.5], [4.5, 4]])
points = np.repeat(centers, [35, 45, 25, 30], axis=0)
points = points + rng.normal(0, 0.04, size=(135, 2))
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.scatter(points[:, 0], points[:, 1], s=28)
ax.set_xlabel("Value score")
ax.set_ylabel("Satisfaction score")
plt.show()
Rule 13 bad chart example
Good example code
import matplotlib.pyplot as plt
import numpy as np
 
centers = np.array([[2, 2], [3, 3], [4, 2.5], [4.5, 4]])
counts = np.array([35, 45, 25, 30])
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.scatter(centers[:, 0], centers[:, 1], s=counts * 18, alpha=0.55, edgecolor="black")
ax.set_xlabel("Value score")
ax.set_ylabel("Satisfaction score")
ax.set_title("Point size shows repeated observations")
plt.show()
Rule 13 good chart example

Rule 14: Decimal precision

Labels should avoid unnecessary decimals that add noise without adding meaning.

Bad example code
import matplotlib.pyplot as plt
 
products = ["A", "B", "C", "D"]
conversion = [12.3421, 11.9874, 13.2251, 12.7718]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
bars = ax.bar(products, conversion)
ax.bar_label(bars, fmt="%.4f%%")
ax.set_ylim(0, 15)
ax.set_ylabel("Conversion (%)")
plt.show()
Rule 14 bad chart example
Good example code
import matplotlib.pyplot as plt
 
products = ["A", "B", "C", "D"]
conversion = [12.3421, 11.9874, 13.2251, 12.7718]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
bars = ax.bar(products, conversion)
ax.bar_label(bars, fmt="%.1f%%")
ax.set_ylim(0, 15)
ax.set_ylabel("Conversion (%)")
plt.show()
Rule 14 good chart example

Rule 15: Date axis formatting

Date ticks should be formatted at a readable interval.

Bad example code
import matplotlib.pyplot as plt
import numpy as np
 
dates = [np.datetime64("2026-01-01") + np.timedelta64(i * 14, "D") for i in range(10)]
visits = [120, 135, 128, 150, 162, 158, 170, 181, 190, 205]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.plot(dates, visits, marker="o")
ax.set_title("Website visits")
ax.set_ylabel("Visits (k)")
plt.show()
Rule 15 bad chart example
Good example code
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import numpy as np
 
dates = [np.datetime64("2026-01-01") + np.timedelta64(i * 14, "D") for i in range(10)]
visits = [120, 135, 128, 150, 162, 158, 170, 181, 190, 205]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.plot(dates, visits, marker="o")
ax.set_title("Website visits")
ax.set_ylabel("Visits (k)")
ax.xaxis.set_major_locator(mdates.MonthLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter("%b %Y"))
ax.tick_params(axis="x", rotation=30)
plt.show()
Rule 15 good chart example

Rule 16: Visual economy

Use enough visual encoding to explain the data, but avoid redundant styling.

Bad example code
import matplotlib.pyplot as plt
 
products = ["A", "B", "C", "D", "E", "F"]
values = [18, 25, 22, 31, 27, 20]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.bar(products, values, hatch="//", edgecolor="black")
ax.plot(products, values, marker="D", color="red")
ax.grid(True, axis="both")
ax.set_title("Product sales with redundant styling")
plt.show()
Rule 16 bad chart example
Good example code
import matplotlib.pyplot as plt
 
products = ["A", "B", "C", "D", "E", "F"]
values = [18, 25, 22, 31, 27, 20]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.bar(products, values)
ax.grid(True, axis="y")
ax.set_title("Product sales")
ax.set_ylabel("Sales (k EUR)")
plt.show()
Rule 16 good chart example

Integrity rules

Rules 17-25 check whether the chart avoids misleading scale, encoding, or comparison choices.

Rule 17: Appropriate scale

For bars, a truncated baseline can exaggerate small differences.

Bad example code
import matplotlib.pyplot as plt
 
branches = ["North", "Central", "South"]
satisfaction = [82, 86, 88]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.bar(branches, satisfaction)
ax.set_ylim(80, 90)
ax.set_title("Customer satisfaction by branch")
ax.set_ylabel("Satisfied customers (%)")
plt.show()
Rule 17 bad chart example
Good example code
import matplotlib.pyplot as plt
 
branches = ["North", "Central", "South"]
satisfaction = [82, 86, 88]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.bar(branches, satisfaction)
ax.set_ylim(0, 100)
ax.set_title("Customer satisfaction by branch")
ax.set_ylabel("Satisfied customers (%)")
plt.show()
Rule 17 good chart example

Rule 18: Suitable chart type

Categorical comparisons are easier to read as bars than as a line that implies sequence.

Bad example code
import matplotlib.pyplot as plt
 
products = ["Shoes", "Bags", "Watches", "Perfume", "Jewelry"]
sales = [54, 31, 46, 28, 62]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.plot(products, sales, marker="o")
ax.set_title("Sales by product category")
ax.set_ylabel("Sales (k EUR)")
plt.show()
Rule 18 bad chart example
Good example code
import matplotlib.pyplot as plt
 
products = ["Shoes", "Bags", "Watches", "Perfume", "Jewelry"]
sales = [54, 31, 46, 28, 62]
pairs = sorted(zip(sales, products))
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.barh([p for _, p in pairs], [s for s, _ in pairs])
ax.set_title("Sales by product category")
ax.set_xlabel("Sales (k EUR)")
plt.show()
Rule 18 good chart example

Rule 19: Color map quality

Continuous color should use a perceptual scale and a labeled color bar.

Bad example code
import matplotlib.pyplot as plt
import numpy as np
 
heat = np.outer(np.linspace(0, 1, 12), np.linspace(0.2, 1, 12))
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
image = ax.imshow(heat, cmap="rainbow")
ax.set_title("Demand intensity")
fig.colorbar(image, ax=ax)
plt.show()
Rule 19 bad chart example
Good example code
import matplotlib.pyplot as plt
import numpy as np
 
heat = np.outer(np.linspace(0, 1, 12), np.linspace(0.2, 1, 12))
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
image = ax.imshow(heat, cmap="viridis")
ax.set_title("Demand intensity")
colorbar = fig.colorbar(image, ax=ax)
colorbar.set_label("Orders per store")
plt.show()
Rule 19 good chart example

Rule 20: Avoid dual axes

Dual axes can imply relationships that come from scale choices rather than data.

Bad example code
import matplotlib.pyplot as plt
 
months = [1, 2, 3, 4, 5, 6]
revenue = [1.2, 1.5, 1.8, 2.1, 2.4, 2.7]
churn = [9, 8, 7, 6, 5, 4]
 
fig, ax1 = plt.subplots(figsize=(6.4, 3.8))
ax2 = ax1.twinx()
ax1.plot(months, revenue, marker="o", label="Revenue")
ax2.plot(months, churn, marker="o", color="red", label="Churn")
ax1.set_ylabel("Revenue (M EUR)")
ax2.set_ylabel("Churn (%)")
plt.show()
Rule 20 bad chart example
Good example code
import matplotlib.pyplot as plt
 
months = [1, 2, 3, 4, 5, 6]
revenue = [1.2, 1.5, 1.8, 2.1, 2.4, 2.7]
churn = [9, 8, 7, 6, 5, 4]
 
fig, axes = plt.subplots(2, 1, figsize=(6.4, 4.2), sharex=True)
axes[0].plot(months, revenue, marker="o")
axes[0].set_ylabel("Revenue (M EUR)")
axes[1].plot(months, churn, marker="o", color="red")
axes[1].set_ylabel("Churn (%)")
axes[1].set_xlabel("Month")
plt.show()
Rule 20 good chart example

Rule 21: Area baseline

Filled areas should use an honest baseline because area size carries meaning.

Bad example code
import matplotlib.pyplot as plt
 
years = [2020, 2021, 2022, 2023, 2024, 2025]
share = [62, 64, 66, 67, 69, 70]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.fill_between(years, share, 60)
ax.plot(years, share, marker="o")
ax.set_ylim(60, 72)
ax.set_ylabel("Share (%)")
plt.show()
Rule 21 bad chart example
Good example code
import matplotlib.pyplot as plt
 
years = [2020, 2021, 2022, 2023, 2024, 2025]
share = [62, 64, 66, 67, 69, 70]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.fill_between(years, share, 0)
ax.plot(years, share, marker="o")
ax.set_ylim(0, 100)
ax.set_ylabel("Share (%)")
plt.show()
Rule 21 good chart example

Rule 22: Aspect ratio sanity

Extreme aspect ratios can flatten or exaggerate trends.

Bad example code
import matplotlib.pyplot as plt
 
quarters = [1, 2, 3, 4, 5, 6, 7, 8]
index = [12, 14, 15, 16, 18, 19, 21, 22]
 
fig, ax = plt.subplots(figsize=(7.8, 2.2))
ax.plot(quarters, index, marker="o")
ax.set_title("Compressed aspect ratio hides the trend")
plt.show()
Rule 22 bad chart example
Good example code
import matplotlib.pyplot as plt
 
quarters = [1, 2, 3, 4, 5, 6, 7, 8]
index = [12, 14, 15, 16, 18, 19, 21, 22]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.plot(quarters, index, marker="o")
ax.set_title("Balanced aspect ratio shows the trend clearly")
plt.show()
Rule 22 good chart example

Rule 23: Histogram bin quality

A histogram needs enough bins to reveal the distribution shape without creating noise.

Bad example code
import matplotlib.pyplot as plt
import numpy as np
 
rng = np.random.default_rng(42)
scores = rng.normal(72, 9, 420)
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.hist(scores, bins=3)
ax.set_title("Exam scores")
ax.set_xlabel("Score")
ax.set_ylabel("Students")
plt.show()
Rule 23 bad chart example
Good example code
import matplotlib.pyplot as plt
import numpy as np
 
rng = np.random.default_rng(42)
scores = rng.normal(72, 9, 420)
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.hist(scores, bins=18)
ax.set_title("Exam scores")
ax.set_xlabel("Score")
ax.set_ylabel("Students")
plt.show()
Rule 23 good chart example

Rule 24: Category color consistency

The same category should keep the same color throughout a chart.

Bad example code
import matplotlib.pyplot as plt
 
quarters = ["Q1", "Q2", "Q3"]
desktop = [42, 45, 48]
mobile = [31, 34, 38]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.plot(quarters, desktop, marker="o", color="blue", label="Desktop")
ax.plot(quarters, mobile, marker="o", color="orange", label="Mobile")
ax.scatter(["Q2"], [45], color="orange", s=90)
ax.scatter(["Q2"], [34], color="blue", s=90)
ax.legend()
plt.show()
Rule 24 bad chart example
Good example code
import matplotlib.pyplot as plt
 
quarters = ["Q1", "Q2", "Q3"]
desktop = [42, 45, 48]
mobile = [31, 34, 38]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.plot(quarters, desktop, marker="o", color="blue", label="Desktop")
ax.plot(quarters, mobile, marker="o", color="orange", label="Mobile")
ax.legend()
ax.set_title("Traffic by device")
ax.set_ylabel("Sessions (k)")
plt.show()
Rule 24 good chart example

Rule 25: Diverging zero reference

Positive and negative values need a clear zero reference.

Bad example code
import matplotlib.pyplot as plt
 
departments = ["Ops", "Sales", "Support", "Product"]
delta = [-8, 12, -5, 9]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.bar(departments, delta)
ax.set_title("Change vs target")
ax.set_ylabel("Change (%)")
plt.show()
Rule 25 bad chart example
Good example code
import matplotlib.pyplot as plt
 
departments = ["Ops", "Sales", "Support", "Product"]
delta = [-8, 12, -5, 9]
colors = ["#b42318" if value < 0 else "#2f855a" for value in delta]
 
fig, ax = plt.subplots(figsize=(6.4, 3.8))
ax.bar(departments, delta, color=colors)
ax.axhline(0, color="black", linewidth=1.2)
ax.set_title("Change vs target")
ax.set_ylabel("Change (%)")
plt.show()
Rule 25 good chart example