Patch fully covered with Matplotlib

Question:

I’m trying to make a program that covers the area of ​​a function based on a figure. My main problem is that the pattern leaves a lot of empty spaces. One way that I could think of to fix this was by trying to indicate in the code that each figure must touch at least 2 vertices of another figure. However, I have not been able to implement it successfully.

I noticed the "numVertices" parameter in the documentation. However, I didn’t get anything so far. Could someone help me with this?

Code:

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle, RegularPolygon
from scipy.integrate import quad

def plot_function_with_pattern(func, x_range, y_range, base_size, shape="rectangle", include_touching=True, start_x=None, start_y=None):
    fig, ax = plt.subplots()
    x = np.linspace(x_range[0], x_range[1], 1000)
    y = func(x)

    ax.plot(x, y)

    # Calculate number of figures that fit inside function
    num_figures = 0
    if start_x is None:
        start_x = x_range[0]
    if start_y is None:
        start_y = y_range[0]


    for x_i in np.arange(start_x, x_range[1], base_size):
        for y_i in np.arange(start_y, y_range[1], base_size):
            if func(x_i) >= y_i:
                # Check if the figure is touching the function
                touching = func(x_i + base_size/2) >= y_i or func(x_i + base_size/2) >= y_i + base_size or func(x_i) >= y_i + base_size/2 or func(x_i + base_size) >= y_i + base_size/2
                if include_touching or not touching:
                    # Create shape patch
                    if shape == "rectangle":
                        facecolor = 'red' if not include_touching else 'green'
                        patch = Rectangle((x_i, y_i), base_size, radius=base_size, facecolor=facecolor)
                    elif shape == "triangle":
                        facecolor = 'red' if not include_touching else 'green'
                        patch = RegularPolygon((x_i+base_size/2, y_i), 3, radius=base_size/np.sqrt(3), orientation=0, facecolor=facecolor)
                    elif shape == "square":
                        facecolor = 'red' if not include_touching else 'green'
                        patch = RegularPolygon((x_i+base_size/2, y_i+base_size/2), 4, radius=base_size/np.sqrt(2), orientation=np.pi/4, facecolor=facecolor)
                    elif shape == "hexagon":
                        facecolor = 'red' if not include_touching else 'green'
                        patch = RegularPolygon((x_i+base_size/2, y_i), 6, radius=base_size/np.sqrt(3), orientation=np.pi/2, facecolor=facecolor)
                    ax.add_patch(patch)
                    num_figures += 1

    plt.show()

    # Calculate integral of the function
    integral, _ = quad(func, x_range[0], x_range[1])

    # Subtract the amount of figures touching the function if include_touching is False
    if not include_touching:
        num_figures -= int(integral / base_size**2)
        total = num_figures*-1
        print(f"{total} {shape}(s) are inside the function and NOT TOUCHING the function. The base size was {base_size} unit(s) with x range of: {x_range} and y range of: {y_range}. The area of the function is {integral:.4f}.")
    elif include_touching:
        print(f"{num_figures} {shape}(s) are inside the function and TOUCHING the function. The base size was {base_size} unit(s) with x range of: {x_range} and y range of: {y_range}. The area of the function is {integral:.4f}.")

# Example usage
def func(x):
    return -0.00002*x**6 + 0.0011*x**5 -0.024 *x**4 + 0.24*x**3 -0.8*x**2 -x + 10

plot_function_with_pattern(func, x_range=(0, 19.308), y_range=(0, 17.55), base_size=1, shape="hexagon", include_touching=True)

resulting plot

Asked By: NooberPlays

||

Answers:

It took me a while to understand what you are trying to achieve but I think I got it.

First you should specify a color for patch edges so that the shapes are visible (the squares are not visible in your example). Then since your ‘rectangle’ case is incomplete (missing height) I would simply handle every shape as a RegularPolygon. Now if you want to fill the blanks between shapes, you have to move/rotate your polygons every two columns (only squares can be put close to each other without blanks). You can specify these transformations in a dictionary (I simplified some arguments for better readability/generalization):

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle, RegularPolygon
from scipy.integrate import quad

def plot_function_with_pattern(func, x_range, y_range, base_size=1,  num_vertices=4, include_touching=True, face_col='green', edge_col='grey'):
    shape_dict = {
        3: {    # triangle
            'radius': base_size/np.sqrt(3),
            'orientation': 0,
            'rotation': np.pi,  # rotation every two columns
            'y_offset': base_size/(2*np.sqrt(3)),  # y_offset every two columns
            'base_step_x': base_size/2,
            'base_step_y': np.sqrt(3)*base_size/2
        },
        4: {    # square
            'radius': base_size/np.sqrt(2),
            'orientation': np.pi/4
        },
        6: {    # hexagon
            'radius': base_size/2,
            'orientation': np.pi/2,
            'y_offset': np.sqrt(3)*base_size/4,  # y_offset every two columns
            'base_step_x': 3*base_size/4,
            'base_step_y': np.sqrt(3)*base_size/2
        }
    }
    if not num_vertices in shape_dict:
        print(f"Error: polygon with {num_vertices} vertices are not supported.")
        return
    
    fig, ax = plt.subplots()
    x = np.linspace(x_range[0], x_range[1], 1000)
    y = func(x)

    ax.plot(x, y)

    num_figures = 0
    base_step_x = shape_dict[num_vertices].get('base_step_x', base_size)
    base_step_y = shape_dict[num_vertices].get('base_step_y', base_size)
    radius = shape_dict[num_vertices]['radius']
    orientation = shape_dict[num_vertices]['orientation']
    y_start = shape_dict[num_vertices].get('y_offset', base_size/2) # align height of the first shape base with 0

    for i, x_i in enumerate(np.arange(x_range[0], x_range[1], base_step_x)):
        y_offset = y_start
        orient = orientation
        if (i % 2 != 0):
            y_offset += shape_dict[num_vertices].get('y_offset', 0)
            orient += shape_dict[num_vertices].get('rotation', 0)
        for y_i in np.arange(y_range[0] + y_offset, y_range[1] + y_offset, base_step_y):
            patch = RegularPolygon((x_i, y_i), num_vertices, radius=radius, orientation=orient, fc=face_col, ec=edge_col)
            if patch.contains_point((x_i, func(x_i)), radius=1e-9):  # omitting radius triggers a mpl bug
                if include_touching:
                    ax.add_patch(patch)
                    num_figures += 1
                break  
            else:
                ax.add_patch(patch)
                num_figures += 1

    # Calculate integral of the function
    integral, _ = quad(func, x_range[0], x_range[1])

    # Subtract the amount of figures touching the function if include_touching is False
    print(f"{num_figures} polygons with {num_vertices} vertices are inside the function or touching the function. The base size was {base_size} unit(s) with x range of: {x_range} and y range of: {y_range}. The area of the function is {integral:.4f}.")
    
    plt.show()

# Example usage
def func(x):
    return -0.00002*x**6 + 0.0011*x**5 -0.024 *x**4 + 0.24*x**3 -0.8*x**2 -x + 10

plot_function_with_pattern(func, x_range=(0, 19.308), y_range=(0, 17.55), base_size=0.8, include_touching=True, num_vertices=6)

Edit: offsets, steps and rotations are calculated using trigonometry see this page for instance

Triangle:

enter image description here

Square:

enter image description here

Hexagon:

enter image description here

Answered By: Tranbi
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.