Mast
Mast

Reputation: 1894

Figure in a figure in a figure

I'm experimenting with matplotlib to draw figures in figures in figures. Since squares are the most straight-forward to draw, I started with those. In the end I want to write a generator for polygons with a certain width. In the given example, this would be a 4-corner polygon with straight angles and width 1.

My current code plots the following, which is as expected and almost as desired.

square in square in square

Note there is a line between 2,2 and 2,3 which I think can be removed if this is done with a correct algorithm instead of the current code.

A summary of the above is a square boxed in two boxes with an amplitude increasing with 1, assuming the the larger boxes are 'behind' the rest of the boxes.

The method of which I wrote the code producing the above is, well, not really a function. It's a darn ugly collection of points which happen to resemble hollow squares.

import matplotlib.path as mpath
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt

fig, ax = plt.subplots()

INNER_AMPLITUDE = 1.0
OUTER_AMPLITUDE = 3.0

Path_in = mpath.Path
path_in_data = [
    (Path_in.MOVETO, (INNER_AMPLITUDE, -INNER_AMPLITUDE)),
    (Path_in.LINETO, (-INNER_AMPLITUDE, -INNER_AMPLITUDE)),
    (Path_in.LINETO, (-INNER_AMPLITUDE, INNER_AMPLITUDE)),
    (Path_in.LINETO, (INNER_AMPLITUDE, INNER_AMPLITUDE)),
    (Path_in.CLOSEPOLY, (INNER_AMPLITUDE, -INNER_AMPLITUDE)),
    ]
codes, verts = zip(*path_in_data)
path_in = mpath.Path(verts, codes)
patch_in = mpatches.PathPatch(path_in, facecolor='g', alpha=0.3)
ax.add_patch(patch_in)

x, y = zip(*path_in.vertices)
line, = ax.plot(x, y, 'go-')

Path_out = mpath.Path
path_out_data = [
    (Path_out.MOVETO, (OUTER_AMPLITUDE, -OUTER_AMPLITUDE)),
    (Path_out.LINETO, (-OUTER_AMPLITUDE, -OUTER_AMPLITUDE)),
    (Path_out.LINETO, (-OUTER_AMPLITUDE, OUTER_AMPLITUDE)),
    (Path_out.LINETO, (OUTER_AMPLITUDE, OUTER_AMPLITUDE)),
    (Path_out.LINETO, (OUTER_AMPLITUDE, OUTER_AMPLITUDE-INNER_AMPLITUDE)),
    (Path_out.LINETO, (-(OUTER_AMPLITUDE-INNER_AMPLITUDE), OUTER_AMPLITUDE-INNER_AMPLITUDE)),
    (Path_out.LINETO, (-(OUTER_AMPLITUDE-INNER_AMPLITUDE), -(OUTER_AMPLITUDE-INNER_AMPLITUDE))),
    (Path_out.LINETO, (OUTER_AMPLITUDE-INNER_AMPLITUDE, -(OUTER_AMPLITUDE-INNER_AMPLITUDE))),
    (Path_out.LINETO, (OUTER_AMPLITUDE-INNER_AMPLITUDE, OUTER_AMPLITUDE-INNER_AMPLITUDE)),
    (Path_out.LINETO, (OUTER_AMPLITUDE, OUTER_AMPLITUDE-INNER_AMPLITUDE)),
    (Path_out.CLOSEPOLY, (OUTER_AMPLITUDE, OUTER_AMPLITUDE-INNER_AMPLITUDE)),
    ]
codes, verts = zip(*path_out_data)
path_out = mpath.Path(verts, codes)
patch_out = mpatches.PathPatch(path_out, facecolor='r', alpha=0.3)
ax.add_patch(patch_out)
plt.title('Square in a square in a square')

ax.grid()
ax.axis('equal')
plt.show()

Do note I consider this off-topic for Code Review since I'm looking for extending my functionality, not just a re-write which is up to best-practices. I feel like I'm doing it completely the wrong way. First things first.

How should I draw polygons with a certain width using matplotlib, assuming the polygon will be surrounded on the outside with a band of the same form and at least the same width and completely filled on the inside?

Upvotes: 3

Views: 151

Answers (1)

hitzg
hitzg

Reputation: 12711

Handling polygons purely in matplotlib can be quite tedious. Luckily there is a very nice library for these kind of operations: shapely. For your purposes the parallel_offset function is the way to go. The bounds of the polygons which you're interested in, are defined by ring1, ring2 and ring3:

import numpy as np
import matplotlib.pyplot as plt
import shapely.geometry as sg
from descartes.patch import PolygonPatch

# if I understood correctly you mainly need the difference d here
INNER_AMPLITUDE = 0.1
OUTER_AMPLITUDE = 0.2
d = OUTER_AMPLITUDE - INNER_AMPLITUDE

# fix seed, for reproducability
np.random.seed(11111)

# a function to produce a "random" polygon
def random_polygon():
    nr_p = np.random.randint(7,15)
    angle = np.sort(np.random.rand(nr_p)*2*np.pi)
    dist = 0.3*np.random.rand(nr_p) + 0.5
    return np.vstack((np.cos(angle)*dist, np.sin(angle)*dist)).T

# your input polygon
p = random_polygon()

# create a shapely ring object
ring1 = sg.LinearRing(p)
ring2 = ring1.parallel_offset(d, 'right', join_style=2, mitre_limit=10.)
ring3 = ring1.parallel_offset(2*d, 'right', join_style=2, mitre_limit=10.)

# revert the third ring. This is necessary to use it to procude a hole
ring3.coords = list(ring3.coords)[::-1]

# inner and outer polygon
inner_poly = sg.Polygon(ring1)
outer_poly = sg.Polygon(ring2, [ring3])

# create the figure
fig, ax = plt.subplots(1)

# convert them to matplotlib patches and add them to the axes
ax.add_patch(PolygonPatch(inner_poly, facecolor=(0,1,0,0.4),
    edgecolor=(0,1,0,1), linewidth=3))
ax.add_patch(PolygonPatch(outer_poly, facecolor=(1,0,0,0.4),
    edgecolor=(1,0,0,1), linewidth=3))

# cosmetics
ax.set_aspect(1)
plt.axis([-1.5, 1.5, -1.5, 1.5])
plt.grid()
plt.show()

Result:

enter image description here

Upvotes: 1

Related Questions