Skip to content

Help wanted: making the canvas "Tidy up" (untangle connections) algorithm even better 🙏 #18

Description

@gsvprharsha

Hi everyone 👋 first off, thank you for taking the time to even read this. We'd genuinely love some help here, and any contribution (an idea, a comment, a small PR) would mean a lot.

Where we could use a hand

Tempest's threads canvas has a "Tidy canvas" action (right-click the canvas, then Tidy canvas). Its job is to untangle the connections between nodes when a canvas gets messy, when wires cross over each other and run on top of nodes.

It works, but it isn't quite there yet, and we think this is a lovely, self-contained problem for anyone who enjoys graph layout. We'd be really grateful for help making it better. 🙏

What it does today

The current approach is a force-directed layout built on d3-force (already a dependency, so no new packages needed). We went this route because the edges on the canvas are associative, not directional. There's no real parent to child flow to rank into layers, so a Sugiyama/hierarchical layout would invent a hierarchy that isn't in the data.

The simulation currently uses:

  • forceLink: springs pulling connected nodes toward r₁ + r₂ + 80 px apart
  • forceManyBody: charge repulsion, strength -(r * 12)
  • forceCollide: radius r + 24, so large nodes don't clip
  • forceCenter: pins the cluster on its own centroid so it doesn't drift off-screen
  • Each node's radius r is half its diagonal, so bigger nodes claim proportional space
  • Only nodes that carry an edge are laid out; isolated nodes are intentionally left where the user put them
  • The sim runs 300 synchronous ticks, then positions are persisted and the view re-fits

Where to look

  • File: src/components/ThreadsView.tsx, the tidy callback (around line 290)
  • The three "magic" constants that shape the result are the link distance (80), the charge strength (-r * 12), and the collision padding (+24)

Where it still falls short

We'd especially welcome thoughts or PRs on any of these, or anything we haven't thought of:

  • Edges still cross on denser graphs; the crossing-reduction isn't as clean as we'd like
  • Force constants are hand-tuned and fixed; they settle too tightly on some canvases and too loosely on others, with no adaptive scaling to graph size or node dimensions
  • Rectangular nodes are approximated as circles for collision, so wide nodes reserve more space than they need and can still clip at the corners
  • Isolated nodes are ignored entirely, so a left-alone node can end up overlapping the freshly-packed cluster
  • Determinism: the same messy layout doesn't always tidy to the same result

Ideas we'd love to explore with you

None of these are decided, and we'd genuinely like to hear what you think:

  • A dedicated crossing-reduction pass on top of (or instead of) the force sim
  • Rectangle-aware collision instead of circle approximation
  • Adaptive force constants scaled to node count and sizes
  • Trying elkjs or d3-force's newer helpers and comparing results

If any of this sounds fun to you, please don't hesitate to comment, ask questions, or open a draft PR. Even a rough experiment is hugely welcome. Thank you so much for considering it. 💛

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions