Three arguments control how a Matplotlib scatter marker looks, and each one does a single job:
plt.scatter(x, y,
marker="^", # the SHAPE
s=200, # the SIZE, as an area in points squared
c="#c0392b") # the COLOUR
s is the one that surprises people. It is an area, so s=400 is twice as wide as s=100, not four times.
Every chart below is a real Matplotlib window from Python 3.12.5, Matplotlib 3.11.2, NumPy 2.5.3.
import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5]
y = [2, 4, 3, 5, 4]
plt.figure(figsize=(7, 4))
plt.scatter(x, y, marker="^", s=200, c="#c0392b") # shape, size, colour
plt.title("marker, s and c control everything")
plt.grid(alpha=.3); plt.tight_layout()
plt.show()
marker, s and c.Every Matplotlib marker style and its code
Matplotlib ships 37 marker codes. Most guides list five of them, so here is the whole set drawn out:
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
markers = [(k, v) for k, v in Line2D.markers.items() if v != "nothing"]
print("markers available:", len(markers))
fig, ax = plt.subplots(figsize=(10, 5))
for i, (code, name) in enumerate(markers):
row, col = divmod(i, 8)
ax.scatter(col, -row, marker=code, s=220, c="#0b6bcb")
ax.text(col, -row - 0.32, repr(code), ha="center", fontsize=8, color="#333")
ax.set_title("Every Matplotlib marker and its code")
ax.set_xlim(-0.6, 7.6); ax.set_ylim(-4.7, 0.6)
ax.axis("off")
plt.tight_layout(); plt.show()
Output:
markers available: 37
C:\pyguides\runs\scattermarker\ex_allmarkers.py:31: UserWarning: The pixel maker ',' is not supported on scatter(); using a finite-sized square instead, which is not necessarily 1 pixel in size. Use the square marker 's' instead to suppress this warning.
ax.scatter(col, -row, marker=code, s=220, c="#0b6bcb")
marker=.The codes worth memorising are the obvious ones: "o" circle, "s" square, "^" triangle, "*" star, "D" diamond and "x" cross.
- Filled markers —
o s ^ v < > p * h H D d P X 8 .take a face colour and an edge colour. - Unfilled markers —
+ x 1 2 3 4 | _are drawn as lines, soedgecolorsdoes nothing to them. marker="$Z$"renders any character as the marker, which is handy for one-off labels.
The default is "o". You can change it globally with plt.rcParams["scatter.marker"] if you’d rather not repeat yourself.
Matplotlib scatter marker size: the s parameter
s is an area measured in points squared. That sounds pedantic until a marker comes out the wrong size:
import numpy as np
from matplotlib.lines import Line2D
# s is an AREA in points squared, so the diameter is its square root
for s in (25, 100, 400, 900):
print(f"s={s:<5} -> diameter {np.sqrt(s):>5.1f} points")
print()
print("Doubling the width means multiplying s by 4, not by 2.")
print()
print("filled markers:", Line2D.filled_markers)
print("unfilled ones ('1','2','+','x') ignore facecolor and only take a colour")
Output:
s=25 -> diameter 5.0 points
s=100 -> diameter 10.0 points
s=400 -> diameter 20.0 points
s=900 -> diameter 30.0 points
Doubling the width means multiplying s by 4, not by 2.
filled markers: ('.', 'o', 'v', '^', '<', '>', '8', 's', 'p', '*', 'h', 'H', 'D', 'd', 'P', 'X')
unfilled ones ('1','2','+','x') ignore facecolor and only take a colour
s=100 gives a 10-point marker, s=400 a 20-point one.The default is s=36, which is a 6-point marker. If you want markers twice as wide, multiply s by four.
s also takes one value per point, and that is how you build a bubble chart:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(0)
x, y = rng.random(40), rng.random(40)
population = rng.integers(10, 400, 40)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.2))
for i, s in enumerate((25, 100, 400, 900)):
ax1.scatter(i, 0, s=s, c="#0b6bcb")
ax1.text(i, -0.55, f"s={s}", ha="center", fontsize=9)
ax1.set_title("One size per call")
ax1.set_ylim(-1, 1); ax1.axis("off")
# s also accepts one value per point, which is how bubble charts are made
ax2.scatter(x, y, s=population, c="#0a7d32", alpha=.6, edgecolors="white")
ax2.set_title("One size per point (a bubble chart)")
ax2.grid(alpha=.3)
plt.tight_layout(); plt.show()
s on the left, an array of sizes on the right.Because the eye reads bubble area rather than radius, mapping a quantity straight to s is honest. Scaling the radius instead exaggerates large values. The same care applies when you’re fitting a line through a scatter plot.
Matplotlib scatter color for a single colour or a category
One colour for every point is just c= with a colour name or hex code. Categories need one call each:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(1)
groups = {"setosa": "#0b6bcb", "versicolor": "#c0392b", "virginica": "#0a7d32"}
plt.figure(figsize=(7.5, 4.5))
for i, (name, colour) in enumerate(groups.items()):
x = rng.normal(i * 2, .5, 30)
y = rng.normal(i, .5, 30)
plt.scatter(x, y, c=colour, label=name, s=70, alpha=.8)
plt.legend(title="species")
plt.title("A separate scatter call per category")
plt.grid(alpha=.3); plt.tight_layout()
plt.show()
Calling scatter once per group is what makes the legend work. A single call with mixed colours gives you one legend entry, not three.
If the legend needs more control than label= gives you, see the scatter plot legend guide.
Matplotlib scatter color by value with a colormap
When the colour should represent a number rather than a group, pass the numbers themselves to c:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(2)
x, y = rng.random(200), rng.random(200)
depth = x * 2 + y # the value we want colour to represent
plt.figure(figsize=(7.5, 4.8))
sc = plt.scatter(x, y, c=depth, cmap="viridis", s=60)
plt.colorbar(sc, label="depth")
plt.title("c= a numeric array, mapped through a colormap")
plt.tight_layout(); plt.show()
print("c received", depth.shape[0], "numbers, not colours")
print("colormap used:", sc.get_cmap().name)
print("value range mapped:", round(float(depth.min()), 3), "to", round(float(depth.max()), 3))
Output:
c received 200 numbers, not colours
colormap used: viridis
value range mapped: 0.135 to 2.866
c=depth with cmap="viridis", and a colorbar to read it.Matplotlib scales your numbers to the 0–1 range and looks each one up in the colormap. The default is viridis.
Always add the colorbar. Without it the colours are decoration, because nobody can tell which shade means which value.
Why does color= raise a ValueError in scatter?
Because c and color are not the same argument, and only one of them maps numbers:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
values = [0.1, 0.5, 0.9]
# c= maps numbers through a colormap
sc = ax.scatter([1, 2, 3], [1, 2, 3], c=values)
print("c= numbers -> OK, mapped through", sc.get_cmap().name)
# color= refuses them
try:
ax.scatter([1, 2, 3], [1, 2, 3], color=values)
except ValueError as err:
print("color= numbers ->", err)
print()
print("c= also accepts real colours:", ["red", "green", "blue"])
ax.scatter([1, 2, 3], [3, 2, 1], c=["red", "green", "blue"])
print("that works too, which is why the two arguments get confused")
Output:
c= numbers -> OK, mapped through viridis
c= also accepts real colours: ['red', 'green', 'blue']
that works too, which is why the two arguments get confused
c.The message reads 'color' kwarg must be a color or sequence of color specs, and it ends by telling you to use c instead.
| Argument | Accepts | Colormapped? |
|---|---|---|
c= | Colours or numbers | Yes, when given numbers |
color= | Colours only | No |
cmap= | A colormap name | Only affects c as numbers |
Use c by default in scatter. It does everything color does and one thing more.
Adding outlines to scatter markers with edgecolors
Dense scatter plots turn into a single blob. An outline fixes it in one argument:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(3)
x, y = rng.random(60), rng.random(60)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.2), sharey=True)
ax1.scatter(x, y, s=260, c="#0b6bcb", alpha=.55)
ax1.set_title("No outline: overlaps turn to mush")
ax2.scatter(x, y, s=260, c="#0b6bcb", alpha=.55,
edgecolors="white", linewidths=1.5)
ax2.set_title("edgecolors='white': points stay separate")
for ax in (ax1, ax2): ax.grid(alpha=.3)
plt.tight_layout(); plt.show()
edgecolors="white" with linewidths around 1.5 separates overlapping points without adding visual weight.
Pair it with alpha between 0.5 and 0.7 for anything above a few hundred points. For fully see-through markers, see transparent scatter plots.
Common scatter marker mistakes
| Symptom | Cause | Fix |
|---|---|---|
| Markers far too big or small | Treating s as a diameter | s is an area: use width**2 |
ValueError on color | Numbers passed to color= | Use c= |
| Legend shows one entry | One scatter call for all groups | Call it once per group with label= |
edgecolors does nothing | An unfilled marker like "x" | Use a filled marker, or set color |
| Colours look meaningless | No colorbar | plt.colorbar(sc) |
More Matplotlib plotting guides:
- Add a legend to a scatter plot
- Make scatter plots transparent
- Draw a best fit line on a scatter plot
- 3D scatter plots
- Time series scatter plots
- Scatter plots from a pandas DataFrame
Frequently asked questions
How do I change the marker in a Matplotlib scatter plot?
Pass marker= with a code such as "^", "s" or "*". The complete list is in the Matplotlib markers reference.
What does the s parameter mean in scatter?
It is the marker area in points squared, so the marker’s diameter is the square root of s. s=100 draws a 10-point marker and the default is 36.
What is the difference between c and color in scatter?
c accepts either colours or numbers, and numbers get mapped through a colormap. color accepts colours only and raises a ValueError if you give it numbers.
How do I colour scatter points by value?
Pass the numeric array as c and set cmap, then add plt.colorbar() so the colours can be read.
How do I give each category its own colour?
Call plt.scatter once per category with its own c and label. One call for all of them produces a single legend entry.
How many marker styles does Matplotlib have?
37 codes, of which 16 are filled markers that accept both a face colour and an edge colour.
Why is edgecolors not doing anything?
You are using an unfilled marker such as "x" or "+". Those are drawn as lines, so they only have a single colour.
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn