GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Implement head resizing (and reversal) for larrow/rarrow/darrow by CharlieThornton33 · Pull Request #29998 · matplotlib/matplotlib Conversation
|
|
Hi everyone, I was just wondering whether someone would be willing to have a look at this PR? I feel like it's in a complete or nearly-complete state but need some guidance - the two failing CI jobs seem to be:
Any help would be greatly appreciated. Thanks! |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
PR-cleanliness: This is a check so that we don't merge commits with unneeded images, which would increase repo size. You can either squash and force-push. Or ignore this and we'll squash-merge at the end. Ignore the macos test failure. It's unrelated. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Thank you for getting back so quickly! Would it be okay if you handled the squashing - I'm relatively new to git, and I'm not sure of the best method to use for squashing to keep a relatively clean but traceable commit history? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Looking at the arrows in the test image, https://github.com/matplotlib/matplotlib/pull/29998/files#diff-ff8adb592a399d60ee5c93fbff7552182f53b74be4573ca21db404ef3c3a6673, the outline sometimes cuts through the text, which must not happen.
From plotting
plt.text(0.5, 0.5, 'text', size=24, bbox=dict(boxstyle="rarrow", fc="w", ec="k"),)
plt.text(0.5, 0.5, 'text', size=24, bbox=dict(boxstyle="square", pad=0, fc="C0", ec='none'))
we can see that the original calculation had a shaft with the length of the text, i.e. the arrow head starts at -pad from the end of the text.
This setting was ok for the hard-coded parameters, but when head_size and head_angle can vary, we have to invest more into a good positioning of arrow head.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Since I'm going to need to make some relatively major changes to the contents of this PR (a redo of the arrow drawing code and a rewrite of its unit test), would it be better for me to amend my last commit (d9b0b32) as suggested in the contributing guide, or make a few new commits? I'm not quite sure how to add more commits to an open PR - do I just push the changes to my development branch? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@CharlieThornton33 yes you can just push more commits to your current branch and they will appear in this PR. Since you already have several commits they will ultimately need squashing one way or the other (and I see you already asked that we handle that), so I do not think you gain anything from the |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I've just redone the padding (only for It's also a bit concerning that the CI workflow seems to run on every commit to this PR - am I okay to disable tests in my commit messages until the one where I write the new test to save on CI resources? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I've just noticed - all of the |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@CharlieThornton33 I wouldn't worry about the CI failing, but if they are still running when you push another commit they will be interrupted and re-started. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
I'm a bit confused about how to test one of @QuLogic's review notes: How would it be possible to test modification of a public attribute? Would it just be initialising the box with |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I believe @QuLogic's comment is fuelled by the idea: What if somebody created a boxstyle class and then modified it arrowstyle = LArrow(head_angle=45) plt.text(0, 0, "text", bbox={'boxstyle': arrowstyle} arrowstyle.head_angle=30 plt.show() Since the class stores all parameters in attributes as is and all of the logic is in |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| # Pad to position original box and its margins exactly inside arrow shaft | ||
| padding_offset = (0.5 * pad) + (0.25 * mutation_size) | ||
| x0 -= padding_offset |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
| # Pad to position original box and its margins exactly inside arrow shaft | |
| padding_offset = (0.5 * pad) + (0.25 * mutation_size) | |
| x0 -= padding_offset | |
| # Pad to position original box and its margins exactly inside arrow shaft | |
| padding_offset = (0.5 * pad) + (0.25 * mutation_size) | |
| x0 -= padding_offset |
I don't understand this. I thought the original box starts at
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I'm not entirely sure myself why this is needed. When writing the code for padding, I noticed that padding just via print(<text object>.get_bbox_patch().get_path()) to look numerically at the positioning of the drawn vertices. From this, I found that there was a discrepancy present that depended only on self.pad linearly, then used linear regression to get this empirical expression for padding_offset.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
On main, without setting head_length, etc, the example from the What's new note looks like:

whereas here, it looks like:

and if we revert the extra padding_offset, it goes closer to before:

but it's still not a perfect match:

So I don't quite understand the math added here, and I guess it depends on how close we want it to match the old version. Currently, it doesn't at all, but without padding_offset, it's still not exactly the same either.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Hi, I was wondering how I should proceed with this PR - I can't think of anything else that needs finishing for it. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
The result looks good. Though, I would like to understand the calculation #29998 (comment). Relying on empirical values that we don’t understand poses a danger that they are incorrect for some cases. We need to debug the logic step by step and either extract the coordinates to draw the picture by hand, or draw points/rectangles (see e.g. #29833 (comment)) to understand what’s going on. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
|
||
| .. plot:: | ||
| :include-source: true | ||
| :alt: Six arrow-shaped text boxes, all containing the text "Arrow". The top left arrow has a shorter head than default, while the top right arrow a longer head. The centre left double arrow has a "road-sign" shape (head as wide as the arrow tail), while the centre right arrow has a "backwards" head. The bottom left arrow has two heads which are larger than default, and the bottom right arrow has a head narrower than its tail. |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
You should be able to wrap this as long as you wrap the first line.
:alt:
Six arrow-shaped text boxes, ...
more text ...
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| # Pad to position original box and its margins exactly inside arrow shaft | ||
| padding_offset = (0.5 * pad) + (0.25 * mutation_size) | ||
| x0 -= padding_offset |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
On main, without setting head_length, etc, the example from the What's new note looks like:

whereas here, it looks like:

and if we revert the extra padding_offset, it goes closer to before:

but it's still not a perfect match:

So I don't quite understand the math added here, and I guess it depends on how close we want it to match the old version. Currently, it doesn't at all, but without padding_offset, it's still not exactly the same either.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
my main concern is consistency. The new code must produce reasonable results, I.e. text must be inside the arrow outline, for all parameter combinations. The code should be not too complex (try to avoid too much special-casing). Matching existing behavior is only third - note that these are outlines around a text, and are defined via the text position. The tip position and arrow size are not explicitly set but depend on font metrics and size. I therefore think it is permissible to not exactly reproduce existing behavior. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Discussed an the call. Keeping consistency on these is not super important, no one should be extracting quantitative information from these arrows.
Personally I think the smart algorithm will probably look better, but fixed pad is my second choice. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Just as a reminder, I've drawn all(?) the options in #29998 (comment), #29998 (comment). The bounding-box options (4), (5) are generally not as nice as thought; or at least not better than the simple ones (1), (2). I believe if we want to be smart, we must be even smarter - possibly a combination of cases or some more advanced distance metric for tip-to-text. Would it be an option to mark the behavior as provisional and allow us the freedom to later change the head positioning algorithm? - Only slight caveat is that we change the behavior for existing users now (one-time break is justified to enable generalization), but we would possibly break them again and leave them in a limbo state in between. Feature flags (#30519) would help with these, but we'd still have to implement them soon. Overall, possibly the sane decision is to defer this topic to after the 3.11 release. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
As a meta comment - if the goal is "to bring attention to a certain region on the axes" this doesn't seem like the right tool. I'd make a precisely placed arrow and add text over top and adjust the text to fit the aesthetics of the arrow. Trying to reverse engineer that behaviour into something precisely depending on a text bounding box doesn't seem like a good API. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
That is true and the reason why we can do a breaking change in size/positioning. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
The following patch implements the "tangent shaft" rules suggested above. The resulting shafts vary continuously with head width and angle; the only somewhat awkward case is the one of an arrow shaft longer than the box itself (at the beginning of the varying angle example); it could easily be changed to be drawn as a "full triangle" instead if we decide to do so. the patchdiff --git i/lib/matplotlib/patches.py w/lib/matplotlib/patches.py index 0c8d6b5fee..e7b3b77362 100644 --- i/lib/matplotlib/patches.py +++ w/lib/matplotlib/patches.py @@ -2526,7 +2526,7 @@ class BoxStyle(_Style): class LArrow: """A box in the shape of a left-pointing arrow.""" - def __init__(self, pad=0.3): + def __init__(self, pad=0.3, head_width=1.5, head_angle=90): """ Parameters ---------- @@ -2534,25 +2534,46 @@ class BoxStyle(_Style): The amount of padding around the original box. """ self.pad = pad + if head_width < 0: + raise ValueError("'head_width' must be nonnegative") + self.head_width = head_width + if head_angle % 360 == 0: + raise ValueError("'head_angle' must be nonzero") + self.head_angle = head_angle def __call__(self, x0, y0, width, height, mutation_size): - # padding + # padding & padded dimensions pad = mutation_size * self.pad - # width and height with padding added. - width, height = width + 2 * pad, height + 2 * pad - # boundary of the padded box + dx, dy = width + 2 * pad, height + 2 * pad x0, y0 = x0 - pad, y0 - pad, - x1, y1 = x0 + width, y0 + height + x1, y1 = x0 + dx, y0 + dy - dx = (y1 - y0) / 2 - dxx = dx / 2 - x0 = x0 + pad / 1.4 # adjust by ~sqrt(2) + head_dy = self.head_width * dy + mid_y = (y0 + y1) / 2 + shaft_y0 = mid_y - head_dy / 2 + shaft_y1 = mid_y + head_dy / 2 - return Path._create_closed( - [(x0 + dxx, y0), (x1, y0), (x1, y1), (x0 + dxx, y1), - (x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx), - (x0 + dxx, y0 - dxx), # arrow - (x0 + dxx, y0)]) + cot = 1 / math.tan(math.radians(self.head_angle / 2)) + + if cot > 0: + # tip_x is chosen s.t. the angled line moving back from the tip hits + # i) if head_width > 1: the box corner, or ii) if head_width < + # 1 the box edge at the point giving the correct shaft width. + tip_x = x1 + cot * min(dy, head_dy) / 2 + shaft_x = tip_x - cot * head_dy / 2 + return Path._create_closed([ + (x0, y0), (shaft_x, y0), (shaft_x, shaft_y0), + (tip_x, mid_y), + (shaft_x, shaft_y1), (shaft_x, y1), (x0, y1), + ]) + else: + dx = -cot * (head_dy - dy) / 2 # cot < 0! + return Path._create_closed([ + (x0 + min(dx, 0), y1), (x0 - max(dx, 0), shaft_y1), + (x0 - max(dx, 0), shaft_y0), (x0 + min(dx, 0), y0), + (x1 - min(dx, 0), y0), (x1 + max(dx, 0), shaft_y0), + (x1 + max(dx, 0), shaft_y1), (x1 - min(dx, 0), y1), + ]) @_register_style(_style_list) class RArrow(LArrow): @@ -2565,41 +2586,45 @@ class BoxStyle(_Style): return p @_register_style(_style_list) - class DArrow: + class DArrow(LArrow): """A box in the shape of a two-way arrow.""" - # Modified from LArrow to add a right arrow to the bbox. - - def __init__(self, pad=0.3): - """ - Parameters - ---------- - pad : float, default: 0.3 - The amount of padding around the original box. - """ - self.pad = pad + # Modified from LArrow to add a right arrow to the bbox; see comments above. def __call__(self, x0, y0, width, height, mutation_size): - # padding + # padding & padded dimensions pad = mutation_size * self.pad - # width and height with padding added. - # The width is padded by the arrows, so we don't need to pad it. - height = height + 2 * pad - # boundary of the padded box - x0, y0 = x0 - pad, y0 - pad - x1, y1 = x0 + width, y0 + height + dx, dy = width + 2 * pad, height + 2 * pad + x0, y0 = x0 - pad, y0 - pad, + x1, y1 = x0 + dx, y0 + dy - dx = (y1 - y0) / 2 - dxx = dx / 2 - x0 = x0 + pad / 1.4 # adjust by ~sqrt(2) + head_dy = self.head_width * dy + mid_y = (y0 + y1) / 2 + shaft_y0 = mid_y - head_dy / 2 + shaft_y1 = mid_y + head_dy / 2 - return Path._create_closed([ - (x0 + dxx, y0), (x1, y0), # bot-segment - (x1, y0 - dxx), (x1 + dx + dxx, y0 + dx), - (x1, y1 + dxx), # right-arrow - (x1, y1), (x0 + dxx, y1), # top-segment - (x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx), - (x0 + dxx, y0 - dxx), # left-arrow - (x0 + dxx, y0)]) + cot = 1 / math.tan(math.radians(self.head_angle / 2)) + + if cot > 0: + tip_x0 = x0 - cot * min(dy, head_dy) / 2 + shaft_x0 = tip_x0 + cot * head_dy / 2 + tip_x1 = x1 + cot * min(dy, head_dy) / 2 + shaft_x1 = tip_x1 - cot * head_dy / 2 + return Path._create_closed([ + (shaft_x0, y1), (shaft_x0, shaft_y1), + (tip_x0, mid_y), + (shaft_x0, shaft_y0), (shaft_x0, y0), + (shaft_x1, y0), (shaft_x1, shaft_y0), + (tip_x1, mid_y), + (shaft_x1, shaft_y1), (shaft_x1, y1), + ]) + else: + dx = -cot * (head_dy - dy) / 2 # cot < 0! + return Path._create_closed([ + (x0, y0), + (x1 - min(dx, 0), y0), (x1 + max(dx, 0), shaft_y0), + (x1 + max(dx, 0), shaft_y1), (x1 - min(dx, 0), y1), + (x0, y1), + ]) @_register_style(_style_list) class Round: This needs to be rebased appropriately to give credit to the earlier authors (e.g. by committing their tests and docs after proper editing); as usual I'll just post the patch here for now, feel free to pick it up and turn it into a PR if I don't do that first. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
This is superseeded by #31198. @CharlieThornton33 thanks for your detailed work, it's included (with attribution) in the follow up. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
[ENH]: "Road sign" boxstyle/annotation
There was an error while loading. Please reload this page.