aws / aws/graph-explorer

Spike: Resolve per-type graph styling off both selectors and element data

Open
#2,111 0 comments 0 reactions 0 assignees View on GitHub
exploration internal performance ready-for-human tech debt
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.