Skip to content

Fix/bubble group geometry#8

Merged
Mnikley merged 19 commits into
mainfrom
fix/bubble-group-geometry
Jul 22, 2026
Merged

Fix/bubble group geometry#8
Mnikley merged 19 commits into
mainfrom
fix/bubble-group-geometry

Conversation

@Mnikley

@Mnikley Mnikley commented Jul 22, 2026

Copy link
Copy Markdown
Member

1.16.0 - Bubble-set rework & Excel merge-import

Bubble groups

  • New styling controls: Padding and Corridor Width sliders (scale with node size) + Avoid Other Nodes switch.
  • Outlines render as smooth curves at every zoom (canvas, PNG and SVG identical); hulls are node-size-aware and hug members tightly at minimum settings.
  • Guarantees: members are never clipped or lost; enclosed non-members are carved out visibly instead of silently swallowed; no more jagged/wobbly outlines around dense graphs.

Data editor

  • ⤒ Import merges an Excel workbook into the loaded graph with a preview modal (new/updated counts, new columns, ignored rows).
  • Two join modes: Extend & add (upsert, default) or Extend existing only (matched rows only, no dangling edges).
  • Fix: re-saved workbooks with rich cell values (formatted text/hyperlinks/formulas) import cleanly.

Compat: saved files load unchanged; stored bubble knob values keep applying (padding/corridor are now node-radius fractions, avoidance >0 = on).

Mnikley added 19 commits July 21, 2026 14:45
Per-group Padding and Corridor Width sliders multiply the bubblesets-js
influence-field radii (nodeR0/R1 and edgeR0/R1) so tight two-node blobs can
be shrunk and thin arms to outlying members thickened. Values persist with
bubbleSetStyle in JSON; multipliers are clamped to [0.25, 4] in geometry.

Also fold each member's on-screen radius into the outline identity checksum:
the fit consumes node radii, so a size change (or a post-load size settle)
now invalidates the cached hull instead of leaving a stale outline until
membership changes.
…d hulls

The influence field now scales with the group's mean member radius instead
of bubblesets-js' absolute pixel constants (tuned for ~15px nodes), so hulls
stay visually proportional at any configured node size and the padding/
corridor knobs read as fractions of a node. The legacy scale option is gone;
resolution floors keep every field radius at marching-grid scale and
pixelGroup is clamped to [1, 4]px.

Members are now guaranteed enclosed: bubblesets-js validates only center
points, so avoid-node pressure plus B-spline shrinkage could clip a node by
its own hull (measured -12px). Grazed members get full-padding discs unioned
in; disconnected pieces are joined with corridor-width capsule links so the
result is always one simple ring. Rings are snapped to a 1/32px grid before
clipping (polygon-clipping throws on float-noise segments, which silently
disabled the guarantee) and the sample stride adapts to the thinnest field
feature to prevent B-spline hooks.

Interior non-members are carved as holes via polygon difference (the field
can only fjord the boundary), rendered even-odd on canvas, PNG and SVG.
Avoid-node moves now invalidate the outline cache; morphBuffer no longer
scales with the knobs (corridor rerouting painted phantom lobes).
New per-group Avoidance slider (0-3): multiplies the non-member negative
field; 0 lets the hull cover other nodes (and skips avoid routing entirely),
higher steers around them harder. Defaults retuned for the node-size-aware
field: padding 0.1, corridor 0.25 (razor-tight hugs with thin finger
corridors, per live feedback); padding/corridor slider minimum lowered to
0.05. Saved workspaces keep their stored values; missing avoidance fills
to 1 via the existing defaults merge.
Remove an unused variable and four extraneous f-string prefixes.
Paint bubble rings as Catmull-Rom cubic Béziers (tension 1/6) on both the
live canvas and the SVG export, consuming identical control points from the
new bubble_smoothing.js — the polygon stays the source of truth for all
math, only the painters interpolate. Raise the outline simplify tolerance to
half a grid cell (fewer control points now mean smoother curves, not facets)
and replace rectangular straggler links with stadium capsules.

The member-enclosure guarantee now measures clearance against the sampled
smoothed curve (what is actually painted) and repairs in up to three rounds
with growing discs, since disc-union junctions can themselves smooth inward
past a member. Avoidance becomes boolean in the geometry (any value > 0 = ON;
multiplying bubblesets' non-member factor never worked — the library's
marching iteration fights back), and squeezed avoid holes fall back to the
largest member-clearance-respecting radius instead of silently swallowing
the node (floor 0.35× its radius, documented ceiling).
Replace the Avoidance slider with an 'Avoid Other Nodes' switch (the value
had no useful continuous range) — persisted as numeric 0/1 so saved
workspaces stay compatible; legacy values > 0 load as ON. The switch names
its checkbox via aria-label since the row label is a separate element.

Retune group defaults from padding 0.1 / corridor 0.25 to 0.15 / 0.3: the
marching-grid floor gives small nodes proportionally more margin, so large
nodes rendered visibly strangled at the old values (side-by-side r=9 vs
r=35 renders). Hole breathing room gets a matching floor of 0.25× the
carved node's radius.
…nobs

Live feedback on a dense STRING network: the negative field of hundreds of
non-members makes the contour visibly wobble wherever it threads between
them, and one corridor-severing hole dropped ALL carves wholesale, leaving
swallowed non-members with no visual out. The user prefers smooth hulls over
avoidance, so:

- avoidance now defaults to OFF (per-group switch turns carving back on);
  saved workspaces keep their stored value
- padding/corridor floors drop to 0.01 (slider min/step 0.01) and defaults
  tighten to 0.05 / 0.1 — the resolution floors plus the enclosure guarantee
  make near-zero knobs safe, giving hug-tight bodies and finger-like arms
- carveAvoidHoles retries discs one by one when the batch difference breaks
  the one-ring guarantee, so a single bad hole no longer silently swallows
  every other non-member (subtractHoleDiscs)
Remove the marching-grid floors on the influence-field radii: they fattened
minimum knobs to ~half a node radius on dense graphs (small ratio-1 node
radii), which is why padding/corridor 0.01 still painted wide. A field
thinner than a grid cell now simply registers nothing — skipped entirely
when no radius reaches grid scale — and ensureMembersEnclosed reconstructs
the hull geometrically: a disc per member (hugging each node's OWN radius,
min 2 px pad) joined by thin stadium capsules (min 1.25 px half-width).

connectPolygonComponents links each piece to its NEAREST component instead
of everything to the largest one — nearest-neighbor rounds grow an MST-like
tree of short fingers where the old rule painted a star radiating from one
node. Catmull-Rom control offsets are clamped to 0.35× their segment's
chord: uniform parameterization overshoots where a short junction segment
neighbors a long capsule side, cutting through the discs it should hug
(inactive for evenly spaced rings). Outline simplify tolerance rises to one
full grid cell — with curve painting, fewer control points mean smoother
hulls and less field-summation scallop.
New ⤒ Import button in the data editor header loads an Excel workbook
into the current graph instead of replacing it. A preview modal shows
what the merge will do before anything is applied: new / updated node
and edge counts, new property columns, unchanged and skipped rows, and
capped id lists.

- src/utilities/excel_merge.js: pure computeMergePlan (id-matched
  overlay of label/description/type/style + deep D4Data merge, group-
  contiguous header union, change detection) and the preview modal.
- parseExcelToJson gains merge options: a single nodes/edges sheet
  suffices and edge endpoints validate against existing graph nodes,
  so a workbook can attach new edges to nodes already in the graph.
- DataTable.update() rebuild path extracted into rebuildGraph() and
  shared with the import flow; workspaces, positions, filters and
  styles are preserved. X/Y coordinates in the file seed positions for
  new nodes only.

No version bump or changelog entry yet — happens on main after testing.
Remove an unused variable and four extraneous f-string prefixes.
The straight lines below the field's resolution limit were the geometric
capsules: the marching grid physically cannot trace corridors thinner than
~two grid cells, so the thin-corridor range is always capsule-built.
capsuleRing now runs along a gentle quadratic arc (sagitta 8% of length,
capped by tube width, bulge side stable across refits) with semicircular
caps — thin corridors read organic instead of ruler-drawn.

The leftover space around members at padding 0.01 was the absolute 2 px
disc-pad minimum, which is ~a quarter node radius on dense graphs (small
ratio-1 radii) and magnifies under zoom. Reconstruction minimums are now
PROPORTIONAL: disc pad ≥ 5% of the member's own radius, capsule half-width
≥ 15% of the group's mean radius — knob minimum means visually hugging at
every node scale and zoom.
…oid on

User-picked defaults after the live tuning rounds: snug-but-not-touching
padding, corridor at the field-organic routing threshold, avoidance
enabled (per-disc hole fallback makes it safe on dense graphs).
Workbooks re-saved by Excel/LibreOffice can carry rich cell values
(rich-text runs, hyperlinks, formulas). The parser passed these through
as raw objects, which (a) made every matched row look modified in the
import preview — snapshot compare of object vs stored string — and
(b) crashed the category filter dropdown after the merge rebuild
(TypeError: val.toLowerCase is not a function).

Normalize cell values at the parse boundary (richText joined, hyperlink
text, formula cached result; error/unevaluated cells become empty) for
headers and data alike — this also hardens the full-load path, which
had the same latent bug. DropdownChecklist.sortCategories additionally
coerces to String so hand-crafted JSON files cannot crash the panel.

Adds regression tests incl. merge idempotency (a second import of the
same file must report no changes).
The preview modal now offers two merge modes: 'Extend & add (full outer
join)' — the previous upsert behavior, still the default — and 'Extend
existing only (left join)', which enriches matched rows but ignores file
rows without a match in the graph (including edges onto skipped new
nodes, so no dangling edges). New property columns apply in both modes.
Stats, notes and the Import button state re-render live on toggle, and
showMergePreview now resolves the chosen plan instead of a boolean.
Two handleImportFile tests drive the real preview modal against a
stubbed parser: choosing left join applies that plan (and reports the
ignored rows), canceling leaves the graph untouched. Also extracts the
join-mode picker into its own builder.
@Mnikley
Mnikley merged commit c2b9bbe into main Jul 22, 2026
2 checks passed
@Mnikley
Mnikley deleted the fix/bubble-group-geometry branch July 22, 2026 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant