Matplotlib Scatter Marker: Shape, Size and Color

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()
A Matplotlib scatter plot using triangle markers sized at 200 points squared in dark red
One call, three arguments: 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")
A reference chart of every Matplotlib marker style showing the plotted symbol above its code such as o, s, ^ and star
All 37 markers with the code you pass to 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, so edgecolors does 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
Command Prompt showing that the matplotlib scatter s parameter is an area so the marker diameter is its square root
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()
Two Matplotlib scatter plots comparing fixed marker sizes against per-point sizes forming a bubble chart
A single 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()
A Matplotlib scatter plot with three categories drawn in three colours and a legend naming each species
A loop over the groups, so every call adds its own legend entry.

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
A Matplotlib scatter plot coloured by value using the viridis colormap with a colorbar on the right
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
Command Prompt showing matplotlib raising a ValueError when numeric values are passed to the color argument instead of c
The error message names the fix: use 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.

ArgumentAcceptsColormapped?
c=Colours or numbersYes, when given numbers
color=Colours onlyNo
cmap=A colormap nameOnly affects c as numbers

Use c by default in scatter. It do