| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
|
👍 Holding off approving for a bit more discussion about the name. The appveyor failure is not real. |
Sorry, something went wrong.
|
@ImportanceOfBeingErnest Can you please champion your own PR ? |
Sorry, something went wrong.
|
I think what can be discussed here is the following: Should the same argument that is introduced here be used later on to distinguish between several inbuild marker modifications, for example: MarkerStyle("v", normalize_path="old-style")
MarkerStyle("v", normalize_path="area")
MarkerStyle("v", normalize_path="unit-square")
MarkerStyle("v", normalize_path="perceptually-uniform-01")
? |
Sorry, something went wrong.
|
One of the places we have historically gotten our selves into trouble is trying to fold too much functionality into to any given layer. From the point of view of MarkerStyle we can't stop normalizing by default (to maintain API), but the simplest extension is to add a "just don't touch it" and rely on the user to do whatever normalization they want. However, I see the argument that making it a bool paints us into a corner we may want to be able to get out of [1]. I propose we start with: normalization: str, {'unit-scale', 'none'}, optional
In the future we will be able to add more known strings in a safe way and to allow callables to be passed. This rhymes with patterns in our codebase and numpy/scipy. [1] sometimes it seems that in programming that the cardinality of options seems to be 0 (no option), 2 (yes/no), or infinite |
Sorry, something went wrong.
|
Ok, let's think this through. normalization="unit-scale" would be the default. It would leave everything as it is currently. One drawback here is that "unit-scale" may create false expectations for some predefined markers, which are not scaled into the unit square (as seen from the examples in #16623). So, for now: MarkerStyle("D", normalize="unit-scale") # leaves "D" unchanged, not scaled
MarkerStyle(Path(...), normalize="unit-scale") # scales path to unit square
MarkerStyle("$f$", normalize="unit-scale") # scales path to unit square
MarkerStyle("D", normalize="none") # leaves "D" unchanged, not scaled to unit square
MarkerStyle(Path(...), normalize="none") # does not scale path
MarkerStyle("$f$", normalize="none") # scales path to unit square
# (because there is no other canonical scale)
In addition, in the future MarkerStyle("D", normalize="area") # scales "D" to unit area (==leaves it unchanged
since it already is 1 unit^2 in size)
MarkerStyle("d", normalize="area") # scales "d" to unit area
MarkerStyle(Path(...), normalize="area") # might try to calculate the area from the path -
what happens if it fails?
MarkerStyle("$f$", normalize="area") # same as above
Is this how we want to have it? |
Sorry, something went wrong.
|
Hmm, you are correct that the letter-named markers are not "unit-scale", maybe we should keep the default behavior as "default" and then add "unit-scale" that makes everything fit inside of a unit-box (centered at 0) for both Path and string based input, "none" which imposes no normalization on Paths that are passed in, and lets the string-based do what it does. In the future we can add "bounding-box-area" and "filled-area" normalization values that apply to everything? |
Sorry, something went wrong.
|
I now went with normalization : str, {'classic', 'none'}, optional, default: "classic"
I think "default" would be fine for now as well, but suppose the defaults are changed in the future, that notion might become ambiguous. In this respect "classic" conveys better what it is... it's not modern, it's not great, but it's classic, like it's always been. I am not implementing "unit-scale" here in this PR. This would be significantly more work
|
Sorry, something went wrong.
|
I know I'm a bit late to the conversation (had to escape the country because of covid), but I agree that something like "classic" would be best to convey the current behavior.
I haven't settled on good, short names for each of these behaviors yet, but I agree that something like:
seem useful enough to consider as future options. Just would like to bring up that if (1)-(3) are on the table, then we probably want to not call any one of them in particular "unit-scale" right now, as this description fits them all three pretty well.
I can comment on the difficulty of these calculations, having just written code to do the bounding box calculation for custom paths with finite-width stroked edges. The machinery for "bounding-box-area" is already in place (requires just some simple helpers added to bezier.py in #16607). The machinery for "filled-area" would need to be added. The best way to do this would be iterative refinement (calculating area of approximating polygon to the curve for increasingly fine discretizations until some tolerance is reached). I'd estimate a very dense collection of 2-4 helper functions would need to be added to path.py/bezier.py to accomplish this. On the other hand, this is code I've written before, and it can be easily tested and would be relatively self-contained. I think there's a very strong argument to be made that the scaling of the markers for each of these options should not take into account the markeredgewidth at all. Which is good, because while adding finite edges into the bounding box calculation requires no extra work (code already exists in #16607), adding finite edges to the "filled-area" calculation would require implementing some pretty state-of-the-art stuff, that feels pretty out of scope for the MPL library. |
Sorry, something went wrong.
|
Instead of
I would propose just adding a "classic scaling" attribute to each "marker" in MarkerStyle.markers. Then the transform property of the marker could be much more neatly folded into __init__, since it would use centralized code determined by the normalization switch. On the other hand
Chiming in as outsider (since I've already stuck my nose in here) that I agree this would fit better in a separate PR. |
Sorry, something went wrong.
|
@brunobeltran Is it fair to summarize your position as: a) this is a non-trivial problem b) this lays down the skeleton of the public API we are going to need c) this should go in about as-is so we can build on it? |
Sorry, something went wrong.
There was a problem hiding this comment.
modulo tests passing and a rebase.
Sorry, something went wrong.
|
Previously, the unscaled paths of the builtin markers were effectively not part of the public API; with this PR, they are. Do we want to change the size of the unscaled versions of the builtin markers while this is still possible? |
Sorry, something went wrong.
|
Mhh. .get_path and .get_transform have always been public, rigth? So changing those would be a breaking change? |
Sorry, something went wrong.
Yup 👍
This doesn't seem true to me? Sure now users might rely on the size of a marker given normalization="none", but we can easily document in the future that normalization="none" will default to "classic" for the case of built-in markers. I mean, you could argue that the path/transform were always publicly available (MarkerStyle.get_path()/MarkerStyle.get_transform()). This is especially true since, contrary to what was initially proposed above,
would not actually leave "D" unchanged, but scale it down by a factor of 1/sqrt(2) to fit into the unit square. EDIT: @ImportanceOfBeingErnest beat me to this, agree with his comment. |
Sorry, something went wrong.
|
Fair enough, they were public if one tried hard enough, but this makes them much more easily accessible. |
Sorry, something went wrong.
|
By "more easily accessible" you mean "discoverable" or "useful"? Or else, I might not really understand the argument because accessibility does not change here. Arguably there is a high reduncancy between the path and the transform. So in principle one could define any marker just by a path. In that case, one could deprecate get_transform; is that what you have in mind? |
Sorry, something went wrong.
|
Missed the call today, but I would like to say that I highly prefer the original proposal, for a few reasons:
My alternate proposal (see #16891), would be to instead provide the user with two convenience arguments normalization and centering that basically correspond to the special cases where transform is Affine2D().scale and Affine2D().translate, but with the amount of scaling/transforming chosen to be something generally useful (scale to area=1, center of mass to origin, etc). If the user wants to do something more complicated, it seems like it would be less work for them to just transform the path themselves? |
Sorry, something went wrong.
Can you clarify what the "original proposal" is? |
Sorry, something went wrong.
Sorry: by "original proposal" I meant @ImportanceOfBeingErnest's proposal to have the user pass in an arbitrary path if they want, and to only provide them with some helpers for rescaling/etc using string inputs (instead of asking them to control the marker's ._transform, which is currently just an implementation detail of the existing built-in marker types). The built-in paths would by default not be affected but if you requested e.g. normalization='area', then it would work just as well for the built-in paths as for a custom path that way. Otherwise, (if we instead ask the user to pass in a transform object) in order to say, normalize "*", the user would have to know what transform is currently being applied to it, or otherwise do something that feels silly like ``` star_path = MarkerStyle('*').get_path() normed_star = MarkerStyle(star_path, tranform='unit-area') ``` |
Sorry, something went wrong.
|
I guess adding a transform argument (instead of normalization) is totally possible. I just wonder if that doesn't make things unnecessarily confusing. While we do have transform as argument elsewhere in the library, it's always the actual transform of the artist.
|
Sorry, something went wrong.
|
I don't think that a transform kwarg is any more confusing than a set_transform method. Allowing transform to take additional string arguments or special transform subclasses for paths that achieve the normalizations doesn't seem too confusing. Its just strange to allow access to a path._transform object, allow the user to path.set_transform, but then make the init kwarg normalization with no direct access to transform. |
Sorry, something went wrong.
|
I just don't think the user should have access to ._transform at all. It's currently (and feels like it should remain) an implementation detail (edit: am I wrong about this?). They can already pass in arbitrary paths, the point of normalization kwarg was just to make their lives easier I think, not to give them a way to tap into MarkerStyle's implementation? |
Sorry, something went wrong.
|
@brunobeltran OK, so by "Original proposal" you didn't mean this PR? I guess I agree that an expert user can just set a transform before passing to MarkerStyle so its not clear why the set_transform needs to be implemented here. Whether there should be a bunch of normalization and offset options is a different question. |
Sorry, something went wrong.
|
The transform is not an "implementation detail", because .get_transform is public. As said, one could deprecate it, if that is desired. Indeed, .set_transform is not strictly needed. But it's a nice to have feature that allows to scale or distort existing markers. I.e. marker=MarkerStyle("d")
marker.set_transform(marker.get_transform() + Affine2D().rotate_deg(30))
is just quicker / less convoluted than marker = MarkerStyle("d")
marker = MarkerStyle(marker.get_path().transformed(marker.get_transform()+ Affine2D().rotate_deg(30)), normalization="none")
This assumes .get_transform is not deprecated. If it is, the above is not possible any more and one would need to construct the complete marker path from scratch. |
Sorry, something went wrong.
|
@ImportanceOfBeingErnest You're right, if we get rid of the [get/set]_transform interface, for each new marker you want, you would need to pass the fully transformed Path to MarkerStyle. But you definitely don't have to "construct the marker from scratch". The convoluted API you're describing is still assuming that ._transform exists in the first place. Actually deprecating .get_transform() would be quite the pain. It was clearly originally included to maintain a uniform interface with Artist's, and is used throughout the collections and backend code. On the other hand, we can easily leave it in place and just have it always return IdentityTransform. All we have to do is a really simple code change to have the "old" transforms be applied during marker construction. Then the Path will be enough, you never need to access the transform at all (although it will still be possible to support all that old backend/collections code that depends on it). d_path = MarkerStyle("d").get_path()
rot_d = Affine2D().rotate_deg(30).transform_path(d_path)
marker = MarkerStyle(rot_d)
or similar. |
Sorry, something went wrong.
I do mean this PR (and its associated discussion), as opposed to the alternate proposal from the call last week that we instead use a Transform kwarg.
Maybe whether there should be "a bunch" of normalization and offset options is a different question (I propose two kwargs, "normalization" and "centering", not "a bunch"). However, this PR adds a normalization kwarg. So that's definitely a question for right now. I am definitely in favor of a "normalization" kwarg instead of forcing a user to pass a Transform, which is currently the only other proposal. |
Sorry, something went wrong.
|
Note that deprecating .get_transform() is much easier than having it return the identity transform. The latter would be an API change. Also mind that .get_path() suddenly returning a different path would be an API change as well. The only clean way to do this would be to give both a new kwarg that sets the return, in the sense of I'm not convinced that this is really needed. Also, having the transform on a generic path is a nice thing and keeps the code short. The transform would also be useful for implementing any of the other types, which mostly are just a scaled or translated version of the existing ones. |
Sorry, something went wrong.
|
I've played around with custom markers more this week, and I am pretty convinced that allowing a user to modify the marker in-place at all is not ideal. Some issues:
actual_path = marker.get_transform().transform(marker.get_path()) width = actual_path.get_extents().width # compute something about path marker.set_transform(marker.get_transform() + Affine2D().scale(1/width)) and at that point, I might as well (i.e. it's not really extra typing, and definitely feels more intuitive/discoverable) to just do path = marker.get_path(transformed=True) width = path.get_extents().width # compute something about path new_path = Affine2D().scale(1/width).transform_path(path) new_marker = MarkerStyle(new_path) where transformed=True could be added as a temporary kwarg while we phase out the old return values, as you propose above. That said, I could easily be persuaded if I was given a use case where modifying the marker after creating it is useful, I just wasn't able to find one.
I probably just don't understand the matplotlib development constraints well enough yet, but these both seem like API changes to me? In one we remove a method, in another we change it's return value? It's just as easy to add a kwarg and a deprecation warning, saying that in the next version, we'll be switching to the transformed=True behavior by default and removing the transformed kwarg. The development burden to remove .get_transform() is much higher, since it's used in the backend in a duck-type-y way, and so while I could probably remove all internal instances of it, I'm not confident enough in our test coverage to be sure I wouldn't break something. |
Sorry, something went wrong.
|
@ImportanceOfBeingErnest @brunobeltran is there interest in pushing this forward, either in the context of this PR or a new one? I guess the original motivation makes it seem that this would be generally helpful, though I'm not sure we agreed on the API. |
Sorry, something went wrong.
|
This functionality was implemented in #20914 in a way inline with #16773 (comment) (adding a transform keyword but composing it with the default transform so slightly differently parameterized than this proposal, but I think with the same capabilities). Thank you for your work on this @ImportanceOfBeingErnest and @brunobeltran . I do hope we hear from both of you again! |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
PR Summary
This PR allows markers to be non-normalized or transformed:
p = Path([[-1,-1],[1,-1],[0,1]]) ax.scatter(...., s=20**2, marker=MarkerStyle(p)) ax.scatter(...., s=1, marker=MarkerStyle(p.transformed(Affine2D().scale(10)), normalize_path=False))ax.scatter(...., s=20**2, marker=MarkerStyle("s")) m = MarkerStyle("s") m.set_transform(Affine2D().scale(20)) ax.scatter(...., s=1, marker=m)Why is this useful?
Until now, it is impossible use arbitrary custom markers. Custom markers are always normalized to the unit square. So for a given path, the size and center point of the marker are changed by matplotlib.
Common problems:
You want to rotate a given marker without it changing size. See
You want to create a custom marker that is sized relative to another marker, see e.g. https://matplotlib.org/gallery/lines_bars_and_markers/scatter_piecharts.html, without setting its size via the markersize property of the artist, or that is off-centered.
Finally, you may want to create a set of markers that are uniform in area, or perceptually uniform, as desired in Sizes of different markers are not perceptually uniform #15703
PR Checklist