Skip to content

[Bug]: Cannot use empty markers in scatter #24404

Open

Description

@Atcold

Bug 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

image

Expected outcome

Empty markers.

Matplotlib Version

3.5.1

Activity

  1. jklymak commented on Nov 8, 2022

    @jklymak
    Member

    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)
  2. Atcold commented on Nov 8, 2022

    @Atcold
    Author

    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.

  3. jklymak commented on Nov 8, 2022

    @jklymak
    Member

    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.

  4. tacaswell commented on Nov 8, 2022

    @tacaswell
    Member

    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.

  5. jklymak commented on Nov 9, 2022

    @jklymak
    Member

    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.

  6. added
    API: consistencyConsistency of the matplotlib API, including naming, behavior, defaults, …
    on Nov 9, 2022
  7. Atcold commented on Nov 10, 2022

    @Atcold
    Author

    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.

  8. added this to the v3.7.0 milestone on Nov 11, 2022
  9. added
    Difficulty: Mediumhttps://matplotlib.org/devdocs/devel/contribute.html#good-first-issues
    🌱 Good first issueOpen a pull request against these issues if there are no active ones!
    on Nov 11, 2022
  10. tacaswell commented on Nov 11, 2022

    @tacaswell
    Member

    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.

  11. greglucas commented on Nov 11, 2022

    @greglucas
    Contributor

    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)

  12. GraceJiang0312 commented on Dec 5, 2022

    @GraceJiang0312

    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')

    scatter_facecolor

    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)

    scatter_edgecolor

  13. wu-theodore commented on Dec 6, 2022

    @wu-theodore

    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?

  14. Atcold commented on Dec 6, 2022

    @Atcold
    Author

    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')

    scatter_facecolor

    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)

    scatter_edgecolor

    I'm not sure what this message means.
    I was reporting a bug.

  15. 3 remaining items

  16. melissawm commented on Jan 9, 2023

    @melissawm
    Member

    Hello @Gairick52 - as pointed above we don't assign issues. As long as there is no Pull Request associated with the issue, you can feel free to work on it. Please take a look at our Contributing guide and if you have specific questions you can also join our gitter room or one of our monthly New contributor meetings (on Zoom). Cheers!

  17. claytharrison commented on Feb 19, 2025

    @claytharrison

    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.

  18. story645 commented on Feb 19, 2025

    @story645
    Member

    Sorry but how does #29130 close this

    Sorry, GitHub auto close behavior, should have double checked what it was auto closing.

  19. giboul commented on Jul 21, 2025

    @giboul

    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]

  20. ashishamith commented on Aug 20, 2025

    @ashishamith

    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.

  21. cairibep commented on Oct 22, 2025

    @cairibep

    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?

  22. added a commit that references this issue on Jan 14, 2026
    bcdca3b
  23. timhoffm commented on Jan 14, 2026

    @timhoffm
    Member

    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.

  24. removed
    🌱 Good first issueOpen a pull request against these issues if there are no active ones!
    on Jan 14, 2026
  25. added 2 commits that reference this issue on Jan 15, 2026
    4af2e00
    05f7443
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    API: consistencyConsistency of the matplotlib API, including naming, behavior, defaults, …Difficulty: Mediumhttps://matplotlib.org/devdocs/devel/contribute.html#good-first-issues

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions