Skip to content

Fix: Change layout engine logic - #108

Merged
AlexIchenskiy merged 6 commits into
release/1.0.0from
fix/change-layout-engine-logic
Mar 5, 2026
Merged

Fix: Change layout engine logic#108
AlexIchenskiy merged 6 commits into
release/1.0.0from
fix/change-layout-engine-logic

Conversation

@AlexIchenskiy

@AlexIchenskiy AlexIchenskiy commented Mar 4, 2026

Copy link
Copy Markdown
Collaborator

This PR aims to refactor the core simulation and position calculation logic, fix some long-standing bugs - such as issues with render and recenter callback functionality - and add long-awaited features like simulation cancellation.

List of the most important changes:

  • Restructured layout engines into engines/dynamic/ and engines/static/ subdirectories with a shared BaseLayoutEngine abstract class that consolidates common fields (_nodes, _edges, _nodeIndexByNodeId), cancellation support (_cancelSimulation), and a MessageChannel-based scheduler (_scheduleNext) for non-blocking chunked execution

  • Added chunked asynchronous execution to static layout engines (circular, grid, hierarchical) with cancellation support, _pendingRecalculation queuing to prevent batch operations from being silently dropped, and _emitProgress for progress tracking

  • Added chunked asynchronous execution to the force layout engine's _runSimulation, replacing the synchronous blocking loop that froze the main thread during simulation

  • Changed the alpha parameter to drop number of iterations per simulation from ~300 to ~100, improving performance

  • Removed the separate SimulatorSettings abstraction (IForceLayoutSettings) and folded its options into IForceLayoutOptions, collapsing setSettings() and setLayoutEngine() into a single setSettings(ILayoutSettings) call - establishing layout settings as the single abstraction layer for all position-recalculation configuration

  • Fixed render callback timing - render(onRendered) now fires the callback after SIMULATION_END + RENDER_END instead of synchronously before the simulation completes.

  • Fixed transition callbacks - recenter(), zoomIn(), and zoomOut() now use .on('end', ...) instead of .call() for post-transition callbacks

  • Fixed the throttle function - rewrote it with proper time-based gating to prevent stale closures and frozen renders

  • Cleaned up the D3 simulation lifecycle - _resetSimulation now properly stops and detaches listeners before creating a new simulation

  • Simplified and improved the web worker - replaced the verbose switch/case with a handler map, removed the SetLayoutEngine message type, created the engine lazily on the first SetSettings, and enabled real-time engine switching using the SetSettings message

  • Designed an IGraphInteraction interface to cleanly separate entity interaction concerns (select/unselect, hover/unhover by ID) from the core graph data model, exposed via view.interaction.* - consistent with the existing view.data.* pattern

  • Fixed the recenter() logic to accept options so that it recenters correctly for edge cases such as unbalanced tree views

@AlexIchenskiy AlexIchenskiy self-assigned this Mar 4, 2026

@tonilastre tonilastre left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! The structure or layouts and engines are well done, and easy to extend in the future. And the performance, 🚀 !

I also like the graph interaction part because it was always hard to maintain those states from internal node/edge functions.

I've added some comments, mostly nitpicking.

Comment thread src/simulator/shared.ts Outdated
setSettings(settings: ID3SimulatorEngineSettingsUpdate): void;
setSettings(settings: DeepPartial<ILayoutSettings>): void;

getIsSimulationRunning(): boolean;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicking, but can this be isSimulationRunning()?

this.emitToWorker({
type: WorkerInputType.SetSettings,
data: settings,
} as IWorkerInputPayload);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have new typescript in this project, you can use satisfies here instead of as.

Comment thread src/models/interaction.ts
unhoverAll(): number;
}

export class GraphInteraction<N extends INodeBase, E extends IEdgeBase> implements IGraphInteraction {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cool!

}

if (data.edges) {
this._edges = this._edges.concat(data.edges);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why edges are not being merged by id?

);

let lastProgress = -1;
let i = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicking, but does it make sense to call this step instead of i because i is usually used a blocked locally variable, this i is all around in runChunk.

) {
const angleStep = (2 * Math.PI) / nodes.length;
let lastProgress = -1;
let i = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as before for i

const rows = Math.ceil(Math.sqrt(nodes.length));
const cols = Math.ceil(nodes.length / rows);
let lastProgress = -1;
let i = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here :), and probably on other engines

isPhysicsEnabled: false,
alpha: {
alpha: 1,
alphaMin: 0.05, // default alphaMin is 0.001, which results in 285 ticks to converge. Using 0.05 converges to similar stable results in 106 ticks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment thread src/views/orb-view.ts Outdated
Comment on lines +295 to +304
const layout = this._settings.layout;
const isHorizontal =
layout.type === 'hierarchical' && (layout.options as IHierarchicalLayoutOptions).orientation === 'horizontal';
const isVertical =
layout.type === 'hierarchical' && (layout.options as IHierarchicalLayoutOptions).orientation === 'vertical';
const reversed = (layout.options as IHierarchicalLayoutOptions).reversed;
const recenterOptions: IFitZoomTransformOptions = {
anchorX: isHorizontal ? (reversed ? 'end' : 'start') : 'center',
anchorY: isVertical ? (reversed ? 'end' : 'start') : 'center',
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to get these anchor variables from the recenter itself? We could have defaults for each layout, but user can override it through the argument of this function, e.g.

recenter({ anchorX, anchorY }, onRendered?);
recenter(onRendered?);
...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could also save these defaults for layouts in layout settings too. I find it odd to have these checks here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was skeptical about these checks too - it gets complicated because it depends both on the layout type and the layout options (it's different for horizontal/vertical tree, for example). I've added them to the layout settings, but I also extracted the logic for the default anchoring depending on the current layout; I believe this is the cleanest solution for now

@AlexIchenskiy
AlexIchenskiy merged commit 572be3f into release/1.0.0 Mar 5, 2026
2 checks passed
@AlexIchenskiy
AlexIchenskiy deleted the fix/change-layout-engine-logic branch March 5, 2026 09:47
tonilastre added a commit that referenced this pull request Jul 28, 2026
… tooling

* Fix: Fix multiple maps issue (#33)

* Chore: Update package.json version

* New: Change the API to handle OrbView and OrbMapView (#34)

* New: Change the API to handle OrbView and OrbMapView

* New: Change the API for select/hover strategies

* Chore: Release/1.0.0

* New: Add support to get selected/hovered nodes and edges (#61)

* New: Added support to get selected nodes and edges

* New: Added support to get hovered nodes and edges

---------

Co-authored-by: Abhinav Singh Parmar <abhinavparmar147@gmail.com>

* New: Add support for enabling and disabling dragging of nodes (fixes #62) (#69)

* New: Add feature to enable/disable node dragging (fixes #62)

* New: Added support to modify interaction from setSettings

* New: Updated documentation for interaction property

* New: Add feature to enable/disable zoom (fixes #62)

* NEW: Updated documentation for interaction property

* NEW: Updated documentation to include isDragEnabled

* New: Add support for custom edge line style (#77)

* New Added support for custom edges

* Refactor: Streamline edge rendering code and optimize line style handling

* New: Add support for handling device pixel ratio (#45)

* Chore: Move container and canvas creation from the view to the renderer

* New: Add devicePixelRatio render property and handler

* Fix: Add default DPR for older browsers

* Chore: Remove useless check for automatic DPR

* New: Add new simulator (#56) (#57)

* New: Add new simulator (#56)

* New: Add simulator scenarios for manual testing
* New: Refactor simulator (WIP)
* New: Add progress overlay, Update descriptions
* Fix: Introduce new simulator event, Fix main-thread behavior
* Fix: Rearrange class methods based on visibility
* Fix: Improve naming
* Chore(release): 0.2.0
* Fix: Tweak simulator, adjust API slightly
* Fix: Temporarily patch some physics behavior
* Fix: Adjust re-heating parameters
* Fix: tweak physics behavior -> immediately stop sim when disabling
* Chore: Remove the beta from release branches

---------

Co-authored-by: dlozic <davidlozic@gmail.com>

* New: Add zoom and recenter functions (#74)

* New: Add zoom and recenter functions

* Fix: Reduce excessive recentering (#75)

* Fix: Remove excessive recenterings
* Fix: Remove unused code

* Fix: New simulator (#92)

* Chore: Refactor naming

* New: Add new events

* Chore: Refactor code styling

* Docs: Remove unused flags

* Chore: Remove unused simulation functions

* Chore: Refactor view render function calls

* Chore: Add missing tests

* New: Add removal functions (#96)

* New: Add removal functions

* Fix: Add missing callback data

* Chore: Refactor remove return values

* Chore: Refactor remove function type usage

* Fix: Default settings for node placement (#98)

* New: Add properties setters and getters (#93)

* New: Add node properties setters

* New: Add edge properties setters

* New: Add properties getters

* New: Add patch for nodes and edges

* Fix: Make getters return copies

* Fix: Edge factory listeners copying

* Fix: Jest outdated tests

* Fix: Github actions node version

* Chore: Refactor observer interface

* Chore: Refactor node/edge constructor settings

* Chore: Refactor node/edge function grouping

* Chore: Refactor node/edge function grouping

* Fix: Listeners behaviour

* Chore: Refactor property copying

* Chore: Refactor subject implementation

* Fix: Set position behaviour on node drag

* Chore: Upgrade node version

* Chore: Refactor function type check

* Fix: Remove listener behaviour

* Chore: Refactor util naming

* Chore: Remove unused type assertion

* Chore: Refactor position setter options

* Chore: Refactor property patch function

* Fix: Set map node position behaviour

* Chore: Refactor simulator data patching

* Chore: Change observers to callbacks

* New: Add state setters with options (#95)

* New: Add state setters with options

* Chore: Remove leftover comments

* Chore: Refactor state setter logic

* Chore: Refactor state types

* Fix: Rename merged function usage

* Fix: Merged variable naming

* Chore: Fix tests

---------

Co-authored-by: dlozic <davidlozic@gmail.com>
Co-authored-by: Oleksandr Ichenskyi <55350107+AlexIchenskiy@users.noreply.github.com>
Co-authored-by: AlexIchenskiy <aichenskiy@gmail.com>

* New: Add zoom in and out functions (#100)

* Fix: Skip unnecessary listener notify on set style

* Fix: remove unnecessary rerender on state change

* Chore: Update documentation

* Fix: Node/edge getter performance issue

* Chore: Add data change docs example

* Fix: Docs typos

* Fix: Disable source map generation (#105)

* New: Add tree layout (#107)

* New: Add new layouts

* New: Add layout options

* Chore: Make layout dynamically changeable

* Chore: Update docs

* Fix: Naming typo

* Chore: Refactor layouts

* New: Enable layout node add/remove

* Chore: Improve behavior for recurrent nodes

* Chore: Move some simulator settings to layout

* Chore: Refactor code quality and performance

* Fix: Layout behavior on change

* Chore: Add recenter on layout change

* Fix: Layout change behavior

* Fix: Simulation behavior on data deletion

* Chore: Add recenter on layout change

* Fix: Change layout engine logic (#108)

* Fix: Change layout engine logic

* New: Add simulation cancellation logic

* Chore: Remove leftover code

* Fix: Hierarchical layout recenter logic

* Chore: Refactor package versions and code logic

* Fix: Refactor function types

* New: Add multiselect (#110)

* New: Add multiselect

* Chore: Simplify logic

* Chore: Update package.json

* New: Add SVG export (#111)

* New: Add SVG export

* Chore: Refactor code quality

* New: Add WebGL renderer (#109)

* New: Add WebGL renderer

* New: Add naive WebGL force layout computation

* New: Add WebGL improved node/shape geometry options

* New: Add labels and node images

* Fix: GPU drag behavior

* Chore: Remove leftover comments

* Fix: WebGL renderer style invalidating

* Chore: Refactor code quality

* Chore: Add WebGL example and docs

* New: Add docs page (#112)

* New: Add docs page

* New: Add more docs examples

* Update .github/workflows/docs.yml

---------

Co-authored-by: Toni <toni.lastre@memgraph.io>

* Chore: Finish up the release process

* Chore: Fix sync between package.json files

* Chore: Add new package-lock.json

---------

Co-authored-by: David <davidlozic@gmail.com>
Co-authored-by: Abhinv Singh Parmar <abhi171b010@gmail.com>
Co-authored-by: Abhinav Singh Parmar <abhinavparmar147@gmail.com>
Co-authored-by: Abhinv Singh Parmar <abhinav.parmar@infosys.com>
Co-authored-by: Oleksandr Ichenskyi <55350107+AlexIchenskiy@users.noreply.github.com>
Co-authored-by: AlexIchenskiy <aichenskiy@gmail.com>
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.

2 participants