Line, bar, pie, and scatter charts straight off a data frame — no matplotlib boilerplate.
What .plot actually does
Why would you ever import matplotlib to draw a chart when the data frame can draw it for you?
That is the whole pitch for .plot. It is an accessor, like .str and .dt, so you get one method per chart type: .plot.line(), .plot.bar(), .plot.pie(), .plot.scatter(). The older spelling, .plot(kind='line'), does exactly the same thing and still turns up in a lot of code, mine included. I prefer the accessor form now, because each chart type gets its own signature and its own documentation page, and because tab completion lists all eleven kinds for you.
The part people miss is that you do not tell .plot what to draw. You tell it by handing it the right shape. The index becomes the x axis, each column becomes one series, and the values become the y axis. That is the entire contract, and it means that when a chart looks wrong the fix is almost never a plotting argument — it is a groupby, a set_index, or an unstack upstream of the plot.
Scatter is the exception that proves the rule: it wants two columns of numbers rather than an index and a value, so you name them with x= and y=, and it exists only on data frames. Ask a series for one and Pandas says so in the words I would use myself: ValueError: plot kind scatter can only be used for data frames.
Official documentation: DataFrame.plot, and the four accessor methods plot.line, plot.bar, plot.pie, and plot.scatter.
The arguments that earn their keep
s.plot.line(figsize=(8, 4), # inches, width by height
title='...', # saves you a call to ax.set_title
ylabel='GW', # and one to ax.set_ylabel
rot=45, # rotate the tick labels
logy=True) # log scale on the y axis
df.plot.bar(stacked=True, # one bar per row, columns piled up
rot=0, # keep short labels horizontal
color=[...], # one color per column
subplots=True) # a separate panel per column instead
df.plot.scatter(x='...', y='...',
c='...', # color the points by a column
colormap='Spectral',
alpha=0.3) # essential once points overlap
s.plot.pie(autopct='%1.0f%%', ylabel='')
Four carry most of the weight: figsize, because the default is small; title and ylabel, because they save a round trip into matplotlib; and rot, because category labels collide constantly.
A worked example, on real data
The Global Coal Plant Tracker lists every coal-fired generating unit on earth — 13,906 rows, each with a country, region, status, capacity, commissioning year, and annual CO2. Bamboo Weekly #64 built a puzzle around it, and it suits all four charts: a time dimension, a category dimension, two correlated numbers.
import pandas as pd
url = ('https://www.bambooweekly.com/content/files/wp-content/uploads/2024/02/'
'global-coal-plant-tracker-january-2024.xlsx')
df = pd.read_excel(url, sheet_name='Units',
usecols=['Country', 'Region', 'Status', 'Start year',
'Capacity (MW)',
'Annual CO2 (million tonnes / annum)'])
Start with the line chart, and notice how much of the work happens before the plot call. A time axis needs a real datetime index, so I convert the commissioning year, set it as the index, and resample by decade:
by_decade = (
df
.loc[df['Status'] == 'operating']
.dropna(subset=['Start year'])
.assign(commissioned=lambda df_: pd.to_datetime(df_['Start year'].astype(int),
format='%Y'))
.set_index('commissioned')
['Capacity (MW)']
.resample('10YS').sum()
.div(1000)
)
by_decade
commissioned
1942-01-01 0.3930
1952-01-01 17.8494
1962-01-01 71.7737
1972-01-01 152.2763
1982-01-01 196.1144
1992-01-01 211.6531
2002-01-01 632.6907
2012-01-01 723.5499
2022-01-01 117.7180
Freq: 10YS-JAN, Name: Capacity (MW), dtype: float64
Now the chart is a single call, and it inherits its x axis from the index:
by_decade.plot.line(title='Operating coal capacity by decade commissioned',
figsize=(8, 4), ylabel='GW')
A gentle climb through the twentieth century, then a near-vertical jump in the 2000s. The last point is a bit of a lie — that bucket holds only 2022 and 2023, so two years get plotted against nine decades. Charts never warn you about this.
For categories, bar and barh are the same chart rotated, and which one you want depends entirely on how long the labels are. Five region names fit across the bottom:
region = (df.loc[df['Status'] == 'operating']
.groupby('Region')['Capacity (MW)'].sum()
.div(1000).sort_values())
region.plot.bar(rot=0, title='Operating coal capacity by region (GW)')
Region
Oceania 22.9030
Africa 51.1526
Europe 167.1535
Americas 221.4440
Asia 1667.4654
Name: Capacity (MW), dtype: float64
Ten country names do not, and no amount of rot= makes "United States" readable on its side. Turn the chart:
(df
.loc[df['Status'] == 'operating']
.groupby('Country')['Capacity (MW)'].sum()
.div(1000)
.nlargest(10)
.sort_values()
.plot.barh(figsize=(7, 5))
)
Country
Poland 28.5096
Russia 37.8571
South Korea 40.1340
Germany 40.3615
South Africa 43.6241
Indonesia 51.5566
Japan 55.1230
United States 200.0902
India 237.1482
China 1136.7310
Name: Capacity (MW), dtype: float64
Note the .sort_values() before the plot. barh builds from the bottom up, so sorting ascending puts the largest bar at the top, where a reader's eye starts.
That same five-row region series also makes a defensible pie, because five is about the limit:
region.plot.pie(autopct='%1.0f%%', ylabel='')
Scatter needs the two column names spelled out, and alpha once there are thousands of points:
(df
.loc[df['Status'] == 'operating']
.plot.scatter(x='Capacity (MW)',
y='Annual CO2 (million tonnes / annum)',
alpha=0.3, figsize=(7, 5))
)
6,580 points sitting almost perfectly on a line — the two columns correlate at 0.98, which is what you would expect given that one is largely computed from the other.
Finally, the move I use most: a grouped result plotted directly, with no intermediate variable at all.
(df
.loc[df['Status'].isin(['operating', 'construction'])]
.groupby(['Region', 'Status'])['Capacity (MW)'].sum()
.unstack('Status')
.div(1000)
.plot.bar(rot=0, figsize=(8, 4))
)
Status construction operating
Region
Africa 2.8050 51.1526
Americas 0.1200 221.4440
Asia 193.6535 1667.4654
Europe 0.7350 167.1535
Oceania NaN 22.9030
Two columns, so two bars per region, with the legend built from the column names. This is why unstack and plotting sit next to each other so often: unstacking is how a grouped result becomes the shape a chart wants.
Five mistakes people make
Plotting before the index means anything. If your dates are still strings, .plot.line() treats them as unordered categories and spaces them evenly in row order. Group this dataset by a string year without sorting and the x axis comes out as ['1955', '2022', '1984', '2003', '1969', '1967'] — a chart that looks like a chart and means nothing. A real datetime index also exposes gaps: resampling by year gives 82 points where only 75 distinct years appear in the data, so the seven years with no new capacity show up as the flat stretches they are, instead of being silently closed up.
Reaching for .plot.bar() when the labels are long. Vertical bars get vertical labels, and vertical labels are unreadable. barh costs you nothing and fixes it. A related trap: bar treats a datetime index as plain categories, so a quarterly series comes out labeled 2020-03-31 00:00:00, 2020-06-30 00:00:00, and so on, where line on the same series draws a properly formatted date axis.
Forgetting that .plot returns an Axes. It is a matplotlib object, not a data frame, so the method chain ends there: region.plot.bar().sort_values() raises AttributeError: 'Axes' object has no attribute 'sort_values'. Put the plot call last. The return value is a gift, though — catch it and you can keep going in matplotlib with ax.set_ylim(0, 500), ax.axhline(100), or ax.figure.savefig('coal.png').
Pie charts on more than a handful of categories. df['Status'].value_counts() here returns nine statuses, five of them under 300 rows. Nine slices, most of them slivers, is just a table with the numbers taken out. Pie earns its place with three to six parts of one whole; otherwise use barh. Two rules to know before they bite: on a data frame you must name the column with y= or pass subplots=True, and negative values raise a ValueError outright.
Pretending matplotlib is not down there. Pandas plotting is a wrapper: you need matplotlib installed, and every plot lands on matplotlib's current Axes unless you pass ax=. A Jupyter notebook wraps up a figure per cell, so you never notice. In a script, two consecutive .plot.line() calls draw both series onto one set of axes, and mixing kinds can fail outright — a time-series line followed by a categorical bar chart in the same figure raises AttributeError: 'Index' object has no attribute 'freq' in Pandas 3, because the Axes is still in time-series mode. Pass ax= when you mean to overlay, and call plt.close() when you do not.
Where it shows up in Bamboo Weekly
Eighty-nine of the 185 Bamboo Weekly solutions draw at least one chart, which makes plotting the most common way a Bamboo Weekly method chain ends. Four free-to-read ones, each showing a different chart:
Bamboo Weekly #54: Household debt has one of the most heavily tuned plot calls in the archive, stacking six categories of American consumer debt from the New York Fed's quarterly report: .plot.bar(stacked=True, rot=60, figsize=(8, 8), fontsize=5, color=[...]). With 84 quarters on the x axis, it shows both how much tuning a real chart needs and why stacked=True beats six separate bars at every one of those points.
Bamboo Weekly #74: UK elections is the pie chart: df['party_abbreviation'].value_counts().plot.pie() over the 2024 seat counts, then drawn again with colors= giving Labour red, the Conservatives blue, and the Liberal Democrats orange. Fifteen parties should be too many slices, and it works only because three of them — Labour with 413 seats, the Conservatives with 121, the Liberal Democrats with 72 — swamp the other twelve. That lopsidedness is the finding.
Bamboo Weekly #72: City travel uses a stacked bar of transport modes and a scatter colored by category, .plot.scatter(x='population', y=0, c='abc', colormap='Spectral') — reached only after a stack and a join put the data in scatter shape.
Bamboo Weekly #46: Pedestrians takes US Department of Transportation crash data and plots one groupby('YEAR') twice: as .plot.line() for the share of crashes involving a pedestrian, and as .plot.bar(stacked=True) for the raw counts with and without. Two charts, one grouping — the clearest demonstration in the archive that the chart type follows from the question, not from the data.
Practice it
Work through a .plot() exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/plot/
Go deeper
Because the shape decides the chart, the pages worth reading next are the reshaping ones: groupby for one row per category, unstack for one column per series, and resample for the datetime index a line chart wants. The Pandas user guide on chart visualization is the full catalog. There are eleven chart kinds; this page covers five of them, and the other six are area, hist, box, kde, density, and hexbin.
Bamboo Weekly is the practice. If you want the structured version — full courses with downloadable Jupyter notebooks, plus live Pandas office hours when you get stuck — that is LernerPython+Data. A paid Bamboo Weekly subscription is included with it.
Related methods
px.line()— when you want the interactive chart that Bamboo Weekly now uses by defaultpx.bar()— for the interactive version of the same chart
See it on real data
Below are the 2 Bamboo Weekly exercises that use plot on real-world data — try each one, then study the worked solution.
Part of the Pandas Methods Index. See also practice by skill.