A recreation of the small-multiples beeswarm genre — in the spirit of Nadieh Bremer'sSatellites in Space — built on NetworkCustomChart plus the newpackedClusterMatrix recipe. Each of ~3,000 procedurally-generated satellites is a single composite glyph encoding four data channels at once: an icon (class), color (category), size (mass), and lightness shade (launch date). Columns are controlling regions; rows are orbital types. Hover any glyph for its details, hover the legend to isolate a category or class, and open it full screen.
Preview
Satellites in Space
Each glyph is one active satellite from a procedurally-generated catalog of 2,961. Columns are controlling regions, rows are orbital types. A glyph’s color shows category,size mass, shade launch date, and a stroked icon its class. Hover a glyph for details; hover the legend to isolate; open Controls to drive the recipe’s parameters.
Category — color
Test and training
Communications
Imaging, surveillance & meteorology
Navigation
Research
Class — icon
Business/commercial
Civil
Amateur/academic
Defense
Launch date — shade
19742020
A white dot marks U.K. satellites.
- Satellites in Space: 2,961 active satellites across 7 regions and four orbital types
- U.S.: 1,403 satellites
- Western Europe: 330 satellites
- China: 380 satellites
- Russia: 172 satellites
- Japan: 80 satellites
- India: 96 satellites
- Other: 500 satellites
Four channels on one composite glyph
The defining move of this graphic is a composite glyph: every mark carries hue, size, and shade, and a minority also carry a class icon — the “most marks are plain circles, a few are marked” model from the source:
- Icon → class. The base mark is always a filled circle. Only the classes in
iconMap get an unfilled, stroked icon drawn inside the circle (Civil → star, Amateur → triangle, Defense → chevron); the dominant Business/commercial class stays a plain circle, so only ~20% of marks are iconed. The icon is rendered with the sharedsymbolPathString helper — the same path generator the legend uses. - Color → category. A fixed semantic palette via
layoutConfig.colorMap. - Size → mass. An area-encoded radius scale (the same scale drives the graduated-circle legend).
- Shade → launch date. A continuous channel: the category hue is shaded lighter→darker by date. The
shade() helper interpolates inCIELAB, so the hue stays put while only lightness moves — older satellites read as a paler tint, newer ones as a deeper shade.
This composite (a hit-testable base mark + a decorator drawn in the overlay) is a pattern several recipes share — so the building blocks live in a small recipe chrome kit(roundedEnclosure, bandLabel, markCallout,readField) exported from semiotic/recipes for any custom layout to reuse.
Banded rows of packed clusters
packedClusterMatrix bins records into a column-per-region, cell-per-orbit matrix, then packs each cell as a beeswarm of variable-radius glyphs. In the default rowMode: "banded", orbit rows arealigned global bands: every region shares one y-range per orbit, so the row labels line up, a single rounded enclosure spans the columns for each orbit, and a region’s column is only as tall as its highest occupied orbit (so columns vary in height). Column widths track region counts, softened by proportionExponent so small regions like Japan stay legible. (rowMode: "stacked" instead sizes each column’s cells by its own counts — closer to the source’s organic mosaic, at the cost of aligned labels.)
Packing is deterministic and self-contained — a seeded jittered-grid seed plus a few spatial-hash relaxation passes (no d3-force in the hot loop, which keeps it fast even in non-scope-hoisted bundles). A maxAreaFraction cap scales every radius down to a packable density so clusters never overlap, on any canvas size. The geometry is cached by a content signature of the layout-affecting inputs, so hovering the legend, isolating a category, or returning from full screen re-styles the marks without re-packing — only a genuine data or dimension change re-packs.
What the frame provides for free
The recipe is a pure layout function; everything around it comes from theStreamNetworkFrame the chart wraps. Hover hit-testing across all ~3,000 marks, tooltips, an accessible description and summary, reduced-motion / high-contrast handling, theming, and server-side SVG/PNG export are inherited — the engineering you'd otherwise hand-write around a bespoke D3 graphic. The cluster enclosures, column headers, and row labels are drawn in the layout's overlays layer (pointer-events: none), so they decorate without intercepting a hover.
Customization
| What | Where | How |
|---|
| Class icon (composite glyph) | iconAccessor + iconMap | Stroked icon inside the filled circle; unmapped values stay plain. (OrsymbolAccessor/symbolMap to make the base mark itself the shape.) |
| Category hue | layoutConfig.colorMap | A fixed value→color map (wins over the theme palette) |
| Shade ramp | shadeAccessor + shadeStrength / shadeReverse | Numeric or date field; CIELAB lightness ramp on the hue |
| Row layout | rowMode + cellSizing + proportionExponent | "banded" (aligned bands) or "stacked" (per-column heights) |
| Packing density | maxAreaFraction + packPadding + iterations | Area cap (radii scale to fit), collision padding, relaxation passes |
| Callouts | layoutConfig.callouts | Leader lines to named marks: [{ field, value, label }] |
| Legend highlight | layoutConfig.highlight | { field, value } (or an array AND) dims every glyph that doesn't match |
Source Code
Abbreviated for readability — data arrays and steps are truncated. See the full runnable source in docs/src/examples/recipes/.
JSX
| 1 | import { NetworkCustomChart } from "semiotic/network" |
| 2 | import { packedClusterMatrix } from "semiotic/recipes" |
| 3 | import { generateSatellites, CATEGORY_COLORS, CLASS_ICONS, REGIONS, ORBITS } from "./satellites" |
| 4 | |
| 5 | const data = generateSatellites() // [{ id, region, orbit, mass, category, klass, launch, uk }] |
| 6 | |
| 7 | // One composite glyph per record, four data channels at once: |
| 8 | // color = category size = mass shade = launch date |
| 9 | // icon = class (most marks are plain circles; a few carry a stroked icon) |
| 10 | <NetworkCustomChart |
| 11 | nodes={data} |
| 12 | nodeIDAccessor="id" |
| 13 | layout={packedClusterMatrix} |
| 14 | width={1040} |
| 15 | height={660} |
| 16 | layoutConfig={{ |
| 17 | columnAccessor: "region", // matrix columns |
| 18 | rowAccessor: "orbit", // stacked cells within a column |
| 19 | sizeAccessor: "mass", // glyph area |
| 20 | colorAccessor: "category", // glyph hue |
| 21 | colorMap: CATEGORY_COLORS, |
| 22 | iconAccessor: "klass", // stroked icon INSIDE the filled circle ... |
| 23 | iconMap: CLASS_ICONS, // ... only for mapped classes (rest stay plain) |
| 24 | shadeAccessor: "launch", // glyph lightness (perceptual Lab shade) |
| 25 | markerAccessor: "uk", // white center dot for a flagged subset |
| 26 | columnOrder: REGIONS, |
| 27 | rowOrder: ORBITS, // bottom -> top |
| 28 | rowMode: "banded", // aligned orbit bands: labels align, one border per band |
| 29 | proportionExponent: 0.85, |
| 30 | callouts: [{ field: "name", value: "Hubble Space Telescope", label: "Hubble" }], |
| 31 | }} |
| 32 | frameProps={{ background: "#0a1330", tooltipContent: (d) => /* ... */ null }} |
| 33 | /> |