Skip to content

Figures

enumeratio edited this page Oct 8, 2026 · 8 revisions

Figures

Design, agreed 2026-10-06; partly built. The first concrete step of Speculative-Graphics-and-Space §3 for the plane: layers as graphics values projected into a shared coordinate frame, shown together, with controls bound to the expression rather than to a renderer. The vocabulary is Wolfram's wherever Wolfram has the idea; the extensions are marked.

An overlay is Wolfram's Show: graphics combined in one coordinate system. What a reader works with around it is Wolfram's too: a caption is Labeled, a legend Legended. Its variables are declared where they are scoped (see Variables). There is no Figure head of its own.

Labeled(
  Show(
    LatticeTiles(QuadraticIntegers(_d),
      ColorRules -> [
        IsPrime -> ColorData("Dusk", Sqrt(Abs(Norm)), Band -> 10),
        IsIrreducible && !IsPrime -> Teal,
        IsUnit -> White,
        _h(Selected) -> Opacity(0.35, White)],
      ColorMixing -> "Screen",
      BoundaryStyle -> [IsZero -> White, Unknown -> Gray, Selected -> Directive(White, AbsoluteThickness(2.5))]),
    GridLines -> [10, 10], Axes -> True, AspectRatio -> Automatic, Selection -> _s),
  StringTemplate("Primes of $\mathbb{Q}(\sqrt{_d})$, d = {_d}, lighting the {_h} of the selection."),
  Bottom,
  Variables -> [
    _d -> Variable(Integers, -5, Where -> IsSquareFree && !IsSquare, Range -> [-400, 400]),
    _h -> [Labeled(Associates, "associates"), Labeled(Multiples, "multiples")],
    _s -> []])

Written as notatio markup (Speculative-Vdom-Markup) the same expression is elements, children as positional arguments and PascalCase attributes as options. A page needs no ClientOnly: the tags say what runs where.

Variables

  • Variables -> [_d -> …] on any element declares the variables of its subtree and makes it their scope; the nearest declaration wins, and the page is the outermost scope. DynamicModule is the explicit form. (Variables is ours as an option; Wolfram's Variables[poly] function would be PolynomialVariables here, and a Polynomial's own Variables option means the same thing.)
  • A declaration is a starting value (domain inferred), a domain (Integers, Reals, Booleans), a list of choices (Labeled(value, "word") for what a sentence shows), or Variable(domain, start, Where -> test, Range -> [a, b]). The domain picks the control a hole shows, as Wolfram's Control[{u, dom}] picks one; a hole may override it.
  • One spelling everywhere: a variable is the wildcard _d, Epsil's own pattern hole. Undeclared, it stays a hole.
  • StringTemplate is Wolfram's, with Epsil holes: {_d} is the variable's control, any other hole ({N(_d^2)}) a readout, and in a $…$ island _d or _{name} standing alone is the value (\sqrt{_d}: the variable falls into the hole, subscript and all). In markup its text is read as written.
  • An element whose wildcards must not be canonicalized (Show, a compiled plot) takes their values as bindings from its scope rather than a rewritten expression.

Handles, setters, settings

  • Locator(_b) (Wolfram's) is a handle on a variable's point, snapped to the frame's lattice when the layer is one; LocatorAutoCreate -> True adds and removes points with ⌥-click.
  • {Setter(_e, value, "label")} in prose is a link that sets a variable.
  • {InputField(Variables)} holds every declared variable as one sentence of Epsil, to copy and keep, or to paste back and load: the live configuration as source.
  • {_v | random} draws from the variable's declared domain: integers (a Where kept), choices, booleans, reals, a ring's points within their Range.
  • A layer may write the variables its expression names (a selection, a locator, a RadixExpansions' example and the settings it implies).

Options and heads

  • An argument Name -> v is an option exactly when Name is a declared option of the head it sits in; anything else is positional. IsPrime -> Teal inside ColorRules is a rule, never an option, because IsPrime is not one.
  • The markup reader decides the same way, so a child element naming a declared option is that option: <PlotLabel>…</PlotLabel> inside <Show>. Anything Epsil can say, markup can.
  • So every graphics head declares its options in its record.

Frames and views

  • Every layer is projected into a coordinate frame; layers in one frame share one live view — pan, zoom, and AspectRatio -> Automatic (true scale) — so an overlay never drifts.
  • A frame has its own coordinates, as GeoGraphics has GeoProjection: a lattice frame's are the lattice's, so GridLines and Ticks given in them come out rhombic for a hexagonal ring with no skew option. GeometricTransformation moves one layer.
  • A figure may hold several frames, independent when they need to be (GraphicsRow, Inset).
  • Coordinates need not be geometric. Polytope k-faces, lattice indices and the strands of a permutation or partition diagram (a slot and an in/out sign) are all addresses in a frame.
  • The worksheet's screen was always meant to work this way; today each pane pans and zooms on its own. That is a misreading to correct by moving the screen onto frames.

Color

  • A layer colors its elements by ColorRules, as ArrayPlot does: test -> color. A test is a pattern on the element's value, as in Wolfram (_?PrimeQ), or (extension) a property the layer publishes, or &&, ||, ! of them. Elements no rule matches fall to ColorFunction, then to the background: unknown is the identity.
  • ColorMixing (extension) says how matching rules combine. Automatic is Wolfram's: the first match wins. "Normal" paints every match in order with its opacity; "Screen", "Add" and "Multiply" combine every match, and order no longer matters.
  • A test is a function to [0, 1] (extension), crisp when boolean; the weight scales its color's opacity. Its inputs are the element's properties and values, later the view (PlotRange, ViewPoint).
  • A color is a color, Opacity(a, color), or (extension) a color function of the element's values: ColorData(scheme, value, Band -> w), with what happens past the band named as LinearGradientFilling names its padding: "Reflected", "Periodic", "Fixed".
  • Edges are a separate channel, BoundaryStyle, which takes rules too (extension): test -> directive, the directive built from EdgeForm, AbsoluteThickness (pixels; Wolfram's Thickness is a fraction of the plot's width), Dashing, JoinForm, CapForm and a color by Directive. Edges nest inward in rule order; their order always matters.
  • Blend keeps Wolfram's meaning, mixing two colors; it never names how layers combine.
  • Defaults are hand-picked per layer. Generated (Glasbey) colors are for unordered categories with no meaning of their own, such as the digits of a complex base.

Axes and grids

Wolfram's options, lifted: Axes, AxesStyle, AxesOrigin, Ticks, TicksStyle, GridLines, GridLinesStyle, Frame, PlotLabel, given in the frame's coordinates: GridLines -> [10, 10] is one spec per axis of the frame (a lattice frame's second axis is ω's), the extent is PlotRange's. 3-D has no GridLines; Wolfram's FaceGrids draws grids on the bounding box. Axis labels read in the ring's unit (ω, or i for d = −1), typeset.

Caption, legend, selection

  • Caption: Labeled(g, StringTemplate(…), Bottom), the template's holes the scope's variables: field stepper and random, what to highlight, grid, axes, scale. This is where the tangle controls grow.
  • Legend: Legended, generated from the rules (BarLegend for a color function, SwatchLegend for colors), and itself controls: a swatch opens color and opacity, a bar opens the schemes, ↺ cycles the padding, a glyph reverses.
  • Selection (extension; Graph's GraphHighlight is the nearest prior art) is a set of addresses in a frame, written to a variable: Selection -> _s (read and written). Shift/⌘-click selects several. Showing it is the author's choice, such as a collection table of s (a form cycler, a way to clear it, hover lighting a factor's tiles). Selecting an unknown element computes it.
  • Reset: the frame resets its view; the module resets its variables.

Interaction, every plot

  • Gesture handling, after the map embeds' (Google Maps' gestureHandling, Mapbox's cooperativeGestures): cooperative (the default) zooms when the plot is engaged — focused, or full-window — or with ⌘/Ctrl, and otherwise scrolls the page; greedy always zooms; none never does. 2-D and 3-D alike.
  • How the plot then moves is its camera's, a separate choice (three.js's controls are the prior art).
  • Zooming out never blocks: each frame computes within a budget, elements not yet known draw as unknown, and the budget of points in view is explicit.

Sequence

  1. Built (PR #718, as notatio-figure): gesture handling, the tile layer with rules, screen mixing, legend controls, caption holes, multi-select.
  2. Move it onto this vocabulary: Show with ColorRules/ColorMixing/BoundaryStyle, GridLines/Axes/Ticks, Labeled, options decided by declaration (and markup children as options). Figure then stays the glyph element's name until frames take that over too.
  3. Complex bases onto it; story source forms and copy; the worksheet screen onto frames; polytope and strand frames.
  4. Separately, measured: compile a page's figures at first render rather than at build, with a skeleton while they compile, cached in the shared kernel; a release build precompiles.

Open

  • ColorData as a real head whose value is a color function, in Wolfram's forms: ColorData({"Dusk", {0, 10}}) (a range is our band; Wolfram clamps past it, our "Fixed"), {"Dusk", "Reverse"}, an indexed ColorData(97)-like scheme for Glasbey; padding beyond Wolfram's clamp is ours. Scheme names PascalCase.
  • Tests as Epsil patterns over an element's value, beyond published properties.
  • Tables: more than ℤ/n's multiplication (addition, ℤ/pᵏ lifts, Cayley tables of groups).
  • The glyph element (Figure) onto Show frames: strands (slot and in/out sign), polytope faces. Accepted below; slices in progress.
  • What to call notatio markup.
  • Where Show, the layers and the rules live once Speculative-Graphics-and-Space §5's graphics package exists.

The glyph element onto Show frames

Accepted (the maintainer, 2026-10-06), with every lean in §6. Replaces the Open item "The glyph element (Figure) onto Show frames". Nothing below is built. Show is (#718); Figure is still the glyph element's name.

The glyph element draws one combinatorial object as one fixed SVG: no selection, no shared view, no rules. Show draws layers of addressable elements in one frame, styled by rules, selectable. The move: a glyph's parts become addresses in a frame, so a permutation's strands or a polytope's faces are colored, picked and related the way a lattice's tiles are. Two frames: strands, and k-faces.

1. Inventory

Element. <notatio-figure kind value n svg> (packages/components/src/notatio-figure.ts, no shadow root) calls renderGlyph(kind, value, {n}) (packages/frontend/src/glyphs.ts), a pure (integer list) -> SVG string, themed by --notatio-accent|border|fg. No other attributes or options: no selection, hover, view, caption or legend. svg is the escape hatch. A separate element, <notatio-polytope> (notatio-polytope.ts, frontend/src/polytope3d.ts), does draw faces, with its own selected, labels, azimuth/elevation/zoom (see §3).

Kinds (GlyphKind, 11), by what the value encodes:

kind value draws
permutation one-line image matrix of dots
partition, tableau parts Ferrers diagram; cells numbered in reading order
composition parts divided bar
subset (+ n) members row of n cells, members filled
dyck up/down steps mountain range
lattice E/N steps staircase path on its grid
tree, binary-tree preorder child-count / shape word tidy-layout node-link tree
set-partition restricted-growth string blocks as pills
diagram RGS of 2n points (top row, then bottom) two rows of dots, blocks joined

Strand-like: permutation (as a matrix, not strands), diagram, set-partition. Cell-like (a Cartesian frame of ColorRules already): partition, tableau, composition, subset. Path/tree: dyck, lattice, tree (a Graph, not a table). Not a glyph kind at all: polytopes, braids and knots, whose pictures live elsewhere (notatio-polytope; braid docs have none).

Use (counts approximate). About 40 element tags, in markdown/Vue only (no engine head emits one yet; Figure(...) is only in doc comments): packages/symbols/combinatorics/combinatorics/docs/figure.md (~16, every kind), packages/symbols/algebras/diagram/docs/diagram-algebras.md (~6, diagram), .../combinatorics/docs/ranking-and-unranking.md (~4), packages/components/docs/{manipulate,overview}.md, web/.vitepress/theme/components/GlyphGallery.vue (the five-family gallery, v-for), web/playground/inspirations/{tangle,sage,wolfram}.md (~6, driven by _k/_n wildcards). Other callers of renderGlyph: notatio-collection-table.ts (a glyph column), cli/src/node-graphics.ts and notatio-terminal.ts (SVG for non-web surfaces). Reference pages: none; GlyphKind's record is a stub.

2. The strand frame

Coordinates. An address is (slot, level), both naturals: slot ≥ 1 is the position along a row, level ≥ 0 the row, 0 the in side and the last level the out side. A bare diagram has two levels; a stack has more (composition, below). The in/out sign is derived, not a third coordinate: sign = -1 at level 0, +1 at the top, 0 between. It matches the diagram docs' block {1, 2, -1, -2} (negative = bottom row). A strand is not an address; it is a block, a set of addresses.

A cell is a point: a strand's end on a row (a Disk at its position). Crossings, cups and caps are shapes the strands take between cells, drawn from the blocks, not cells of their own. This is GraphicsComplex in Wolfram's sense: a point list plus primitives (Line, BezierCurve) indexed into it.

Properties (has): in, out, through (the block touches both sides), cap (joins two in-side cells), cup (two out-side), propagating (= through), singleton, loop (a block only in the glued middle, a factor of δ), crossing (its strand crosses another), over/under (a braid's sign of the crossing, ±).

Values (value): slot, level, sign, block (id, in first-appearance order, as the RGS's), size (block size), image (the other end of a through strand: a permutation's value), crossings (how many strands this one crosses), exponent (a braid generator's ±1 at a crossing).

Relations (relatedTo(rel, selected, i, j)): SameStrand/SameBlock (the same block), Crosses (its strand crosses the selected one's), Adjacent (neighboring slot, same level), Through-partner (Image: the far end of the selected strand), Above/Below (the levels a composed diagram stacks). Selecting a cell lights its block under ColorRules -> _h(Selected), as Associates/Multiples do for a ring.

describe returns Show's {title, rows}: title the block as sets ("{1, 2, -1}"), rows size, kind (through / cap / cup / loop), image, crossings, and for a braid its generator word.

Maps onto the frame.

  • Permutation [3,1,2]: two levels, n blocks, each {(i, 0), (π(i), 1)}; all through, no cap or cup. crossing falls out of inversions. The matrix picture stays a second layer of the same frame (MatrixPlot-style tiles at (i, π(i))): rows are slot at level 0, columns at level 1.
  • Set partition of [n]: one level; blocks are hyperedges over its cells, drawn as pills (today's setPartitionSvg). A set partition is the strand frame with levels = 1.
  • Brauer / Temperley–Lieb / partition / Motzkin: two levels of n; Brauer blocks are 2-blocks (through, cap, cup), TL adds ¬crossing, the partition algebra allows any block size, Motzkin allows singletons. Today's diagram kind, unchanged in meaning (RGS over 2n).
  • Braids: a word of generators over n strands is n slots and one level per generator, a crossing between adjacent slots at that level, over/under giving the sign. The same frame draws a knot's braid closure with a Closure option later; braids join once the engine head Braid exposes its word (it does).

Composition. One frame with levels, not two frames. Compose(a, b) is a diagram of levels(a) + levels(b) - 1 rows: a's out row is glued to b's in row at a shared level. This is the picture the product is defined by (the diagram docs: "stack, glue, read off, throw the middle away"), and loop blocks live exactly there. Reducing (Simplify/evaluating the product) is a transition from levels = 3 to levels = 2 plus a δ power: a natural Manipulate over a level count. Independent frames (GraphicsRow) stay for side-by-side, e.g. a multiplication table.

3. The polytope k-face frame

Coordinates. An address is (k, index): the face's dimension and its position in that dimension's list, in the collection's own order. Vertices are k = 0. This reuses the (i, j) pair the layer contract already carries, with i as k. Polytope (packages/symbols/combinatorics/polytope/src/face.ts) already gives the data: enumerate(n) (faces in the carrier's order), dimension, point (barycenter in ambient coordinates, exact), hasVertex, and the derived contains.

Shared projective space. The frame is an ambient ℝ^d (d of the largest polytope on it); each layer places its faces by point. Several polytopes share it as in Wolfram's Graphics3D overlay: Show(PolytopeFaces(Permutahedron(4)), PolytopeFaces(Associahedron(4))), one ViewPoint. A frame with d > 3 first projects to 3-D (a fixed linear map, the layer-independent ViewVertical/ViewMatrix), then the camera projects to the screen. The camera is Wolfram's vocabulary: ViewPoint (az/el/distance), ViewVertical, ViewAngle, ViewCenter, SphericalRegion -> True (today's "fit to the circumscribed sphere"). notatio-polytope's azimuth/elevation/zoom/Orbit become these.

Properties (has): vertex, edge, ridge, facet, interior, top (the polytope itself); incident is a relation, not a property. Values: dimension (= k), vertexCount, valence (faces it is on), depth (distance from the viewer; lets ColorRules shade, replacing shade), index, plus the carrier's own: a permutahedron face's ordered set partition, Dimension.

Relations: Incident (share a vertex-containing relation either way), FaceOf (the selected face contains this one; contains, so reflexive), HasFace/Cofaces (this contains the selected), Adjacent (share a facet of one dimension lower: a face-poset cover), SameDimension. Selecting a facet with ColorRules -> [_h(Selected) -> …], _h being FaceOf or Cofaces, lights the star or closure: the "faces incident to it" picture. MeshCellIndex is the address, MeshCells(poly, k) the collection of k-faces; a face's describe is PolyhedronData-like ({title: "2-face {{1,2},{3}}", rows: dimension, vertices, incident}).

Marks. A 0-face is a Point, a 1-face a Line, a 2-face a Polygon, a 3-face a Polyhedron shown as its boundary; GraphicsComplex indexes all of them to one vertex list. Hit-testing returns the face's own address, which is what notatio-polytope already does by writing data-face on every mark.

Bounded tables vs. points anywhere. A polytope at order n is finite: a bounded layer (bounds = {i: [0, d], j: [0, faces(k)]}), never "classified outward from the center", so Show's budget machinery is idle. A point cloud (the vertices of many orders, or a sampled MeshRegion) is a points() layer of (0, n) addresses; k > 0 marks come from incident over those, as GraphicsComplex does.

Later: orienting projections and slices (the maintainer, 2026-10-08). How an n-D frame reaches 3-D should be controllable, not fixed: the n-to-3 map is one replaceable step (a Show option read by camera frames; default the fixed projection), so a user can rotate a higher-dimensional polytope in hyperspace before it is projected. Alongside projections, sections: a hyperplane slice through n-D space, and plane slices through 3-D (Wolfram's slice surfaces in SliceContourPlot3D and RegionSlice are the vocabulary to check). Not in slice 4; slice 4 only keeps the projection replaceable.

4. Fit with Show's layer contract

Today's TileLayer assumes: an address is a pair (i, j) of integers; a mark is a tile of the basis's cell shape at latticePoint(i, j); a selection is a list of such pairs. Both new frames fit the address ((slot, level), (k, index)): has, value, relatedTo, describe, and Selection -> _s as a set of addresses are unchanged, and so are ColorRules, ColorMixing and Locator(_v) (snapped to the nearest address). What does not fit is geometry: a strand is a curve between cells, a face a polygon in 3-D, and the lattice view is 2-D with a Voronoi cell.

The smallest extension, optional members on a layer, all absent for a lattice:

  1. place(i, j) -> number[] (the address's point in the frame's space, any d) and mark(i, j) -> Primitive (Disk, Line, Polygon, Text, in Wolfram's heads). Absent: the lattice's own latticePoint and cell, as today. points() is the same idea (a list of places) and folds into place over a finite set.
  2. links?(): readonly (readonly Address[])[], the blocks/edges as address tuples (Wolfram's Graph edges, GraphicsComplex lines), drawn after the marks and styled by the same BoundaryStyle by property of their members; a hit on a link selects its members.
  3. A view that is not LatticeView: the frame names its view kind (plane today; fixed for a strand diagram, no pan or zoom beyond fit; camera for 3-D with ViewPoint). ViewPoint et al. are options of Show, read only by a camera frame; gesture handling is unchanged.

has/value/relatedTo need nothing more: relatedTo already takes the selected address and the candidate's, and the relations above (SameBlock, FaceOf) are just names. One gap: a relation to a set of selected addresses is evaluated as an OR over selection (drawTiles: selection.some(...)); Crosses and Cofaces of several selections want that too, so it stays. The rest (maxIndex, known, prepare) are for lazy lattices; a bounded layer reports known = true.

This is deliberately not strand- or polytope-specific. A layer that places marks at addresses, relates them, and lets a selection name them serves tables, trees, graphs, org charts and timelines as much as diagrams: the same contract draws a tree kind (nodes at (depth, order), links the parent edges) and a dyck path (cells at (step, height), links the steps). General layout is the target (Speculative-Native-Rendering opening); the frame is where a layout engine plugs in, supplying place and links while rules, selection and legend stay generic.

5. Migration

<notatio-figure kind value> stays as a thin shorthand and lowers to Show (the lowering is structure.ts's, Speculative-Vdom-Markup): Figure("permutation", [3,1,2]) is Show(StrandDiagram(Permutation([3,1,2]))), the other kinds to their layers (Diagram-algebra kind="diagram" to StrandDiagram(...), partition/tableau/composition/subset to cell layers over a Cartesian frame). Each kind's current SVG is the default styling (its rules) so pages look the same; the svg escape hatch stays for hand-drawn work. The functions in glyphs.ts are kept as the non-web renderers (cli, notatio-terminal, SSR) until the boxes route covers them.

New head options users get for free: ColorRules, Selection -> _s, Locator, Labeled, Legended, GridLines -> None. Heads: StrandDiagram(expr) (layer), PolytopeFaces(poly) (layer); Figure keeps its name.

Slices, each its own PR (fewer, bigger where related, per the CI note):

  1. Contract. Optional place/mark/links and the fixed view in tiles-canvas.ts and notatio-show.ts; a toy layer in tests; lattice layers unchanged.
  2. Strand frame with permutation, set partition and diagram kinds; Figure kind="permutation"|"set-partition"|"diagram" lowered; the diagram-algebras guide gets Selection and Crosses. Then Compose (levels, loops).
  3. Cell kinds onto a Cartesian frame (partition, tableau, composition, subset); tree/path kinds (dyck, lattice, tree) as links layers. This is the generalization test.
  4. Polytope k-face frame (camera view, PolytopeFaces), <notatio-polytope> lowered to it, its selected as Selection.
  5. Braids onto the strand frame; then the gallery and the playground pages move to Show.

Strands first: no new view kind, the diagram docs are the nearest users, and slice 3 is the cheap check that the contract is not strand-shaped. Polytopes need the camera.

6. Decisions (the leans, accepted)

  1. Address as (i, j) or number[]? Strands and faces both fit a pair, and Show, Locator and hit-testing all assume one. Lean: keep the pair; widen to a tuple only when a layer needs three coordinates.
  2. Levels, or two frames, for composition? Lean: levels in one frame; independent frames only for side-by-side.
  3. Is a strand an address or a relation? Selecting "this strand" means selecting its end cells. Lean: cells are addresses, strands are blocks (links); a hit on a link selects its ends.
  4. Heads and names. StrandDiagram and PolytopeFaces are my invention; Wolfram has no diagram head (KnotData and BraidDiagram are not it). Lean: those names, as layers.
  5. Cell kinds as strand kinds? partition/tableau/subset are plain cell grids. Lean: a Cartesian frame, not the strand frame; share only the contract.
  6. Camera options on Show or per frame? Lean: on Show, ViewPoint and friends as in Graphics3D, ignored by a 2-D frame.
  7. notatio-polytope keeps its own element? Lean: no; one Show, with the element a shorthand like Figure.
  8. Braids and knots now or later? The Braid word already exists but crossings need over/under drawing. Lean: slice 5, after the strand frame is in use.
  9. Rename Figure once frames land? #717 was closed unmerged. Lean: leave it; a thin shorthand needs no rename pressure.

Home

Project

Design: every design page, settled, speculative and archived

Clone this wiki locally