cytoscape.js v4 HEAD · 47a5c66d · built 2026-09-16 14:48Z

Changelog

All notable changes to Cytoscape.js are recorded here. This file starts at the 4.0 line; for the 3.x history see the releases page.

The format follows Keep a Changelog, and this project follows semantic versioning.


[Unreleased] — 4.0.0

v4 is a rewrite of the model and the renderer. The public API keeps v3's shapecy.add(), eles.filter(), node.position(), the traversal and algorithm surfaces, the alias spellings — while several v3 mechanisms were removed rather than reimplemented.

Porting a v3 app: read MIGRATING.md. It carries the recipe tables, the measured property-by-property diff, and the list of things that compile and then behave differently.

Not released. cytoscape@3 remains the released library.

Added

  • The layout audit's instrument (round 125.10 / 125.11): the debug page's live spacing slider and airiness readout, an options panel with every option the selected layout takes and the cy.layout() call it spells, debug/airiness.js's nearest-box probe shared with the suites, and npm run benchmark:layout-audit — one layout on one of the page's networks, headless, with the page's own sheet, printing the columns a sitting reads.

  • condense on circle and radial (round 125.3 / 125.5, grid's spelling): size the ring(s) by the nodes and avoidOverlapPadding rather than by the viewport box, so the gap between neighbours is the option and the box only centres. Off by default; the rings fill the box as before.

  • A seed on the random layout (round 125.8): the same seed places the same graph the same way, run after run — the stability every other built-in has. Omitted, the scatter is unseeded as before.

  • Taxi tracks — automatic turn distances per bundle (round 124, item 59): taxi-turn: auto takes an edge's px turn from a track the store assigns from live positions, so the runs of different fan-outs sit on distinct lines in the gap instead of one; taxi-track: source | target | family (default source) says which edges share a track and taxi-track-spacing (default 10) how far apart tracks sit. Style-side: edges keep routing themselves after a drag, under every layout. Flow gains edgeSep (default 10): the gap below a rank grows to max(rankSep, tracks × edgeSep + 20), and the long edges into one target share one chain anchored at the target so the taxi leg meets no node body on the way down. The track sweep runs whole once per geometry epoch (8 ms on a 1.9k-edge DAG, 160 ms on a pathological 8.7k-edge one); a conflict component past 128 bundles keeps the staircase order rather than the crossing rule (124.7).

  • Component packing on the discrete layouts, and the pack layout (round 123, item 58): packComponents: true on circle, concentric, grid, breadthfirst and radial lays each disconnected component out on its own — one ring, one set of rings, one grid, one tree per component — and shelf-packs the drawings largest first under componentSpacing, componentGroup, componentOrder and groupSpacing, force's spellings; off (the default) every layout is v3's one figure. Locked nodes are left out and the packed field is moved off them; an explicit boundingBox holds the field. cy.layout({ name: 'pack' }) re-packs the components where they stand under the same options, the largest held — layout-utilities' separateComponents with grouping and order, translation only. Breadthfirst draws its trees as blocks by default: each rank ordered tree-first, each tree a column band with its root over the middle of its subtree, the singletons a block of rows, the bands wrapped into shelves; one component is v3's picture exactly.

  • Force's re-pack takes a grouping and an order (round 121, componentGroup, componentOrder, groupSpacing): a function of each disconnected component's description ({ nodes, size, width, height }) keys the groups, which pack on their own and stand in a row by key; a comparator orders the components ahead of the largest-first order — so the EnrichmentMap shape (negatives left, positives right, rows by size with each row by score) is one force call. And components of five nodes and up turn to a canonical angle at the settle — the principal axis flat, or a ring's farthest node up — under tidyComponents.

  • Force's smallest components take canonical shapes (round 120, tidyComponents, default true): a pair stands as a vertical barbell, three make a point-up triangle, four a diamond, sized to the edge length and the bodies' clearance — so the re-pack lays small components out in rows by size, and two centre-aligned labels on a pair never sit side by side. A component with a locked node, or with mixed per-edge lengths, keeps the sim's shape.

  • The infinite force run (round 118.3, infinite: true): the live force-directed layout that never ends, ticking only while the field moves — at rest it costs nothing — and reheating on a drag (the grabbed node pinned for the gesture, its neighbourhood reflowing), a position() write, an added or removed element, or layout.reheat(); stop() lands the positions as they stand. On both executors. v3 had this only as the cola extension's infinite.

  • Force's overlap separation reaches 25k (round 118.1, open call 57). The settle's separation handed the 25k random scene back with more overlap than the sim gave it; a component of a thousand nodes or more with most of its nodes touching is now expanded about its centroid by what its median overlapping pair asks for before the local passes run, and a best-state guard restores the shallowest field any stage reached. 5k, 10k and 25k random scenes come out with no overlapping bodies, the pass runs in under 3 s at 25k, and the settled field grows under 2x.

  • Force can keep bodies apart in the sim, and honours boundingBox (rounds 116–118). avoidOverlap on force now says how: true / 'settle' (the default) separates the padded node boxes exactly after the settle; 'sim' runs a separation sweep — the settle's own primitive — after every tick on the CPU and GPU executors alike, so a streamed run is held open as it streams and ends clear with no settle pass; 'both' does both; false neither. The per-tick sweep is opt-in (117, open call 56): the settle's pass is cheaper for a one-shot run. (116.1 built the in-sim half as a short-range contact force and 117 made it opt-in as avoidOverlapInSim; 118.2 replaced the force with the projection — a force bounded by its gap clamp lost to a dense graph's spring pressure and annealed every run to the floor — and the flag with the mode.) boundingBox on force now holds the drawing — scaled down, never up, so every body lies inside, then centred (flow's rule, shared as fitBodiesToBox); a pinned node or constraints hold it back, as they hold the component re-pack. A locked child stays where it is when its compound parent is positioned, shifted or dragged — its own subtree with it — and the parent re-derives about the stayers and the movers (v3's rule).

  • Layout cleanup (round 114): every layout reads one set of node dimensions, labels included by default (nodeDimensionsIncludeLabels now defaults to true); locked nodes hold their place against position writes, position tweens and every layout, and are obstacles for overlap avoidance; force gains animateLive (the streaming run), avoidOverlap and avoidOverlapPadding, and its animate: true tweens to the settle like every other layout, with the shared finisher options (spacingFactor, transform, animateFilter, duration, easing, zoom, pan) applying; the extension contract gains ctx.nodeDimensions(), ctx.components(), ctx.finish(), overrides on ctx.layoutPositions() and body-box packComponents; flow's animated runs fit again, and a lone zoom or pan animates. Radial gains avoidOverlap (rings grow to clear it); concentric's chord rule reads a box's diagonal so two squares cannot meet corner-on; flow's boundingBox holds the bodies rather than their centres. test/layout-quality.mjs asserts placement, fit, overlap, locks, animation and component separation for every layout.

  • The flow layout (round 112): a built-in Sugiyama-class layered layout — cy.layout({ name: 'flow' }) — with greedy-FAS cycle removal, GKNV network-simplex layering, weighted crossing minimization and Brandes–Köpf coordinates, replacing the dagre extension for most uses. Emits node positions only: pair it with curve-style: taxi and edges route themselves, staying correct when nodes are dragged. Options include direction, nodeSep/rankSep, layering (network-simplex | longest-path | auto), thoroughness, per-edge minLength/edgeWeight (numbers, score mappings or functions) and rankConstraints (min/max/same id lists). Measured against dagre and elkjs by the layout-quality harness (npm run benchmark:layout-quality).

  • Faithful positionless round-trips (round 112): cytoscape.toColumnarElements now omits the positions column when no input definition carries a position (previously it fabricated an all-zero column), so a positionless graph serialized through the wire comes back positionless — consumers that lay out at load can tell the difference again.

  • Force-layout constraints (round 85.2, absorbing fcose's main draw): alignment: { horizontal, vertical } id-array groups (a locked member pins its group) and relativePlacement: [{ left, right, gap? } | { top, bottom, gap? }], with fixed nodes spelled lock(). Projection after each integration step; unknown ids, placement cycles, and contradictory locked members throw at start. Constrained runs take the CPU executor (measured: ~26 s vs ~0.4 s silent GPU at 25k — accepted for v1, since the constraint population runs at fcose sizes where the CPU settle is seconds).

  • A columnar, CPU-canonical model. Elements live in typed-array columns with stable slots, per-column coalesced dirty spans, a CSR adjacency index and a dictionary-encoded data() sidecar. Reads stay synchronous.

  • A WebGPU renderer: SDF node shapes, curved-edge families evaluated in the vertex stage from live positions, compute culling with indirect draws, GPU picking with a synchronous CPU fast path for nodes, an SDF glyph atlas, early-z, and an adaptive render scale. Gestures hit-test with v3's halos (8/24 rendered px around edges for mouse/touch, 2/8 around nodes), arrowheads are hit targets (hollow counts as filled, as in v3), and pressing any element — edges included — shows v3's :active overlay; cy.pick( x, y ) stays exact.

  • Pointer cursors (round 89): the canvas carries the standard affordances — grab over a draggable node, grabbing while it or the background is being dragged, pointer over any other interactive element, crosshair while box-selecting — and mirrors an active drag onto the document element, so the affordance survives a pointer-captured drag that leaves the canvas. v3 set no cursors at all, so idle over background stays '' (inherit) and the userland mouseover/mouseout recipe every v3 app used still shows through. pointerCursors: false turns the writer off entirely; an object overrides individual entries ({ pan: 'move' }). A touch pointer never gets a cursor.

  • cy.$() is back (round 64) as a plain alias of cy.filter() over the query/predicate forms (selector strings still throw), and cy.byId() joins $id/getElementById as the brevity id lookup. cy.collection() now throws if passed any argument — it used to silently ignore one where v3 builds from it.

  • Per-element bypasses (round 63): the stylesheet's bypasses section — { bypasses: { id: { prop: constant } } } — with v3's method spellings as sugar (ele.style( name, value ), the object form, removeStyle( name? ), removeCss). A bypass beats every sheet rule (default-sheet selection included), survives remove/ re-add of its element, and exports from cy.json() — better than v3, which drops bypasses on export. Constants only; a full cy.style( sheet ) replaces the section (spread the exported sheet to keep it). Style prop keys accept dash-case and camelCase everywhere (foo-barfooBar), the bypasses included.

  • Structured queries and predicates replacing the selector language — cy.nodes( { selected: true } ), cy.nodes( { data: { w: { gt: 1 } } } ), and plain functions for everything richer.

  • Element state is a style condition, which is what replaces v3's state selectors: { when: { selected: true } }, { active: true }, { locked: true }, { grabbed: true } and the rest, on any property. Each takes a boolean, so v3's negative selectors are the same key with false, and the same keys work as query keys. v4's default stylesheet carries v3's :selected, :parent:selected and :active blocks, spread before your own — so declaring the property replaces the rule, exactly as in v3.

  • A serializable mapper DSL for style: linear/log/sqrt/pow/ symlog/diverging/ordinal/threshold/quantize scales, OKLab colour interpolation with named schemes, and case conditionals. Paint channels evaluate in a compute kernel; anything read by culling, picking or a columnar scan stays CPU-canonical.

  • Columnar and binary loading. cytoscape.toColumnarElements(), cytoscape.serializeElements() / deserializeElements(), both accepted directly by options.elements and cy.add(). Numeric columns deserialize as zero-copy views.

  • Style transitions (transition-property/-duration/-delay/ -timing-function) and animation controls (pause/resume/reverse, read-only progress/paused).

  • The force layout — GPU-native force-directed layout, animating live at 100k nodes, with a CPU reference executor for headless and compound graphs; spectral (landmark-MDS) initial placement, degree-normalised springs, grid-pyramid long-range repulsion, per-compound gravity and nesting (gravityCompound, nestingFactor), and component packing (componentSpacing).

  • The radial tree layout (round 85.1, #2493) — hierarchy-aware: each subtree takes a contiguous angular wedge sized by its weight, multiple roots partition the circle in caller order, and orphan components get wedges of their own. Measured 2.8× faster than the breadthfirst-circle recipe at 2,000 nodes.

  • Data-driven layout mappings (round 85.3, #1514): { data, scale?, range?, invert?, default? } on force.edgeLength and concentric.concentric, { data, order? } on grid.sort, circle.sort and breadthfirst.depthSort. Function forms stay as escape hatches; the objects are the canonical, serializable spellings, resolved once at layout start, with loud validation (unknown keys, wrong-kind columns, scale/invert without a range all throw naming the option).

  • A registry-free extension contract: cy.layout( { impl } ) runs an imported class or object; LayoutContext is columnar-first, and it, LayoutImpl and CustomLayout are exported types, so an external layout author writes against real types rather than any. ctx.packComponents( spacing? ) (round 87.1) packs laid-out disconnected components into non-overlapping shelves — v3 layout-utilities' separateComponents in one call, and the same packing the force layout's componentSpacing uses.

  • Border and outline stroke styles on every shapeborder-style (solid/dashed/dotted/double, with v3's erase behaviour for double), outline-style, and border-dash-pattern/-offset. Dash patterns follow each shape's outline with the phase anchored where v3's canvas path starts, including exact elliptic arc length.

  • chart — v3's 101 numbered pie/stripe properties as one list-valued family with data-driven values, scheme palettes and donut holes.

  • visibility as a paint-only style property beside the structural show()/hide().

  • Per-side compound padding (round 85.4): padding-left/-right/-top/-bottom in the parents sheet group, each a number of px or 'N%' resolved per padding-relative-to; an unset side takes the uniform padding.

  • Slot compactioncy.compact() (alias cy.gc()) plus an automatic trigger, shrinking scan widths and buffers to the current graph rather than its peak.

  • boxSelectionMode ('contain' | 'overlap'), boxSelectionIncludesLabels, wheelSensitivity, desktopTapThreshold, touchTapThreshold, tapholdDuration.

  • eles.labelBoundingBox(), and labels join boundingBox()/fit() by default.

  • TypeScript declarations built from the source JSDoc, so the API documentation is hover text in an editor.

  • GPU executors for the expensive whole-graph algorithms (round 65). markovClustering, affinityPropagation, pageRank, floydWarshall, betweennessCentrality, kMeans, kMedoids, fuzzyCMeans and hierarchicalClustering accept executor: 'cpu' | 'gpu' | 'auto' (default 'auto'): the CPU is the bit-reproducible reference, the GPU runs WGSL compute kernels where WebGPU exists, and 'auto' picks per measured per-family crossovers. Measured on an RX 570-class adapter: Markov clustering up to 663× (31.3 s → 47 ms at 1,024 nodes), k-medoids up to 146×, fuzzy c-means up to 70×, Floyd–Warshall up to 28×, betweenness up to 18×, k-means up to 25×. PageRank and hierarchical clustering route to the CPU under 'auto': the CPU PageRank iterates sparsely (O(E) per iteration — orders of magnitude on sparse graphs) and the hierarchical merge engine went flat-typed, leaving the GPU no edge to win there.

  • Eight new algorithm families, designed matmul-first for the GPU tier (rounds 69–70), all on the same async executor contract and with no v3 counterpart. Round 69: eles.triangleCount() (per-node triangle counts, local clustering coefficients, total triangles and transitivity — A²∘A on the GPU), eles.neighborhoodSimilarity() (pairwise Jaccard / cosine / overlap coefficients over neighbor sets — A·Aᵀ on the GPU) and eles.katzCentrality() (attenuated walk counting; like PageRank its sparse CPU iteration owns 'auto' and the GPU path serves an explicit 'gpu'). Round 70, aimed at network-biology workloads: eles.randomWalkWithRestart() (seed-set network propagation — the disease-gene-prioritization primitive) and eles.randomWalkWithRestartProximity() (the all-pairs proximity matrix, a Neumann matmul iteration on the GPU), eles.heatDiffusion() / eles.heatKernel() (HotNet-style heat propagation; exp(−t·L) by scaling-and-squaring on the GPU), eles.effectiveResistance() (resistance distance and commute time off the Laplacian pseudo-inverse — f64 elimination on the CPU, Newton–Schulz matmul iteration on the GPU; O(n³) on both sides, so the GPU wins at every density), eles.simRank() (the Jeh–Widom recursive similarity, two matmuls per iteration) and eles.motifCensus() (the sixteen-class Holland–Leinhardt triad census — '030T' is the feed-forward loop — computed from seven trace primitives and pinned by a brute-force classifier spec). The whole-collection closenessCentralityNormalized joined the async tier in round 69: its GPU path rides the blocked Floyd–Warshall kernels and folds each distance row on the device, reading back n floats instead of the n² matrix.

Changed

  • Label boxes are measured where a canvas exists (round 125.1): the store measures label blocks with measureText at style time in a document or with an OffscreenCanvas, so a layout run before the first frame — the layout: option, an app's load — separates the boxes the frame will draw. Before, an estimate stood in until the first frame and 154 of em-web's 569 labels lay wider than it, so label-inclusive avoidOverlap left 14 overlapping pairs at load. Headless keeps the estimate. boundingBox() and fit() before the first frame see the same boxes.

  • Force's small-component shapes survive the settle's separation pass (125.1): the tidy pair, triangle and diamond are sized by the exact per-pair separation and the separation sweeps stay within a component when a re-pack follows, so avoidOverlap no longer pulls a diamond apart.

  • Flow reads extents per side and per direction (125.2): a label hung to one side of its body no longer counts on both sides, a rightward flow separates its ranks by heights and its rows by widths rather than the reverse, and a node nothing aligned (a free singleton) moves to the median of its neighbours within its rank's slack instead of the leftmost feasible x — the "IRAK1 far from its only parent" case, 1,632 px to 68. Reactome with labels: 21.7 to 17.0 Mpx²; the Greek gods rightward with labels: 3.42 to 1.62.

  • Radial infers a component's true roots (125.3, the maintainer's sitting): the nodes with no incoming edge, falling back to the maximum-degree rule only for a component that has none (a cycle). Reactome's centre was Innate Immune System (degree 17) with the one true root on ring 1; the hierarchy now reads outward from its root.

  • Breadthfirst is sized by the pixel viewport (125.4), as grid, circle, concentric and radial are; it read cy.extent() in model coordinates, so its drawing scaled with the zoom at the moment it ran (nine times airier on a page fitted at zoom 0.11). At zoom 1 nothing changes. A compound parent is never a root (an edgeless scope parent made every npm-deps position NaN under the undirected default).

  • The shelf packer stacks short components under earlier columns (125.9): a row holding one tall component and several short ones used to leave the short columns empty below; now a box goes under the leftmost earlier column of its row with the room (first fit, spacing kept, like-sized boxes never stacked, off under a componentOrder). em-web under radial + packComponents: packing efficiency 0.66 to 0.83; the pack layout 0.58 to 0.67.

  • Preset rejects a half position (125.8): a map entry of { x: 100 } wrote y: NaN into the store and a null axis became 0; both now throw a TypeError naming the node.

  • The line casing draws per edge, in v3's order (round 124.4): line-outline-* used to be a global pass under all lines — a halo against nodes only; now each edge draws its casing then its line in z order, so a later edge's casing gaps an earlier edge's line where they cross, as v3 draws it (the parity scene's difference fell from 72 px to 10). Anyone using a casing sees gapped crossings now; the old global-halo look is the edge underlay.

  • The non-presenting GPU force batch is priced before doubling (round 121.5): the device's time on the last completed batch bounds the next to what a 100 ms budget buys, so the ramp no longer overshoots on a heavy scene (the 25k scene's batch rides 3–16, and no run frame passes 50 ms). And the run's synchronous start is shorter (121.6): the spectral seed's BFS is typed, 190–204 → 70–75 ms at 25k × 50k, bit-identical.

  • The GPU force executor's iteration cost (round 119): the grid's cell scan is a 256-thread workgroup scan rather than one thread walking every cell (3.2 ms an iteration on 569 nodes before, 19 ms at 10k), and a run nobody watches mid-run — animate: false, or animate: true's tween — batches up to 64 iterations per frame by what the device keeps up with instead of the live stream's three. em-web's silent run 1.5 s → 0.3 s; the 25k scene 11.8 s → 3.3 s. stepsPerFrame now paces only animateLive and infinite.

  • Force's threshold defaults to 2% of the mean ideal edge length (round 119.3; 1.2 px at the default length) for animate: false and the tween, measured to reproduce the 0.1 px settle's edge lengths, stress and overlaps within noise on seven fixtures at 2–3× the speed; animateLive and infinite keep 0.1 px, since a stream's stop is visible. Pass threshold to pin either. em-web's silent run on the page: 0.3 → 0.15–0.24 s.

  • The stylesheet is { nodes, edges, parents, core } — an object of property objects, not a list of selector blocks. State-dependent styling is a case condition rather than a :selected-style block.

  • Draw order is structural and stays that way: compound parents, then edges, then leaf nodes, then labels; slot order within a stream.

  • Picking answers in the reverse of that order: leaf, then edge, then compound parent (round 97), so what you see is what you pick. A click or hover on an edge crossing a parent's body now answers the edge, where it used to answer the parent the renderer had drawn underneath it; nesting depth does not change the order, and among parents the deepest still wins. cy.pick() states the order as contract. Two visible consequences: hover styling on a parent body stops firing where an edge lies under the cursor (v3 behaves this way, so it is parity restored), and a click inside a parent body waits for the edge tile — one GPU roundtrip — where it used to answer synchronously. v3 ordered these by z-index / z-compound-depth, which v4 does not have, so a deeply nested v3 parent could beat a shallower edge where v4's edge wins. Dragging is unchanged: a press still grabs the parent body it lands on.

  • force under animate: false on a rendered flat graph is async (round 87.2): executor choice is availability-driven, so a flat rendered graph with a device hands integration to the GPU for both animate values, where it used to run the CPU sim synchronously on the main thread. Presentation does not pick the executor: since round 114 animateLive: true streams positions to the screen per frame, animate: true integrates silently and then tweens to the settle, and animate: false integrates silently off-mirror while the screen holds the pre-run frame until the settle lands in one write. The visible consequence: positions are readable at layoutstop / promise(), not on the line after run(). Headless and compound-graph runs keep the synchronous CPU path, so specs and servers are unaffected.

  • Animations run concurrently by channel and sequence by promise; overlapping channels evict the older animation in place.

  • The expensive whole-graph algorithms are async (round 65; the closeness family joined in round 69): the executor-tier methods above return promises — await the call, then use the result exactly as in v3. Runtime option validation on them surfaces as a rejection; a bad executor value still throws synchronously. The traversal tier (bfs, dfs, dijkstra, aStar, bellmanFord, kruskal, components, degree centrality and the single-root closenessCentrality) stays synchronous; only the O(n³) whole-collection closenessCentralityNormalized moved.

  • hierarchicalClustering's mean linkage works (round 65.10) — a deliberate deviation: v3's mean linkage never assigned the cluster sizes its weighted-average formula read, so the first mean merge produced NaN distances and the linkage silently degenerated. v4 tracks sizes; mean is the weighted-average linkage the docs always claimed.

  • Colours tween in OKLab, matching the mapper default (v3 tweened per-channel in sRGB).

  • spring( bounce ) replaces spring( tension, friction ).

  • Default curve-style is straight (v3: bezier), and default text-valign is bottom (v3: top).

  • cy.elements() returns nodes then edges, not mixed insertion order.

  • Positions are Float32 (~7 significant digits).

  • Compound event bubbling is v3's, with the remaining ordering deviation confined to within a phase (registration order).

  • stop( jumpToEnd ) — the clearQueue argument is gone with the queue.

  • font-family, font-style, font-weight are global constants, one face per glyph atlas.

  • Rendering requires WebGPU; headless requires nothing.

  • Comparing elements across two instances throws instead of answering wrongly. Element identity is a slot in one store, so the first node of one graph and the first node of another used to compare as the same element: same() returned true, intersection() returned everything, difference() returned nothing, and union() silently dropped the other graph's elements. The twelve affected methods (same, anySame, contains, indexOf, union, difference, intersection, symmetricDifference, diff, allAreNeighbors, edgesWith, edgesTo) now reject a collection from another instance.

  • A corrupt binary payload fails fast rather than allocating for what it declares. Three cases found by fuzzing — an out-of-range dictionary index, an over-long packed-id blob length, and an impossible data-key count — could hang a load or take tens of seconds before erroring; each now throws a contract error naming the field.

Removed

  • The round-90 API review's parity baggage: cy.forceRender(), cy.batchData(), cy.mutableElements() (it was elements() by another name), onRender/offRender (use cy.on( 'render', … )), and the bind/unbind/listen/unlisten listener aliases — the event surface follows Node's EventEmitter spellings, plus pon. Demoted rather than removed (working, but @internal and out of the typed surface): cy.renderer()cy.stats() is the public frame-stats snapshot in its place — instanceString(), the silentPosition(s)/silentShift writes, the StyleEngine and animation machinery, and the Viewport class.
  • Selector strings, everywhere — and cy.$() with them (cy.$id() is the id lookup). Passing one throws, naming the replacement.
  • Classes (addClass/removeClass/toggleClass/hasClass/ flashClass) — data() plus mappers is the replacement.
  • Style functions (( ele ) => props) throw; per-element bypasses are back since round 63 (see Added), spelled as in v3 and canonically the stylesheet's bypasses section.
  • CSS-string stylesheets and cytoscape.stylesheet().
  • z-index, z-compound-depth, z-index-compare, sortByZIndex, zDepth.
  • restore(), clone(), copy() and the import form of cy.json(): removed elements are terminally dead.
  • The animation queue, the queue option and the step callback (all three spellings throw).
  • Custom easing functionscubic-bezier() and linear() cover any drawable curve.
  • Event namespaces: a type is matched whole, so 'tap.ns' is one literal name.
  • The vmouse* aliases and raw mouse/touch re-emits (mousedown, click, touchstart, …) — pointer* is their modern spelling. mouseover/mouseout still fire. These names still register and then never fire, because custom event names must stay legal.
  • cy.notify() / noNotifications() — the renderer is dirty-driven.
  • renderTo; per-element font-family; viewport-fixed labels.
  • The canvas-era performance optionshideEdgesOnViewport, textureOnViewport (+ outside-texture-bg-*), motionBlur, motionBlurOpacity.
  • Style properties: background-blacken, bounds-expansion, content, position, display, text-metrics, box-selection, box-select-labels, edge-text-rotation, the min-*-bias-* quartet, the singular control-point-distance/ segment-distance/segment-weight/segment-radius spellings, the mid-*-arrow-fill/-width pairs, and the numbered pie-N-*/stripe-N-* families. The no-dash shape spellings (roundrectangle, cutrectangle, concavehexagon) throw in all three enums that took them.
  • The cose layout — not ported; force is v4's answer.
  • The extension registry — no cytoscape.use(); extensions are imports.

Not yet implemented

  • text-border-style (the label box border does not dash). border-style/outline-style and border-dash-pattern/-offset themselves work on every shape; border-cap/border-join are dropped (dash ends are perpendicular cuts by construction).
  • Core, collection and renderer extension points.

Decided against rather than pending: cytoscape.warnings() (errors throw and warnings warn, with no toggle over either — the fail-loudly contract stands whole) and functional preventDefault() for v4's own gesture defaults (the explicit toggles are the gesture-control surface; the call reaches the browser's default only).

Known deviations

Accepted differences from v3's rendering and semantics — arrow tips on approximate boundaries for some shapes, butt caps on layer strokes, outline dash phase on polygon-family shapes, a conservative edge-label bounding term, no decimation on the curved edge stream, and others — are enumerated in src/README.md under "Known deviations from v3". Each is recorded where the feature is described, with the reason.

One is worth naming here because it is visible in ordinary styling: v3 makes a hollow or translucent arrowhead read as one shape with its edge by erasing the head's footprint from the canvas, and v4 shortens the line instead — no extra pass, and the same pixels wherever the head covers the line. It does not reach mid arrows, which sit mid-line, so arrow-fill: hollow on a mid-source/mid-target head still shows the line through it.