GeezerVison is showing github.com — [Bug]: Cannot use empty markers in scatter · Issue #24404 · matplotlib/matplotlib
[Bug]: Cannot use empty markers in scatter #24404DescriptionBug summary
I'd like to have a scatter plot with empty 'o' markers (just the edge).
This seems impossible.
Code for reproduction
from matplotlib.pylab import *
x = arange(0, 10)
scatter(x, x, c=x, facecolors='none')
Actual outcome

Expected outcome
Empty markers.
Matplotlib Version
3.5.1 Activityjklymak commented on Nov 8, 2022 It's inconvenient, but not impossible.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
x = np.arange(0, 10)
norm = plt.Normalize(0, 10)
cmap = mpl.colormaps['viridis']
cols = cmap(norm(x))
plt.scatter(x, x, facecolors='none', edgecolors=cols) Atcold commented on Nov 8, 2022 Okay, rephrasing.
It's impossible without additional code.
Can we add your code to this function when c=x, facecolors='none' are specified?
It seems like reasonable behaviour to me. jklymak commented on Nov 8, 2022 scatter already has a lot of implicit behaviour around colormapping that has accrued over the years. I would personally lean towards the less-used case needing a few more lines of explicit user code rather than more implicit behaviour. facecolors='none' doesn't necessarily mean "please add edgecolors".
A counter proposal would be that plt.scatter(x, y, facecolors='none', edgecolors=x) would do the mapping (just like c=x) for the edgecolor. Thats what I tried first, but it didn't work. I think it's usually clear that x is not an array of colors for scatter so there wouldn't be ambiguity: Nx3 or Nx4 would be treated as colors. Nx1 would be passed through norm and colormap. Reacted by Alfredo Canziani, Kevin Mi, ewpardijs and alkan tacaswell commented on Nov 8, 2022 The default behavior is for the edge colors to follow the face color, layer with if you specify c we ignore the facecolor. I agree with @Atcold that still doing the mapping but just setting the edges is the expected behavior, however I think it might be messy internally.
I have a worry about allowing the edge color to be mapped independently. On one hand you could say the face color and edge color must share the name norm and cmap, but if it will soon seem reasonable to relax that and let each have their own and new we will end up with a ScalarMappable that is really two and the question of how you make a color bar out of that gets complicated....
Sorry for rambling post. jklymak commented on Nov 9, 2022 Oh, you are right - we indeed show and color the edges. In that case, I think I also agree with @Atcold that this is a "bug". Not sure how easy it is to fix without breaking the brittle dance we do here. Atcold commented on Nov 10, 2022 Rather than having the edge colours ‘follow’ the face colours, we could have a mapping that is given to both the face and edge. If either face or edge colour is specified, that takes precedence over the mapping. tacaswell commented on Nov 11, 2022 That is reasonable, but there is a fair amount of book-keeping that will have to be reworked through (having the edge color follow the face it is easier in the case where you re-set the data / colormap / norm in that we only have to track it one place internally and the rest "just works"). Because scatter both takes mappings and fixed colors iirc using 'face' as the color of the edges also helps in that case.
The work here is:
- switch from using
'face' as the edge color by default in the case where there is color
- make sure the edge color also being mapped does not mean we do the data -> norm -> colormap pipelines twice
- add tests of making empty markers with edges that are still mapped
- extend one of the scatter examples to show this
- need to make sure that edge color still tracks the face color is all of the cases where it currently does when changed post-hoc (which maybe annoying, but I think would be an API change). I suspect this will be the thorniest part of this work.
I think this is a good first issue because there is no API design here (that this should work seems non-controversial), but medium difficulty as it will require reading and understanding the internals of both the scatter method and the collections (sub-)classes to sort out how to do the book keeping correctly. This is not a good issue for someone new to Python, but is a good issue for someone new to working on Matplotlib. Reacted by Alfredo Canziani greglucas commented on Nov 11, 2022 This should probably follow similar semantics to the pcolormesh face/edge discussion as well. xref to this comment which has a nice summary plot of the combinations: #18480 (comment) Reacted by Alfredo Canziani GraceJiang0312 commented on Dec 5, 2022 non-empty face colors and non-empty edge colors:
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(0, 10)
plt.scatter(x, x, c=x, facecolors='none')

empty face colors and non-empty edge colors:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm
x = np.arange(0, 10)
norm = plt.Normalize(0, 10)
cmap = matplotlib.cm.get_cmap('viridis')
cols = cmap(norm(x))
plt.scatter(x, x, c='none', facecolors='none', edgecolors=cols)

wu-theodore commented on Dec 6, 2022 Hello, new contributor here. I'd like to outline my exploration/thoughts so far and invite discussion to clarify the source of the issue and what the change would look like.
I took a look at the internals of the scatter method under _axes.py and the edgecolor parameter seems to be set by _parse_scatter_color_args. There, the specified edge color argument precedence seems to be as follows:
- kwargs['edgecolor']
- edgecolors (is an explicit kw argument in scatter())
- kwargs['color'] (==kwcolor)
- 'face' if not in classic mode else None
Now, the proposed change seems to be related to the case where edgecolor and color are implicitly None. In this case, we would skip the first 3 cases and enter the 4th case, which would be to set edgecolor = 'face'. As far as I can see, this condition is implemented by
if edgecolors is None and not mpl.rcParams['_internal.classic_mode']:
edgecolors = mpl.rcParams['scatter.edgecolors']
A fix then would be to add another check prior to this conditional that checks for whether c is defined, and if so, try to assign c to edgecolor (without breaking duplicating the data->norm->colormap pipeline, of course).
I suppose that alternatively, we could instead handle edgecolor value assignment when the face color is none within cbook._combine_masks from
if plotnonfinite and colors is None:
c = np.ma.masked_invalid(c)
x, y, s, edgecolors, linewidths = \
cbook._combine_masks(x, y, s, edgecolors, linewidths)
This would more closely resemble what is already being done when the facecolor (i.e., colors) is None.
As someone new to working with MatPlotLib, I am not sure about the ramifications of this change and would like some input from a more experienced contributor. Specifically, would this create any issues with the bookkeeping in collections? What kind of unexpected implicit behaviour might arise from such a change? Reacted by Alfredo Canziani Atcold commented on Dec 6, 2022
non-empty face colors and non-empty edge colors:
import numpy as np import matplotlib.pyplot as plt
x = np.arange(0, 10) plt.scatter(x, x, c=x, facecolors='none')

empty face colors and non-empty edge colors:
import numpy as np import matplotlib.pyplot as plt import matplotlib.cm
x = np.arange(0, 10) norm = plt.Normalize(0, 10) cmap = matplotlib.cm.get_cmap('viridis') cols = cmap(norm(x)) plt.scatter(x, x, c='none', facecolors='none', edgecolors=cols)

I'm not sure what this message means.
I was reporting a bug. melissawm commented on Jan 9, 2023 claytharrison commented on Feb 19, 2025 Sorry but how does #29130 close this? Nothing in this issue is about problems using c and facecolors at the same time , it's about being unable to use c for just edgecolors while setting facecolors to "none". It's still impossible to have empty scatter symbols using just keyword args - which gets annoying when using pyplot through wrappers like e.g. Xarray's. story645 commented on Feb 19, 2025
Sorry but how does #29130 close this
Sorry, GitHub auto close behavior, should have double checked what it was auto closing. giboul commented on Jul 21, 2025 Hello everyone, new matplotlib lover here
For anyone interested in a solution that could be a one-liner, I was satisfied with the following
Edit: this might be less useful than @jklymak's answer when a colorbar is needed.
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(0, 10)
colors = plt.cm.viridis((x-x.min())/(x.max()-x.min()))
plt.scatter(x, x, c='none', edgecolors=colors)
plt.show()
[matplotlib-3.10.3] ashishamith commented on Aug 20, 2025 Hello 👋, I’d like to take this up. Planning to submit a small patch that handles empty markers properly in scatter(). Please let me know if that works. cairibep commented on Oct 22, 2025 Hello!
I've been trying to solve this problem internally and have had some interesting results. I'd like to request a PR on this.
Can I take on this task? added a commit that references this issue on Jan 14, 2026This issue will close once commit bcdca3b is merged into the 'main' branch.bcdca3b timhoffm commented on Jan 14, 2026 I'm not clear what the target state is here; and thus also not whether moving away from "face" and doing some "bookkeeping" strategy is reasonable.
Citing myself #30962 (comment):
I'd expect that everything should be handled in Collections, i.e. the interaction between data, (norm, cmap), and facecolors, edgecolors. Any logic outside of Collections is likely bound to fail with inconsistent behavior if some properties of the collection are updated later.
Ideally, there's one universal logic in Collection tying everything together, but it might be that PathCollection requires a different logic, or in theory even a PathCollection representing makers might have different requirements. This is something that needs much more in-depth investigation.
What I first want to see is systematic summary of the logic how data, (norm, cmap), and facecolors, edgecolors resolve to the eventual colors, both what the current state is and what the desired target state is.
Overall, I'm very sceptical on having state. Ideally, everything is determined dynamically at draw-time. So I would fundamentally question the current Collection architecture with update_scalarmappable()
|
def update_scalarmappable(self): |
. If recomputation is an issue, caching is still the lesser evil to managing state.
I'm removing "Good first issue" as I think this needs a deep architecture investigation. added 2 commits that reference this issue on Jan 15, 2026This issue will close once commit 4af2e00 is merged into the 'main' branch.4af2e00 This issue will close once commit 05f7443 is merged into the 'main' branch.05f7443
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|