Skip to content

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.

python
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 ​

python
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:

python
from gofish import Shape
from gofish.shape import hull  # the same function as Shape.hull

Members ​

MemberIs exactlyWhat it draws
Shape.box()its own kindThe members' bounding box, grown by padding, rounded corners.
Shape.oval()its own kindThe smallest-area ellipse around the padded members.
Shape.arcs()its own kindThe padded members, with every concave corner an arc.
Shape.blob()the general regionMembers, 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() ​

OptionTypeDefaultDescription
rxfloatCorner radius in pixels, x. Default: ry, or 2 when neither is given.
ryfloatCorner radius in pixels, y. Default: rx, or 2 when neither is given.

Shape.oval() ​

OptionTypeDefaultDescription

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.

python
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() ​

OptionTypeDefaultDescription
radiusfloat12The 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.

python
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() ​

OptionTypeDefaultDescription
convexityfloat0.5The 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.
holesboolFalseWhether 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.
roundnessfloatHow 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.
neckfloat1How 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.
avoidingAnyNodes 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.
clearancefloat4Pixels 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, a line is 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 convexity of 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() ​

OptionTypeDefaultDescription
convexityfloat1The 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.
holesboolFalseWhether 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.
roundnessfloatHow 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.
neckfloat1How 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.
avoidingAnyNodes 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.
clearancefloat4Pixels kept between the outline and each avoided node.

Shape.spine() ​

OptionTypeDefaultDescription
convexityfloat0The 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.
holesboolFalseWhether 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.
roundnessfloatHow 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.
neckfloat1How 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.
avoidingAnyNodes 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.
clearancefloat4Pixels kept between the outline and each avoided node.