Shape
The Shape family holds the outlines enclose draws around its members. A shape is the value of the shape option of enclose. The padding and the paint stay on enclose.
from gofish import Shape, circle, enclose, layer, spread
(
layer([
spread(
[
circle(r=5, fill="#111").name("a"),
circle(r=5, fill="#111").name("b"),
circle(r=5, fill="#111").name("c"),
],
dir="x",
spacing=95,
alignment="middle",
)
])
.relate(lambda a, b, c: [
enclose(
[a, c],
shape=Shape.spine(avoiding=[b], clearance=6),
padding=22,
fill="#ff2400",
fill_opacity=0.3,
)
])
.render(w=380, h=150)
)Signature
Shape.box(rx=None, ry=None)
Shape.oval()
Shape.arcs(radius=None)
Shape.blob(convexity=None, holes=None, roundness=None, neck=None, avoiding=None, clearance=None)
Shape.hull(convexity=None, holes=None, roundness=None, neck=None, avoiding=None, clearance=None)
Shape.spine(convexity=None, holes=None, roundness=None, neck=None, avoiding=None, clearance=None)Every member is a function call. Each returns a plain dict, {"kind": ..., **params}, for example {"kind": "box", "rx": 16, "ry": 16}. Shape.hull() and Shape.spine() are Shape.blob() with some params preset, so their dicts have kind "blob": Shape.hull(**opts) is Shape.blob(convexity=1, neck=1, **opts), and Shape.spine(**opts) is Shape.blob(convexity=0, **opts). Any option overrides the preset. avoiding holds marks (ref(...), select_all(...), a .relate() operand); the chart carries their references.
The family is also a module of its own:
from gofish import Shape
from gofish.shape import hull # the same function as Shape.hullMembers
| Member | Is exactly | What it draws |
|---|---|---|
Shape.box() | its own kind | The members' bounding box, grown by padding, rounded corners. |
Shape.oval() | its own kind | The smallest-area ellipse around the padded members. |
Shape.arcs() | its own kind | The padded members, with every concave corner an arc. |
Shape.blob() | the general region | Members, links between them and filled gaps, rounded. |
Shape.hull() | Shape.blob(convexity=1, neck=1, **opts) | The padded convex hull, rounded. |
Shape.spine() | Shape.blob(convexity=0, **opts) | A tree of tubes that links the members, and nothing else. |
Shape.box() is the default. A blob is a union of discs, tubes and triangles, so no blob setting draws a box.
Shape.box()
| Option | Type | Default | Description |
|---|---|---|---|
rx | float | Corner radius in pixels, x. Default: ry, or 2 when neither is given. | |
ry | float | Corner radius in pixels, y. Default: rx, or 2 when neither is given. |
Shape.oval()
| Option | Type | Default | Description |
|---|
The ellipse holds every member grown by padding (a circle is a disc, a line is a tube along its segments, and any other node is its bounding box), and of all such ellipses it has the least area. It is not fit to the bounding box, so it may be rotated: it follows the members, not the axes. A single member gives a circle.
from gofish import Shape, circle, enclose, layer, position
enclose(
[
layer([
position([circle(r=6, fill="#4c78a8")], x=10, y=10),
position([circle(r=9, fill="#4c78a8")], x=60, y=30),
position([circle(r=6, fill="#4c78a8")], x=110, y=70),
position([circle(r=12, fill="#4c78a8")], x=170, y=90),
])
],
shape=Shape.oval(),
padding=10,
fill="#4c78a8",
fill_opacity=0.15,
stroke="#1f3b63",
).render(w=260, h=160)Shape.arcs()
| Option | Type | Default | Description |
|---|---|---|---|
radius | float | 12 | The radius of the closing disc, in pixels: the radius of every concave arc. 0 is the padded union; a large radius nears the padded convex hull. |
The outline is the members grown by padding, closed by a disc of radius: every gap the disc cannot pass through is filled, and every concave corner is a circular arc of radius. At radius 0 it is the padded members' union, and a large radius nears the padded convex hull. Holes are always filled. Around circles, the outline is made of circular arcs only, which is the contour of a bubble treemap (Görtler et al. 2018). Two discs of radius a whose padded edges are g apart join only when 2 · a · radius ≥ a · g + g² / 4.
from gofish import Shape, circle, enclose, pack
enclose(
[pack([circle(r=r, fill="#f2c14e") for r in (30, 18, 22, 10, 14, 8)], spacing=6)],
shape=Shape.arcs(radius=12),
padding=6,
fill="#f2c14e",
fill_opacity=0.25,
stroke="#333",
).render(w=220, h=220)Shape.blob()
| Option | Type | Default | Description |
|---|---|---|---|
convexity | float | 0.5 | The share of the members' convex hull that is filled, smallest gaps first, from 0 (the spanning tree alone) to 1 (the whole hull). It is the only option that fills gaps between members, so it decides the pieces and the filled area. |
holes | bool | False | Whether a gap left open inside the silhouette stays a hole. Off, every hole is filled and each avoided node it held is cut back out as its clearance circle, as allow_holes in PostGIS and Shapely. |
roundness | float | How far the outline's corners are rounded, in pixels. It changes how sharply the outline bends and nothing else: no gap fills, no hole opens, no two pieces join. Default: padding. | |
neck | float | 1 | How far the outline is NOT pulled in between members, above 0 and at most 1: a lone link becomes a tube neck times the bulge, and the edge of a filled region sinks in between its members. 1 does not pull in. 0 is not allowed: a link neck times the bulge would have no width, and its two sides would meet. It never changes the pieces or holes. |
avoiding | Any | Nodes to keep out: a list of nodes or refs, or a selectAll(name) selection. A listed node that is also a member is ignored. Each avoided node inside the region becomes a round hole, and links route around the others. | |
clearance | float | 4 | Pixels kept between the outline and each avoided node. |
A blob is built from these pieces, in the enclosure's pixels:
- each member grown by
padding(a circle is a disc, alineis a tube along its segments, and any other node is its bounding box); - the links of the shortest tree that joins the members, as tubes that taper from one member's bulge to the other's, routed around the avoided nodes;
- the Delaunay triangles of the members' points, filled from the smallest gap up, until they hold
convexityof the convex hull's area.
Each avoided node, grown by clearance, is cut out of the union. The outline of the rest is traced exactly, pulled in by neck between members, and rounded by roundness. convexity alone decides which gaps fill, so it decides the pieces and the holes; roundness and neck never change them. The Enclose essay describes the construction.
Shape.hull()
| Option | Type | Default | Description |
|---|---|---|---|
convexity | float | 1 | The share of the members' convex hull that is filled, smallest gaps first, from 0 (the spanning tree alone) to 1 (the whole hull). It is the only option that fills gaps between members, so it decides the pieces and the filled area. |
holes | bool | False | Whether a gap left open inside the silhouette stays a hole. Off, every hole is filled and each avoided node it held is cut back out as its clearance circle, as allow_holes in PostGIS and Shapely. |
roundness | float | How far the outline's corners are rounded, in pixels. It changes how sharply the outline bends and nothing else: no gap fills, no hole opens, no two pieces join. Default: padding. | |
neck | float | 1 | How far the outline is NOT pulled in between members, above 0 and at most 1: a lone link becomes a tube neck times the bulge, and the edge of a filled region sinks in between its members. 1 does not pull in. 0 is not allowed: a link neck times the bulge would have no width, and its two sides would meet. It never changes the pieces or holes. |
avoiding | Any | Nodes to keep out: a list of nodes or refs, or a selectAll(name) selection. A listed node that is also a member is ignored. Each avoided node inside the region becomes a round hole, and links route around the others. | |
clearance | float | 4 | Pixels kept between the outline and each avoided node. |
Shape.spine()
| Option | Type | Default | Description |
|---|---|---|---|
convexity | float | 0 | The share of the members' convex hull that is filled, smallest gaps first, from 0 (the spanning tree alone) to 1 (the whole hull). It is the only option that fills gaps between members, so it decides the pieces and the filled area. |
holes | bool | False | Whether a gap left open inside the silhouette stays a hole. Off, every hole is filled and each avoided node it held is cut back out as its clearance circle, as allow_holes in PostGIS and Shapely. |
roundness | float | How far the outline's corners are rounded, in pixels. It changes how sharply the outline bends and nothing else: no gap fills, no hole opens, no two pieces join. Default: padding. | |
neck | float | 1 | How far the outline is NOT pulled in between members, above 0 and at most 1: a lone link becomes a tube neck times the bulge, and the edge of a filled region sinks in between its members. 1 does not pull in. 0 is not allowed: a link neck times the bulge would have no width, and its two sides would meet. It never changes the pieces or holes. |
avoiding | Any | Nodes to keep out: a list of nodes or refs, or a selectAll(name) selection. A listed node that is also a member is ignored. Each avoided node inside the region becomes a round hole, and links route around the others. | |
clearance | float | 4 | Pixels kept between the outline and each avoided node. |
