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.
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()
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 2: Axis labels
Axis labels and units make the measurement unambiguous.
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()
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 3: Units and scale clarity
Large values should be scaled and labeled so the reader knows what the numbers mean.
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()
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 4: Legend clarity
Multiple series need clear labels so readers can identify each line.
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()
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 5: Annotation context
Important events or takeaways should be visible when they explain the pattern.
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()
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 6: Uncertainty cues
Estimates should show uncertainty when the uncertainty matters.
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()
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()
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.
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()
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 8: Color accessibility
Charts should remain readable for people with color-vision deficiencies.
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()
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 9: Direct labeling
When lines are easy to label directly, readers should not have to bounce between the plot and legend.
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()
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 10: Avoid chartjunk
Decoration should not compete with the data.
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()
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 11: Too many categories
Too many categories make comparison slow and crowded.
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()
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 12: Sort categorical bars
Sorted bars make ranking and comparison faster.
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()
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 13: Scatter overplotting
Repeated or dense points should reveal density instead of hiding it.
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()
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 14: Decimal precision
Labels should avoid unnecessary decimals that add noise without adding meaning.
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()
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 15: Date axis formatting
Date ticks should be formatted at a readable interval.
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()
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 16: Visual economy
Use enough visual encoding to explain the data, but avoid redundant styling.
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()
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()
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.
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()
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 18: Suitable chart type
Categorical comparisons are easier to read as bars than as a line that implies sequence.
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()
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 19: Color map quality
Continuous color should use a perceptual scale and a labeled color bar.
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()
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 20: Avoid dual axes
Dual axes can imply relationships that come from scale choices rather than data.
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()
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 21: Area baseline
Filled areas should use an honest baseline because area size carries meaning.
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()
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 22: Aspect ratio sanity
Extreme aspect ratios can flatten or exaggerate trends.
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()
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 23: Histogram bin quality
A histogram needs enough bins to reveal the distribution shape without creating noise.
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()
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 24: Category color consistency
The same category should keep the same color throughout a chart.
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()
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 25: Diverging zero reference
Positive and negative values need a clear zero reference.
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()
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()