plot() and the arguments that do most of the work - style, width, colour and what happens with missing data.
Overview
The signature
ax.plot(y) draws y against its index. ax.plot(x, y) draws one against the other.
That one-argument form is convenient and occasionally misleading — a plot whose x axis reads 0 to 99 when the data is dated is usually a forgotten x argument rather than a deliberate choice.
plot accepts lists, NumPy arrays and pandas Series. With a Series it uses the index as x, which is why plotting a time-indexed Series gives a dated axis for free.
Worth knowing
ax.plot(y) uses range(len(y)) as x; ax.plot(x, y) is explicit.
Each plot call takes the next colour from the axes' property cycle, wrapping after ten.
color, linestyle and linewidth do most of the styling; "0.4" is a grey level.
The format string "r--o" packs colour, style and marker into one word — terse, limited, and worth being able to read.
NaN leaves a gap in the line rather than being interpolated across, which is usually the honest choice.
Later draw calls paint on top; zorder overrides that.
Line Plots
plot() and the arguments that do most of the work.
x and y, or just y
With one argument matplotlib supplies the index as x.
example_01.pymatplotlib
Output
Several lines, and where the colours come from
Each call takes the next colour from the axes' cycle.
example_02.pymatplotlib
Output
Style, width and colour
The three arguments that turn a default line into a deliberate one.
example_03.pymatplotlib
Output
The format-string shorthand
Compact, common in examples, and worth being able to read.
example_04.pymatplotlib
Output
Missing values break the line
NaN leaves a gap rather than drawing through it, which is usually right.
example_05.pymatplotlib
Output
Order matters, and so does zorder
Later calls draw on top, unless you say otherwise.
example_06.pymatplotlib
Output
The colour cycle
Each call to plot on the same axes takes the next colour from the property cycle, a list of ten colours defined by the current style.
This is why several lines drawn in a loop come out in different colours with no work, and why the eleventh line repeats the first.
plt.rcParams["axes.prop_cycle"] holds it. ax.set_prop_cycle(...) replaces it for one axes, which is how you enforce a house palette.
Passing an explicit color bypasses the cycle without advancing it.
Styling
Three keywords cover nearly everything:
color — a name ("crimson"), a hex string ("#4c72b0"), a single letter ("r"), a grey level as a string ("0.4"), or an RGBA tuple.
linestyle — "-" solid, "--" dashed, "-." dashdot, ":" dotted, or a dash pattern as a tuple.
linewidth — in points.
Add alpha for transparency, which is how you keep many overlapping lines readable, and marker to show where the actual data points are.
That last one matters more than it looks: a smooth line through four points implies a lot of data that does not exist. Markers say where the measurements are and let the line be what it is — a visual aid rather than a claim.
The format string
ax.plot(x, y, "r--o") sets colour, linestyle and marker in one string.
It is worth learning to read because it appears constantly in examples and documentation. It is not worth preferring: it supports only eight colours, cannot set width or alpha, and is opaque to anyone who has not memorised it.
The order within the string does not matter, and any part can be omitted — "o" alone draws markers with no connecting line, which is a scatter plot by another route.
Missing data
NaN values are skipped, and the line is broken where they occur.
This is the right default. A line drawn straight through a gap asserts that the value moved smoothly across it, which is exactly the thing you do not know.
If you want the line joined, dropping the missing points is explicit about it:
ok = ~np.isnan(y)
ax.plot(x[ok], y[ok])
Note that None behaves like NaN here, and a masked array's masked values are also skipped.
For a long series with occasional gaps, the broken line can look noisy; drawing markers as well makes the pattern of missingness visible rather than merely untidy.
Drawing order
Artists are drawn in the order the calls are made, so later calls sit on top.
zorder overrides it. Higher values draw later. The defaults are set so that the usual expectations hold — patches at 1, lines at 2, text at 3 — which is why a line normally appears above a filled region even when the fill was drawn second.
Where it matters most is grid lines. ax.grid() draws below the data by default, and ax.set_axisbelow(False) puts it on top, which is almost never what you want but is occasionally what you get from a style sheet.
Markers
marker="o" draws a symbol at each data point. The common ones are o circle, s square, ^ triangle, D diamond, . point, + and x.
Markers matter more than they look. A smooth line through five points implies a continuous relationship measured densely; markers say where the measurements actually are and let the line be the visual aid it is.
markevery=10 draws one marker in ten, which keeps the "here are the observations" signal on a dense series without a solid band of symbols.
markersize, markerfacecolor and markeredgecolor style them separately from the line, so a hollow marker is markerfacecolor="none".
Steps and stems
Not every sequence is a line.
ax.step(x, y, where="post") draws a step function, which is correct for anything that holds a value until it changes: a price, a setting, a state. A straight line between the points would claim a gradual transition that did not happen.
where takes "pre", "post" or "mid", deciding whether the step happens at the start or the end of each interval. Getting it wrong shifts every value by one position, and the chart looks plausible either way.
ax.stem draws a vertical line to each point, which suits sparse discrete data.
ax.fill_between under a line turns it into an area chart, appropriate when the quantity accumulates to something meaningful and misleading when it does not.
Multiple lines and readability
Four or five lines on one axes is usually the limit before the chart becomes a tracing exercise.
Beyond that, the options in order of preference are: highlight one and grey the rest, split into small multiples, or reduce to the two lines the chart is actually about.
Where several lines must stay, three things help.
Direct labels at the line ends, instead of a legend.
Ordering the legend to match the vertical order of the lines at their right-hand end, so the eye can map them without hunting. ax.legend(handles=...) in the order you want.
Varying linestyle as well as colour, so the chart survives greyscale.
Interpolation is a claim
A line between two points asserts that the quantity passed through the values in between.
For a temperature sampled hourly, that is reasonable. For monthly sales, the line between January and February is decorative — nothing happened at "January the 15th" in the data.
That does not make the line wrong; connecting points is how the eye reads a trend, and a scatter of twelve unconnected points is harder to follow. But it is worth knowing that the line is an aid rather than data, which is another argument for markers.
Where the gaps are genuinely large, breaking the line is more honest than spanning them, and that is what leaving the NaN in does for you automatically.
Performance note
One plot call with a large array is fast. Many plot calls are slow, because the cost is per artist rather than per point.
Drawing fifty lines in a loop creates fifty artists; where they can be combined, LineCollection draws them as one.
That matters at hundreds of lines rather than dozens, and it is covered properly in the performance module.
A worked example
Turning a default line chart into a finished one is a short, fixed sequence.
fig, ax = plt.subplots(figsize=(7.5, 3.5))
ax.plot(x, y, color="#264653", linewidth=2, marker="o", markersize=4)
ax.set_title("What the chart shows", loc="left", fontsize=12, fontweight="bold")
ax.set_xlabel("Month of 2024")
ax.set_ylabel("Sales (thousands)")
ax.margins(x=0.02)
ax.grid(True, axis="y", alpha=0.3)
ax.set_axisbelow(True)
for side in ("top", "right"):
ax.spines[side].set_visible(False)
Aspect, colour, weight, markers, a title that says something, labelled units, a faint grid on one axis, two spines removed.
Ten lines, none of them clever, and the result looks deliberate. Every one is covered somewhere in this track, and together they are most of what separates a default matplotlib chart from a published one.
Reading a line chart critically
Three questions worth asking of any line chart, including your own.
Does the y axis start at zero, and should it? A line encodes position, so truncation is legitimate — and it changes the apparent size of every change. If a 2% movement fills the chart, the reader should be able to tell.
Are the x values evenly spaced? If the axis is categorical or the points are irregular in time, the slope between points is not a rate, and the eye reads it as one.
What is between the points? Twelve monthly observations joined by lines look like a continuous series. The line is an aid; the data is twelve points.
None of these makes a line chart wrong. They are the things a careful reader checks, and knowing them is what lets you draw one that survives the checking.
In summary
ax.plot(x, y) with color, linewidth, linestyle and marker covers the great majority of line charts.
The colour cycle supplies distinct colours automatically and wraps after ten, which is well past the point where a line chart has stopped being readable.
NaN breaks the line, which is the honest treatment of a gap.
Markers say where the data actually is, and matter most when the points are few — a smooth line through five observations claims a lot.
Later calls draw on top, and zorder overrides it.
And the finishing sequence is the same every time: an aspect ratio suited to the data, a title that says something, labelled units, a faint grid on one axis, and the top and right spines removed.
Smoothing
A noisy series is often plotted with a smoothed version over it, and the pairing is more honest than either alone.
The raw series in a pale colour and the rolling mean in a strong one shows both the variation and the trend, and makes clear that the smooth line is derived rather than measured.
Two cautions worth stating on the chart.
The window is a choice, and a longer one produces a smoother, more convincing line that is further from the data. Naming it — "7-day mean" — is the minimum.
A centred window uses future values, which is fine for describing history and wrong for anything presented as a signal available at the time. A trailing window is the honest choice for that case, at the cost of lagging the turns.
Plotting only the smoothed line, without the raw data, is where this becomes misleading, because the reader has no way to judge how much was smoothed away.
Highlighting within a line chart
A line chart with one important series and several for context is the most common real case, and the treatment is consistent:
for name, y in others.items():
ax.plot(x, y, color="0.85", linewidth=1, zorder=1)
ax.plot(x, focus_y, color="#264653", linewidth=2.5, zorder=2)
ax.text(x[-1], focus_y[-1], " " + focus_name, va="center", color="#264653")
The grey lines give the range and the shape of the group, so nothing is lost. The dark line is unmistakably the subject. The label at the end removes the legend.
zorder ensures the highlighted line is on top regardless of drawing order, which matters when the context lines are drawn in a loop after it.
This pattern is worth having to hand, because it converts the hardest kind of line chart — many series, one message — into one of the easiest to read.
One more thing
ax.plot accepts several x/y pairs in one call — ax.plot(x, y1, x, y2) — which draws both with separate colours from the cycle.
It is compact and gives no way to label the lines individually, so a loop with label= is usually better. Worth recognising in existing code, where it appears often.
The short version
A line chart is the most common chart there is, and the finishing sequence is the same every time: aspect, colour, weight, title, units, a faint grid, two spines removed.
The line itself is an aid rather than data, which is worth remembering when the points are few and the line is doing a lot of implying.
Reading the code back
A finished line chart is about a dozen calls, and it is worth being able to name what each one is for: the figure size sets the aspect, the plot call sets colour and weight, the title carries the message, the labels carry the units, the margins control the breathing space, the grid supports value reading, and the spine removal takes away what carries nothing. Nothing in that list is optional in the sense of being decorative; each answers a question a reader would otherwise have to ask. Writing them in the same order every time makes the chart quick to produce and quick to review, which is most of why a house function is worth having.
Check yourself
0 of 4
Answer without scrolling back up.
What does `ax.plot(y)` use for the x values?
An axis reading 0 to 99 when the data is dated is usually a forgotten x argument rather than a choice.
Why do four lines drawn in a loop come out in different colours?
The cycle holds ten colours, so the eleventh line repeats the first. Passing an explicit color bypasses it without advancing it.
What does matplotlib do with a NaN in the middle of a line?
The right default - a line drawn through the gap would assert the value moved smoothly across it, which is the thing you do not know.
Two calls draw a fill and a line over the same region. What decides which is visible?
Later calls draw on top. Defaults put patches at 1 and lines at 2, so a line usually sits above a fill drawn before it.
Cheat sheet
Line Plots
That one-argument form is convenient and occasionally misleading — a plot whose x axis reads 0 to 99 when the data is dated is usually a forgotten x argument rather than a deliberate choice.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.