Skip to content

[ENH]: Add an easy way to plot an infinite half space (e.g. with a HalfSpace patch) #28152

Open

Description

@carlosgmartin

Problem

Using axhline, one can easily plot an infinite line:

def plot_line(ax, a, b, c, **kwargs):
    """Plots the solution to a x + b y = c."""
    ax.axline((c / a, 0), (0, c / b), **kwargs)

It would be nice to have an easy way to plot an infinite half space:

def plot_half_space(ax, a, b, c):
    """Plots the solution to a x + b y ≥ c."""
    ...

Proposed solution

This could potentially be done by adding a HalfSpace patch to matplotlib.patches.

For an analogue, see Mathematica's HalfSpace and HalfPlane.

Activity

  1. timhoffm commented on May 17, 2024

    @timhoffm
    Member

    This would be a reasonable feature. PRs welcome.

  2. OdileVidrine commented on Jul 26, 2024

    @OdileVidrine
    Contributor

    I think best way to do this is to make it similar to connection patch, as this already takes in two points and draws a line between them.

  3. OdileVidrine commented on Aug 6, 2024

    @OdileVidrine
    Contributor

    Giving this another look, and currently my approach is to make the patch larger than can be displayed, and then clipping it to the bounding box. I am not sure if this is the best way to do this, but to clip to the bounding box a closed polygon must exist and rectangle seems the simplest.

  4. story645 commented on Aug 6, 2024

    @story645
    Member

    Giving this another look, and currently my approach is to make the patch larger than can be displayed, and then clipping it to the bounding box.

    This was once asked for at pydata and the proposed solution, far as I can remember, is something like half the box should be be defined in data space and the other half in axis space and I wonder if the {axhspan/axvspan} implementations might be worth looking at? https://matplotlib.org/devdocs/api/_as_gen/matplotlib.axes.Axes.axhspan.html#matplotlib.axes.Axes.axhspan

  5. OdileVidrine commented on Aug 7, 2024

    @OdileVidrine
    Contributor

    I think it is possible to use axes.get_xlim() and axes.get_ylim() to get the edges of what can be displayed, since patches are artists. However if there is no associated axes I don't know what to do, logically with no boundary it should attempt to draw an infinite shape but this could cause the program to crash or freeze.

  6. story645 commented on Aug 7, 2024

    @story645
    Member

    However if there is no associated axes I don't know what to do, logically with no boundary it should attempt to draw an infinite shape but this could cause the program to crash or freeze.

    If there's no axis then you've got the figure boundaries, and you may want to take a look at https://matplotlib.org/devdocs/users/explain/artists/transforms_tutorial.html if you haven't already

    ETA: also .get_xlim(), .get_ylim() are in data coordinate space, and I'm suggestion using axis coordinate space (which gets you things like infinite lines)

  7. OdileVidrine commented on Aug 7, 2024

    @OdileVidrine
    Contributor

    Oh I see, thank you

  8. jklymak commented on Aug 7, 2024

    @jklymak
    Member

    The same thing can easily be achieved with ax.fill_between except there is no guarantee it is infinite, but you can make it practically infinite, so I'm not clear on the use case that requires a special method. If anything, though, this brings up the idea that we should be able to take individual artists out of autoscale.

    import matplotlib.pyplot as plt
    import numpy as np
    
    fig, ax = plt.subplots()
    
    # a x + b y >= c
    # y = c/b - a/b x
    
    a = 1.
    b = -1.
    c = 0.
    
    x = np.array([-1e13, 1e13])
    y = c/b - a / b * x
    
    # ax.set_autoscale_on(False)
    ax.fill_between(x, y, -1e20)
    ax.set_xlim([0, 10])
    ax.set_ylim([0, 10])
    plt.show()
    
  9. story645 commented on Aug 7, 2024

    @story645
    Member

    so I'm not clear on the use case that requires a special method

    The request at PyData was specifically to show decision boundaries for classifiers, something like https://www.scikit-yb.org/en/latest/api/contrib/boundaries.html, where you want it infinitely zoomable like for interactive visualizations.

  10. jklymak commented on Aug 7, 2024

    @jklymak
    Member

    That still seems doable with a Polygon patch?

  11. story645 commented on Aug 7, 2024

    @story645
    Member

    That still seems doable with a Polygon patch?

    Yeah, I think this ultimately gets implemented as a sorta almost thin wrapper over either polygon patch or fill_between that handles the mixed coordinate systems/transforms - which basically far as I can tell what the ax{hspan, vspan, vline, hline, axline} methods are doing is automatically updating limits and handling transforms.

    Just to confirm, I think the goal here is:
    image

  12. jklymak commented on Aug 7, 2024

    @jklymak
    Member

    There is no mixed co-ordinates needed - you just make the patch really huge in the out of bounds area, and crop to the axis view limits you want. Thats what cropping is for.

  13. jklymak commented on Aug 7, 2024

    @jklymak
    Member
    import matplotlib.pyplot as plt
    import numpy as np
    
    fig, ax = plt.subplots()
    
    x = np.arange(0, 10, 0.2)
    y = np.tanh((x-3)/2) + 1.5
    
    x = np.hstack([[-1e20], x, [1e20]])
    y = np.hstack([[-1e15], y, [-1e15]])
    ax.fill_between(x, y, -1e20)
    
    ax.set_xlim([0, 10])
    ax.set_ylim([0, 3])

    halfspace

  14. story645 commented on Aug 7, 2024

    @story645
    Member

    you just make the patch really huge in the out of bounds area, and crop to the axis view limits you want. Thats what cropping is for.

    You still want it dynamically updated for pan/zoom or adding more data - any time you need to update the limits -which is what all the ax{span, line} methods and/or defining in axis coordinates gives you.

    ETA: basically, your solution inherently doesn't handle the infinite case cause you'll have to explicitly update the patch bounds every time the data gets changed if it affects the view limits and happens to fall outside the polygon

  15. jklymak commented on Aug 7, 2024

    @jklymak
    Member

    You can zoom the above as much as you want, and you can change the data in it the same way you would for any patch, you just have to add the fictitious points at plus or minus infinity.

    your solution inherently doesn't handle the infinite case cause you'll have to explicitly update the patch bounds every time the data gets changed

    I'm not sure what this means.

  16. story645 commented on Aug 8, 2024

    @story645
    Member

    you just have to add the fictitious points at plus or minus infinity.

    What I'm trying to get at is that implementing it as a standalone method would remove the need for the user to manage adding the fictitious points so that the patch grows in sync w/ the data.

    For example, here's most of the implementation for axhspan and ahvspan and you can see how they're flipped versions of each other, where axh manages the y (data coordinate) and infinite x, and axv manages the x (data coordinate) and infinite y.
    sphx_glr_axhspan_demo_001_2_00x

    hspan

    p = mpatches.Rectangle((xmin, ymin), xmax - xmin, ymax - ymin, **kwargs)
    p.set_transform(self.get_yaxis_transform(which="grid"))
    # For Rectangles and non-separable transforms, add_patch can be buggy
    # and update the x limits even though it shouldn't do so for an
    # yaxis_transformed patch, so undo that update.
    ix = self.dataLim.intervalx.copy()
    mx = self.dataLim.minposx
    self.add_patch(p)
    self.dataLim.intervalx = ix
    self.dataLim.minposx = mx
    p.get_path()._interpolation_steps = mpl.axis.GRIDLINE_INTERPOLATION_STEPS
    self._request_autoscale_view("y")
    return p

    vspan

    p = mpatches.Rectangle((xmin, ymin), xmax - xmin, ymax - ymin, **kwargs)
    p.set_transform(self.get_xaxis_transform(which="grid"))
    # For Rectangles and non-separable transforms, add_patch can be buggy
    # and update the y limits even though it shouldn't do so for an
    # xaxis_transformed patch, so undo that update.
    iy = self.dataLim.intervaly.copy()
    my = self.dataLim.minposy
    self.add_patch(p)
    self.dataLim.intervaly = iy
    self.dataLim.minposy = my
    p.get_path()._interpolation_steps = mpl.axis.GRIDLINE_INTERPOLATION_STEPS
    self._request_autoscale_view("x")

    Where an implementation for a patch like this would probably need separable {top, bottom, right, left) and manage some pair of those.

  17. timhoffm commented on Aug 21, 2024

    @timhoffm
    Member

    It should be fairly simple to create axhalfspace() inspired by axline(). The latter has most of the API and logic in place. You just have to define whether to fill left/right of the line, and on drawing, complete the line to an area using the Axes boundaries.

    I belief more complex forms would fall in a different category: functional relations that are sampled on draw to always give a smooth relation - basic: line, a bit more advanced: fill between.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions