Spike: Resolve per-type graph styling off both selectors and element data
- Dominant language
- TypeScript
- Stars
- 481
- Forks
- 108
- Avg merge
- 6d 8h
- Merged PRs (30d)
- 5
Description
## Goal
Validate an architecture that resolves per-type vertex/edge styling **neither** through per-type Cytoscape selectors **nor** through per-element `data()` — so the canvas can be O(1) in style-context count (the #2104 win) *and* free of per-element display state (the #1887 reconciler's requirement), while making restyles explicit and cheap. This must be settled before the reconciler or further style work commits to an implementation, because #2104 (landed) and #1887 (planned) currently hold contradictory models of what an element carries.
## Background / why now
- #2104 fixed the schema-view lockup by replacing ~20k per-type selectors (`node[type="X"]`) with a single `node`/`edge` rule reading precomputed `ge_*` values off `ele.data()`. Style-context count O(elements × selectors) → O(1); Cytoscape STYLE self-time ~88% → ~0% at 10k types. It is a deliberate **stepping stone**, not the durable end state.
- It introduced two downsides:
1. **Data duplication** — every element carries ~13 `ge_*` fields whose values are per-*type* yet copied per-*element*; the current `cy.json(cloneDeep(...))` full-replace copies them all on every change.
2. **Implicit reactivity** — a style edit reaches the canvas only because the element-build hooks re-read style atoms and rebuild the element array; there is no explicit style→canvas channel.
- #1887 (reconciler) is on a collision course: its founding decision is "elements carry structural identity **only**; display resolves at paint time, never baked in." `ge_*`-on-elements reintroduces exactly the baked-in display state that defeats reference diffing — a color edit changes every element's `ge_*` → new references → the reconciler sees "everything changed." #1887's body also still assumes `node[type="X"]` selector-driven styles (pre-#2104).
## The three-way constraint
A durable answer must satisfy all three at once:
1. **O(1) style-context count** regardless of type count (#2104).
2. **No per-element display/style state** so element references stay stable across restyles (#1887).
3. **Explicit, cheap restyle** — a Type style edit updates the canvas without rebuilding elements (#1887 user story; #1725).
Cytoscape natively offers only per-type selectors (fails #1) or per-element data (fails #2). The spike finds and validates the escape.
## Hypothesis to validate (the "unifying bet")
Combine three moves so styling stops being smuggled through the element pipeline:
1. **Class-based dedup** — each element carries one stable style-class keyed on its *distinct resolved style* (not its type). Stylesheet has one rule per distinct style (usually ≪ types; a uniform schema collapses to one rule). Element data shrinks to ~1 class ref. Degrades to O(types) only in the pathological "every type uniquely styled" case — the same worst case as selectors, so no regression.
2. **Style-registry bridge** — an external store (mirroring the existing `iconRegistry` + `useSyncExternalStore` pattern) owns type→style; a thin bridge patches the stylesheet / `ele.data()` imperatively on change, outside React render, decoupled from element identity.
3. **Incremental element updates** — ride on the #1887 reconciler (kill `cloneDeep` full-replace) so element data is diffed/patched, not rebuilt + cloned.
Result: element data ≈ 1 class ref; style values live once per distinct style; restyle = patch one rule / the registry, no element rebuild. Both downsides dissolve.
## Open questions the spike must answer
- **Function-mapper vs class vs `data()`:** a Cytoscape style *function* mapper (`ele => typeStyleMap.get(ele.data('type'))`) gives O(1) selectors + a shared type→style map + no per-element style data — and #1887 already proposes exactly this for edge labels. Why did #1725 deliberately choose `data()` string mappers *over* function mappers? Resolve this (function-mapper caching / perf cost?) — it decides between the function-mapper and class approaches.
- **Class-membership churn:** when a type's style changes bucket, element class assignments must update — measure that this stays cheap (or that the registry / data-patch path handles it).
- **Composition with the reconciler:** how the style channel interleaves with #1887's diff/apply batch without double-work or ordering hazards.
- **Scale:** holds at 10k types / 68k edges, and 70k (#1677)? Benchmark restyle latency, memory, and reference-stability under a Type color edit.
## Expected Outcome
- A recommendation (function-mapper vs class-based; whether a registry bridge is warranted) backed by a throwaway prototype measuring style-context count, restyle latency, element-data memory, and reference stability under a Vertex-Type color edit at 10k+.
- An updated implementation plan feeding #1887 and the styling work, replacing #1887's stale "selector-driven styles" assumption.
- Confirmation the approach composes with the reconciler.
## Out of Scope
- Building the reconciler engine (#1887) or the final styling implementation — this spike unblocks them.
- Layout performance (fcose at scale) — the next wall, tracked separately.
## Related Issues
- Originated from #2104 (the landed stepping stone and its two downsides)
- Gates / blocks #1887 (reconciler must not commit to a styling model until this resolves)
- Supersedes the open question in #1725 (#2104 did the full `data()` conversion)
- Related to #1797 (broader Cytoscape redesign spike) and #1891 (parent initiative)
- Part of #2091
> [!IMPORTANT]
> Internal only — this issue is maintained by the core team and is not accepting external contributions.
Contributor guide
Research direction
Start by reviewing the assumptions and changes in issues #2104, #1887, and #1725, then inspect the Cytoscape styling entry points used by the application. Build a throwaway prototype comparing function mappers and class-based styling, and measure style-context count, restyle latency, memory, and element-reference stability at the stated scales.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend, performance
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100