Matplotlib.pyplot.barh() in Python: A Practical, Production-Ready Guide

I’ve built a lot of charts that looked fine until a real reader tried to compare values quickly. That’s where horizontal bar charts save the day. When category names get long or you want a ranked list that reads like a scoreboard, matplotlib.pyplot.barh() is usually the cleanest answer. I reach for it when I need clarity at a glance, not just pretty visuals.

You’ll see how barh() works from the ground up, how I choose parameters in real projects, and how to avoid the sneaky mistakes that make plots misleading. I’ll also show practical patterns I use in 2026 workflows, including annotation, ordering, styling, and integration with pandas. The goal is simple: you should be able to build a horizontal bar chart that communicates the right story, looks professional, and behaves well when data changes.

If you already know basic Matplotlib, this will feel familiar but deeper. If you’re new, don’t worry—I’ll keep it plain and show you code that runs as-is.

Why horizontal bars win for ranked categories

When I need to compare discrete categories, a bar chart is my default. Horizontal bars are especially strong for rankings and long labels. Your eye reads left-to-right magnitude quickly, and the text can stay horizontal instead of tilted.

A common case: course enrollment, product sales, bug counts per service, or time spent per task. If the category names are long—say “International Expansion Strategy” or “Authentication/Authorization Service”—a vertical bar chart forces rotated labels that slow readers down. With barh(), you just place categories on the y-axis and values on the x-axis, and the labels remain readable.

I also like the mental model: the width of each bar is the value, so the story is literally “how far does it go to the right.” It’s like a ruler. This makes barh() ideal for ranked lists, dashboards, and quick comparisons.

One more subtle advantage: horizontal bars invite sorting. People expect the longest bar at the top. That expectation is easy to meet by sorting data, then calling invert_yaxis() if needed so the highest value sits first.

barh() signature and how I interpret it

Here’s the signature I keep in my head:

matplotlib.pyplot.barh(y, width, height=0.8, left=None, , align=‘center‘, *kwargs)

I translate that into a short checklist:

  • y: category positions or labels. If you pass strings, Matplotlib maps them to positions for you.
  • width: the bar lengths. This is your actual metric.
  • height: bar thickness (default 0.8). Smaller values create space between bars.
  • left: start position along x. Default is 0, but it’s key for stacked bars or offset baselines.
  • align: ‘center‘ or ‘edge‘. I stick to center unless I’m doing precise alignment with a grid.
  • kwargs: style controls like color, edgecolor, linewidth, alpha, and label.

I treat barh() as a little layout engine. You give it coordinates and sizes; it draws rectangles. The rest of your plotting job is to give those rectangles context: labels, ticks, gridlines, and annotations.

A simple but important point: barh() uses y for categories and width for values. If you pass the same data used for a vertical bar chart, you’ll probably swap what goes where. This sounds obvious, but it’s the most common bug I see when people go from bar() to barh().

A clean baseline example you can reuse

This pattern is my default for a clean, readable chart. It includes ordering, spacing, and readable labels. I also avoid extra axes clutter unless I need it.

import matplotlib.pyplot as plt

Data

courses = ["Python", "Java", "C", "C++"]

students = [35, 30, 20, 15]

Sort by value so the plot reads top-to-bottom

pairs = sorted(zip(students, courses), reverse=True)

studentssorted, coursessorted = zip(*pairs)

Plot

fig, ax = plt.subplots(figsize=(8, 4.5))

ax.barh(coursessorted, studentssorted, color="#8B1A1A")

Labels

ax.set_xlabel("Number of students enrolled")

ax.set_ylabel("Course")

ax.set_title("Enrollment by course")

Put the largest at the top

ax.invert_yaxis()

plt.tight_layout()

plt.show()

Why this layout works:

  • Sorting gives a ranked view.
  • invert_yaxis() places the top item first, which matches how we read lists.
  • A modest figure size keeps bars readable without wasting space.

I use this pattern as a template and then add styling or annotations based on the audience.

Styling and readability: small moves, big impact

A good bar chart is more than bars. It’s about legibility and visual hierarchy. Here’s how I decide what to adjust.

Spacing and bar thickness

The default height of 0.8 works for most cases, but if you have many categories, reduce height to prevent overlap.

ax.barh(coursessorted, studentssorted, height=0.6, color="#4C6EF5")

I also increase figure height when there are many items. A rough rule: 0.4–0.5 inches per bar is usually enough for text legibility.

Colors and contrast

I keep colors minimal unless I’m encoding another variable. If there’s a single series, I choose one muted color and use darker text for labels. For multiple series, I use a small palette and a legend.

Avoid low-contrast colors for bars and labels. The most common mistake is using light-colored bars on a white background with thin edges. If you want a softer color, keep the edge line visible:

ax.barh(coursessorted, studentssorted, color="#A3BFFA", edgecolor="#2F4B7C", linewidth=0.8)

Gridlines and axes

I usually keep x-axis gridlines and remove spines. Gridlines help people read exact values without making the chart heavy.

for spine in ["top", "right", "left", "bottom"]:

ax.spines[spine].set_visible(False)

ax.xaxis.setticksposition("none")

ax.yaxis.setticksposition("none")

ax.grid(axis="x", linestyle="--", linewidth=0.6, alpha=0.4)

This creates a clean, poster-like look without sacrificing readability.

Annotation for exact values

If you’re presenting to a non-technical audience, direct labels beat a visible axis. I annotate bars with their values.

for bar in ax.patches:

value = bar.get_width()

y = bar.gety() + bar.getheight() / 2

ax.text(value + 0.5, y, f"{value}", va="center", fontsize=9, color="#444")

Notice I set the x offset (+ 0.5) so labels sit just outside the bar. Adjust that based on your scale.

Real-world patterns I use in production

Charts in real systems are rarely “just one list.” You often need multiple series, stacked totals, or dynamic categories. Here are patterns I use a lot.

1) Grouped horizontal bars

If you want to compare two metrics per category (say, 2025 vs 2026), grouped bars are clearer than stacking because people can see both values without mental subtraction.

import numpy as np

import matplotlib.pyplot as plt

categories = ["Auth", "Search", "Payments", "Reports"]

q1 = [120, 80, 150, 60]

q2 = [140, 95, 130, 75]

y = np.arange(len(categories))

height = 0.35

fig, ax = plt.subplots(figsize=(8, 4.5))

ax.barh(y - height/2, q1, height=height, label="Q1", color="#1F77B4")

ax.barh(y + height/2, q2, height=height, label="Q2", color="#FF7F0E")

ax.set_yticks(y)

ax.set_yticklabels(categories)

ax.set_xlabel("Incidents")

ax.set_title("Incidents by service")

ax.legend()

ax.invert_yaxis()

plt.tight_layout()

plt.show()

Key move: I create numeric y-positions with np.arange so I can offset bars precisely. That’s hard to do with string labels alone.

2) Stacked horizontal bars

Stacked bars show totals while also showing composition. This is perfect for budget breakdowns or time allocation. I use left to position each segment.

categories = ["Design", "Build", "Test", "Deploy"]

engineering = [20, 35, 15, 10]

product = [10, 15, 5, 5]

fig, ax = plt.subplots(figsize=(8, 4.5))

ax.barh(categories, engineering, label="Engineering", color="#2CA02C")

ax.barh(categories, product, left=engineering, label="Product", color="#98DF8A")

ax.set_xlabel("Hours")

ax.set_title("Project time allocation")

ax.legend()

ax.invert_yaxis()

plt.tight_layout()

plt.show()

Important: left should be the cumulative total of prior segments. If you add a third series, the left becomes engineering + product.

3) Positive and negative values

When values can be negative—say, profits vs losses—barh() handles it cleanly. I set the axis at zero and use colors to distinguish direction.

teams = ["North", "South", "East", "West"]

profit = [12, -5, 9, -2]

colors = ["#2CA02C" if v >= 0 else "#D62728" for v in profit]

fig, ax = plt.subplots(figsize=(8, 4.5))

ax.barh(teams, profit, color=colors)

ax.axvline(0, color="#333", linewidth=0.8)

ax.set_xlabel("Profit (M$)")

ax.set_title("Regional profit vs loss")

ax.invert_yaxis()

plt.tight_layout()

plt.show()

This makes the direction of each bar instantly obvious.

4) With pandas pipelines

If your data is already in pandas, you can sort and select quickly, then pass arrays to barh().

import pandas as pd

import matplotlib.pyplot as plt

Example dataset

rows = [

{"service": "Auth", "latency_ms": 120},

{"service": "Search", "latency_ms": 95},

{"service": "Payments", "latency_ms": 180},

{"service": "Reports", "latency_ms": 140},

]

df = pd.DataFrame(rows)

Sort descending and plot

sorteddf = df.sortvalues("latency_ms", ascending=False)

fig, ax = plt.subplots(figsize=(8, 4.5))

ax.barh(sorteddf["service"], sorteddf["latency_ms"], color="#6C757D")

ax.set_xlabel("Latency (ms)")

ax.set_title("P95 latency by service")

ax.invert_yaxis()

plt.tight_layout()

plt.show()

This keeps data prep separate from plotting, which I find easier to test and maintain.

When to use barh() and when I avoid it

I use barh() for discrete categories, ranked lists, and cases where labels are long. But I avoid it when the chart’s purpose is time series or continuous trends. That’s a line chart’s job.

Here’s my quick decision table for 2026 workflows:

Scenario

Best choice

Why I choose it —

— Long category labels

barh

Horizontal labels remain readable Time series trends

plot

Bars hide continuity Composition of totals

stacked barh

Segment size shows part-to-whole Very large number of categories

barh with filtering

I show top N, then summarize the rest Distribution of many points

histogram or box plot

Bar chart would be misleading

If you’re unsure, ask what question you want to answer. If it’s “Which categories are larger,” go with barh(). If it’s “How does this change over time,” pick a line chart.

Common mistakes I see (and how you should avoid them)

I see the same issues over and over. Here’s how I prevent them.

Mistake 1: Not sorting by value

When data comes in arbitrary order, your chart reads like random noise. Sort by value unless the order carries meaning (like a custom workflow order).

Tip: Sort and then invert_yaxis() so the biggest bar is at the top.

Mistake 2: Overcrowded labels

If you plot too many categories, labels overlap or become unreadable. In practice, I show the top 10–20 categories and aggregate the rest into “Other.”

Mistake 3: Misusing left

For stacked bars, left must be cumulative. A frequent error is passing the second series directly, which shifts bars incorrectly and misrepresents totals.

Mistake 4: Using too many colors

Multiple colors can imply multiple meanings. If you only have one series, keep one color. If you do color by another variable, explain it in a legend or annotations.

Mistake 5: Forgetting axis labels

This sounds basic, but it happens. If your chart can’t be understood without context, it’s not doing its job. I always label axes and add a title or caption that states the point.

Mistake 6: Unclear units

If values are in milliseconds, dollars, or percentages, show that in the axis label or annotations. I also prefer “ms” and “$” in the label rather than implicit context.

Performance and scaling considerations

Matplotlib is fast enough for most charts, but I still watch for slowdowns in dashboards or report generation. In my experience, a single barh() plot with up to 200 bars is quick and smooth on a typical 2026 developer laptop. If you need thousands of bars, the chart stops being readable anyway, so performance isn’t the real bottleneck—legibility is.

For report pipelines, I recommend:

  • Pre-aggregate data in pandas or SQL before plotting.
  • Use vectorized operations for sorting and filtering.
  • Keep styles consistent so you don’t spend cycles on customization for each plot.

If you’re generating many plots in a loop, create the figure once and update it, or at least close figures to avoid memory buildup:

for i, dataset in enumerate(datasets):

fig, ax = plt.subplots()

ax.barh(dataset["label"], dataset["value"])

plt.savefig(f"plot_{i}.png")

plt.close(fig)

This prevents a common memory leak pattern where figures stack up in the background.

Modern workflows: I pair barh() with AI-assisted analysis

In 2026, I often use AI tooling to summarize large datasets into categories, then visualize with barh(). The key is to keep the plot faithful to the source data. If an AI model clusters categories or generates labels, I always verify the counts before plotting.

A practical workflow I use:

1) Use a data notebook or pipeline to compute aggregates.

2) Run a lightweight check: totals, min/max, and top 5 categories.

3) Plot with barh() and annotate key values.

4) Save the plot and include the data table in the report for transparency.

This keeps charts aligned with source data and avoids “pretty but wrong” outputs.

If you’re building a report for stakeholders, I also recommend exporting a simple CSV alongside the plot. That gives readers a way to verify the values without asking you for raw data later.

Deep dive: y positions, labels, and ordering strategies

Most beginner examples pass string labels directly to barh() and call it a day. That’s fine for small charts, but once you need precise control—grouping, spacing, or custom order—you’ll want to understand how Matplotlib maps labels to positions.

How barh() maps categories to positions

When you pass a list of strings as y, Matplotlib internally maps them to positions 0..N-1 in the order you supply. This means ordering is purely input-driven. If you want a specific order, you must reorder your data first.

I often do this explicitly to prevent confusion:

labels = ["Critical", "High", "Medium", "Low"]

values = [12, 30, 18, 5]

fig, ax = plt.subplots()

ax.barh(labels, values)

ax.invert_yaxis()

If I need to align grouped or stacked data, I switch to numeric positions with np.arange. That gives me full control:

import numpy as np

labels = ["Critical", "High", "Medium", "Low"]

values = [12, 30, 18, 5]

y = np.arange(len(labels))

fig, ax = plt.subplots()

ax.barh(y, values)

ax.set_yticks(y)

ax.set_yticklabels(labels)

ax.invert_yaxis()

Ordering by multiple fields

Sometimes I sort by a secondary field—like sorting by region, then by value. I handle that in pandas or a structured list before plotting. Here’s a small pattern that keeps things readable:

import pandas as pd

rows = [

{"team": "A", "priority": "High", "bugs": 18},

{"team": "B", "priority": "High", "bugs": 11},

{"team": "A", "priority": "Low", "bugs": 4},

{"team": "B", "priority": "Low", "bugs": 6},

]

df = pd.DataFrame(rows)

ordered = (

df.sort_values(["priority", "bugs"], ascending=[True, False])

)

fig, ax = plt.subplots()

ax.barh(ordered["team"] + " - " + ordered["priority"], ordered["bugs"])

ax.invert_yaxis()

The key is to make the intended order explicit in the data, then reflect it in the plot.

Edge cases: what breaks and how I handle it

Real data is messy. These are the edge cases I see most and how I solve them.

Edge case 1: Zero values

Bars with zero width technically exist but are invisible. That can confuse readers who expect a category to appear. I usually add a minimum annotation for visibility:

for bar in ax.patches:

value = bar.get_width()

y = bar.gety() + bar.getheight() / 2

label = "0" if value == 0 else f"{value}"

ax.text(max(value, 0) + 0.2, y, label, va="center", fontsize=8)

If the zero values are many, I may filter them out and mention it in a caption.

Edge case 2: Extremely long labels

Long labels can push the plot area too far to the right or overlap. I solve it by wrapping or shortening labels and using a footnote for full names.

import textwrap

wrapped = ["\n".join(textwrap.wrap(s, width=18)) for s in labels]

ax.barh(wrapped, values)

If wrapping makes the plot tall, I increase figure height accordingly.

Edge case 3: Mixed units or scale differences

A single plot should not mix units. If you must compare different units, I separate them into subplots or normalize them as percentages. A common anti-pattern is comparing dollars and counts on the same axis just because they “sort of” align.

Edge case 4: Very small values with large outliers

When you have one giant bar and many tiny ones, the chart becomes unreadable. I handle this by:

  • Using a log scale on the x-axis if the data is strictly positive.
  • Splitting into two plots: top N vs the rest.
  • Using an inset plot for the smaller range.

Here’s the log scale approach:

ax.set_xscale("log")

ax.barh(labels, values)

I only use this if I can clearly label the axis and the audience understands log scales.

Edge case 5: Missing or duplicate labels

Matplotlib accepts duplicate labels, but it can confuse readers. I usually disambiguate by adding a secondary field or numbering duplicates before plotting.

A production-ready template I actually use

When I’m building charts for dashboards or automated reports, I keep a consistent template. It saves time and makes charts look unified across a project.

import matplotlib.pyplot as plt

def barh_template(labels, values, title, xlabel, color="#2F5597"):

fig, ax = plt.subplots(figsize=(9, 5))

ax.barh(labels, values, color=color)

ax.set_title(title, fontsize=13, pad=10)

ax.set_xlabel(xlabel, fontsize=10)

ax.set_ylabel("")

# Clean frame

for spine in ["top", "right", "left", "bottom"]:

ax.spines[spine].set_visible(False)

ax.grid(axis="x", linestyle="--", linewidth=0.6, alpha=0.4)

ax.tick_params(axis="y", length=0)

ax.tick_params(axis="x", labelsize=9)

ax.invert_yaxis()

plt.tight_layout()

return fig, ax

Example usage

labels = ["Auth", "Search", "Payments", "Reports"]

values = [120, 95, 180, 140]

fig, ax = barh_template(labels, values, "P95 latency by service", "Latency (ms)")

plt.show()

I keep the function small so it’s easy to audit. I can add optional parameters for annotations or palettes depending on the project.

Comparison table: traditional vs modern approaches

I’ve noticed two distinct styles for barh() usage—simple one-off charts and repeatable production charts. Here’s how I think about the tradeoff.

Aspect

Traditional quick plot

Modern production plot —

— Data prep

Inline lists

DataFrames or pre-aggregated tables Sorting

Often skipped

Explicit and consistent Styling

Defaults

Minimal but intentional Reuse

Ad hoc

Template or helper function Validation

Visual only

Data checks before plotting Output

On-screen

Saved to file + CSV for audit

Both are valid. I still use quick plots in exploratory work. But when data moves into reports, I use the production approach so the plot can survive changes over time.

Practical scenarios you can borrow

Here are a few small, real-world situations where barh() is my go-to. I include the “why” because it matters as much as the code.

Scenario: Top 10 customers by revenue

I plot the top 10 customers by revenue so the sales team can see the rank instantly. I keep labels short and add direct value labels to avoid reading the axis.

customers = ["Acme", "Northwind", "Globex", "Initech", "Umbrella"]

revenue = [2.8, 2.1, 1.9, 1.2, 0.9]

fig, ax = plt.subplots()

ax.barh(customers, revenue, color="#4E79A7")

ax.invert_yaxis()

for bar in ax.patches:

v = bar.get_width()

y = bar.gety() + bar.getheight() / 2

ax.text(v + 0.05, y, f"${v:.1f}M", va="center", fontsize=9)

ax.set_xlabel("Revenue (USD, millions)")

ax.set_title("Top customers by revenue")

plt.tight_layout()

plt.show()

Scenario: Incident count by service

Operations teams often want to know which services are “hot” this week. A sorted barh chart answers that in seconds.

Scenario: Feature adoption rate

I use barh to show adoption across features with long names. If adoption rates are percentages, I use % in the axis label and annotate with exact values.

Scenario: Survey responses

When I have Likert-style responses (Strongly Agree → Strongly Disagree), I often use a diverging stacked barh. That’s a more advanced setup but communicates sentiment clearly.

Advanced pattern: diverging stacked bars for sentiment

This is one of my favorite use cases. You can represent positive and negative responses on either side of zero.

import numpy as np

import matplotlib.pyplot as plt

questions = ["The UI is intuitive", "Support is responsive", "Docs are clear"]

strongly_agree = [20, 18, 12]

agree = [30, 25, 28]

neutral = [25, 30, 35]

disagree = [15, 18, 15]

strongly_disagree = [10, 9, 10]

Convert to percentages

rows = np.array([stronglydisagree, disagree, neutral, agree, stronglyagree])

rows = rows / rows.sum(axis=0) * 100

fig, ax = plt.subplots(figsize=(9, 4.5))

Left side (negative)

left = np.zeros(len(questions))

for vals, color, label in zip(

rows[:2], ["#D73027", "#FC8D59"], ["Strongly disagree", "Disagree"]

):

ax.barh(questions, -vals, left=left, color=color, label=label)

left -= vals

Center neutral

ax.barh(questions, rows[2], color="#E0E0E0", label="Neutral")

Right side (positive)

left = rows[2]

for vals, color, label in zip(

rows[3:], ["#91BFDB", "#4575B4"], ["Agree", "Strongly agree"]

):

ax.barh(questions, vals, left=left, color=color, label=label)

left += vals

ax.axvline(0, color="#333", linewidth=0.7)

ax.set_xlabel("Percentage")

ax.set_title("Survey sentiment by question")

ax.legend(ncol=3, bboxtoanchor=(0.5, -0.15), loc="upper center")

plt.tight_layout()

plt.show()

This is a bit more complex, but it’s extremely effective in stakeholder presentations because it shows balance at a glance.

Annotation strategies that scale

Annotations can clutter fast. I use three simple strategies depending on the chart size.

Strategy 1: Label only the top N

If you have 20+ bars, label only the top 5 with exact values and rely on the axis for the rest.

Strategy 2: Label bars above a threshold

If most values are small, I only annotate bars above a threshold (like 10% or 100 units). This reduces noise.

Strategy 3: Use a summary label

When I’m showing top N and grouping the rest, I’ll annotate the “Other” bar with its value, which hints at the distribution.

Handling changes in data: defensive plotting

In production, data changes constantly. I guard against surprises by checking basic conditions before plotting:

  • Are there missing values?
  • Are there negative values where I didn’t expect them?
  • Is the dataset empty?

Here’s a simple guard pattern I use:

def safe_barh(labels, values):

if len(labels) == 0:

raise ValueError("No data to plot")

if len(labels) != len(values):

raise ValueError("Labels and values must align")

fig, ax = plt.subplots()

ax.barh(labels, values)

ax.invert_yaxis()

return fig, ax

If I’m working in a pipeline, I’d log these issues rather than crash, but the idea is the same: make problems obvious early.

Alternative approaches worth knowing

Even if barh() is your main tool, it helps to know alternatives and why you might choose them.

Alternative 1: seaborn.barplot

Seaborn can handle ordering and confidence intervals automatically. I use it when I’m already in a Seaborn workflow and want a quick statistical summary. The downside: it can be less explicit and harder to control at the pixel level.

Alternative 2: pandas.DataFrame.plot(kind="barh")

This is fast for exploratory data. It’s less flexible for complex layouts but great for quick checks. If I need production quality, I switch to Matplotlib directly.

Alternative 3: plt.barh + ax.text for direct labeling

This is still Matplotlib, but I treat it as its own approach because it replaces axis reading with value labels. This can be powerful in executive summaries where the axis is less important than the numbers themselves.

Alternative 4: Dot plots

When values are close together, I sometimes use a dot plot with ax.scatter on a shared axis. It can reduce clutter and make comparisons easier. But for most audiences, bars are more intuitive.

Production considerations: output, resolution, and consistency

In real workflows, plots aren’t just shown on screen—they’re exported, embedded in reports, or displayed in dashboards. That means I care about resolution, aspect ratio, and consistent style.

Output formats

  • PNG for web dashboards and slides.
  • SVG for crisp vector output in documents.
  • PDF for reports that need high quality in print.

Consistent fonts and sizes

If a report includes multiple charts, I standardize font sizes and title styles. This avoids the “patchwork” look of mismatched plots.

DPI matters

If you export a chart for slides, I use a higher DPI to prevent blurriness:

plt.savefig("chart.png", dpi=200, bbox_inches="tight")

Reproducibility

When stakeholders ask “where did that number come from,” I like having a CSV or table snapshot next to the plot. This makes discussions easier and builds trust.

Troubleshooting checklist I keep on hand

When a barh() chart looks wrong, I run this quick checklist:

1) Are labels and values the same length?

2) Is the ordering correct? Did I sort?

3) Are values in the right unit?

4) Are there negative values I didn’t expect?

5) Did I invert the y-axis after sorting?

6) Are bars too thin or crowded?

7) Are annotations overlapping?

This catches 90% of problems without deep debugging.

Practical testing: validation without overkill

I don’t “test” plots like I test code, but I do validate the data feeding them. Here’s a simple approach:

  • Assert the sum of bars matches a known total.
  • Check that the max value aligns with expected business metrics.
  • Print the top 5 categories as a sanity check.

This is especially useful when charts are generated automatically. It’s the difference between confidence and guesswork.

Long-form example: building a full report chart

Here’s a more complete example that includes data prep, sorting, annotations, and export. This is close to what I use in a real report pipeline.

import pandas as pd

import matplotlib.pyplot as plt

Example data

rows = [

{"service": "Auth", "incidents": 12},

{"service": "Search", "incidents": 5},

{"service": "Payments", "incidents": 18},

{"service": "Reports", "incidents": 7},

{"service": "Billing", "incidents": 9},

]

df = pd.DataFrame(rows)

Sort descending

sorteddf = df.sortvalues("incidents", ascending=False)

fig, ax = plt.subplots(figsize=(8, 4.5))

ax.barh(sorteddf["service"], sorteddf["incidents"], color="#2F5597")

ax.invert_yaxis()

Add annotations

for bar in ax.patches:

v = bar.get_width()

y = bar.gety() + bar.getheight() / 2

ax.text(v + 0.2, y, f"{v}", va="center", fontsize=9)

Style

ax.set_title("Incidents by service (last 30 days)")

ax.set_xlabel("Incident count")

ax.grid(axis="x", linestyle="--", linewidth=0.6, alpha=0.4)

for spine in ["top", "right", "left", "bottom"]:

ax.spines[spine].set_visible(False)

plt.tight_layout()

plt.savefig("incidents_barh.png", dpi=200)

plt.show()

This is deliberate and repeatable. If the data changes, the chart still makes sense and still looks clean.

Using barh() in dashboards and notebooks

When I’m working in notebooks, I lean into interactivity. But when I’m building a dashboard, I focus on consistency. A few habits that keep me sane:

  • Use the same figure size for similar charts.
  • Standardize color palettes across the dashboard.
  • Keep titles short and descriptive.
  • Avoid putting too many charts on one page; five or six is my limit.

If you’re using Matplotlib inside a web app (like a report generator), I also recommend setting a global style so every chart follows the same rules. That reduces manual effort and makes the output feel unified.

Practical advice on sorting and storytelling

Sorting is not just a technical step—it changes the story. I always ask: “What should the reader notice first?” If the answer is “the biggest value,” I sort descending. If the answer is “process order,” I keep the natural order even if the values jump around.

For example, if I’m showing stages of a pipeline, I keep the pipeline order. The chart is less about ranking and more about drop-off between stages. If I sorted by value, I would destroy the story.

This is a subtle but critical decision. barh() makes ranking easy, but ranking isn’t always what you want.

A quick checklist for professional-grade barh charts

Before I ship a chart, I do this:

  • I check the data order and confirm it matches the story.
  • I verify labels, units, and axis titles.
  • I test readability by shrinking the window or viewing on a smaller screen.
  • I confirm that the chart still looks good with one or two categories removed or added.
  • I save the output and view it in the context where it will be used.

This adds a few minutes but saves hours of confusion later.

Closing with practical next steps

You now have a working mental model for matplotlib.pyplot.barh(): categories on the y-axis, values on the x-axis, and styling choices that drive clarity. In my experience, the best horizontal bar charts are the ones that answer a single question cleanly—“Which categories are largest?” or “How do these services compare?” If you keep that question front and center, your chart will do real work instead of just looking nice.

If you want to move forward, pick one dataset you already own—course enrollments, feature usage, incident counts—and apply the sorted baseline example. Then try one enhancement: add annotations or change spacing. I suggest you keep the first version simple and only add styling when you can say what it adds to comprehension. A clean barh() plot with a clear label beats a fancy chart with confusing colors every time.

Finally, I recommend saving your chart template. When you’ve nailed a clean layout, reuse it across projects. Consistency builds trust. Readers learn what your charts mean, and you save time. The best part is that once you’ve made a strong barh() template, you can apply it to almost any ranked category problem you’ll meet in Python.

Scroll to Top