Hylograph.ForceEngine.Simulation
- Package
- purescript-hylograph-simulation
- Repository
- afcondon/purescript-hylograph-simulation
Force Engine Simulation
High-level API for running force simulations. This module provides a clean interface for:
- Creating and configuring simulations
- Running the animation loop
- Handling tick callbacks for rendering
Unlike D3's simulation, we have full control over:
- When forces are applied
- How alpha decays
- When to render
== IMPORTANT: Force Target Caching
D3's forceX/forceY forces cache their target values at initialization time. This is a significant gotcha when animating between different target positions.
The Problem:
If you update node.gridX and call reheat, the force will still use the
old cached values. The simulation appears to run but nodes won't move to
new positions.
Wrong approach:
updateGridXWithFn newXFn sim
reheat sim -- Nodes don't move!
Correct approach:
-- Use the helper that handles re-initialization:
updateGridXYAndReinit (Just newXFn) Nothing forceXHandle Nothing sim
-- Or manually re-initialize the force:
updateGridXWithFn newXFn sim
currentNodes <- getNodes sim
_ <- Core.initializeForce forceXHandle currentNodes
reheat sim
The updateGridXYAndReinit function is the recommended way to animate
force-directed transitions as it handles this caching issue automatically.
#SimulationNode Source
type SimulationNode :: Row Type -> Typetype SimulationNode r = Record (D3_ID + D3_XY + D3_VxyFxy + r)
A simulation node with all required fields for force simulation and transitions. Extends user data row with id, position (x/y), velocity (vx/vy), and fixed position (fx/fy).
Example:
type MyNode = SimulationNode (name :: String, group :: Int)
-- Expands to: { id :: Int, x, y, vx, vy, fx, fy, name :: String, group :: Int }
#D3_FocusXY Source
type D3_FocusXY :: Row Type -> Row Typetype D3_FocusXY row = (cluster :: Int, focusX :: Number, focusY :: Number | row)
#SwizzledLink Source
type SwizzledLink :: Row Type -> Row Type -> Typetype SwizzledLink nodeData r = { source :: SimulationNode nodeData, target :: SimulationNode nodeData | r }
Swizzled link where source/target are node object references After D3 processes links, indices become object references
#Simulation Source
type Simulation :: Row Type -> Row Type -> Typetype Simulation row linkRow = { alpha :: Ref Number, callbacks :: Maybe SimulationCallbacks, cancelAnimation :: Ref (Effect Unit), config :: SimConfig, forces :: Ref (Map String ForceHandle), links :: Ref (Array { source :: Int, target :: Int | linkRow }), nodes :: Ref (Array (SimulationNode row)), prevAlpha :: Ref Number, running :: Ref Boolean, tickCallback :: Ref (Effect Unit) }
A running simulation This is a mutable structure that holds the simulation state
#defaultConfig Source
defaultConfig :: SimConfigDefault simulation configuration
#create Source
create :: forall row linkRow. SimConfig -> Effect (Simulation row linkRow)Create a new simulation (without callbacks)
For the new callback-based API, use createWithCallbacks instead.
#createWithCallbacks Source
createWithCallbacks :: forall row linkRow. SimConfig -> SimulationCallbacks -> Effect (Simulation row linkRow)Create a new simulation with callback system
Example:
callbacks <- defaultCallbacks
onSimulationTick updateDOM callbacks
onSimulationStop (log "Simulation settled") callbacks
sim <- createWithCallbacks config callbacks
#setNodes Source
setNodes :: forall row linkRow. Array (SimulationNode row) -> Simulation row linkRow -> Effect UnitSet the nodes for the simulation This initializes nodes and re-initializes all forces
#addForce Source
addForce :: forall row linkRow. ForceSpec -> Simulation row linkRow -> Effect UnitAdd a force to the simulation
#addForceHandle Source
addForceHandle :: forall row linkRow. String -> ForceHandle -> Simulation row linkRow -> Effect UnitAdd a pre-initialized force handle to the simulation Use this when you've created and initialized a force handle manually (e.g., for dynamic forces not covered by ForceSpec)
#removeForce Source
removeForce :: forall row linkRow. String -> Simulation row linkRow -> Effect UnitRemove a force from the simulation
#clearForces Source
clearForces :: forall row linkRow. Simulation row linkRow -> Effect UnitClear all forces from the simulation Used when switching between force configurations (e.g., grid vs tree vs orbit)
#start Source
start :: forall row linkRow. Simulation row linkRow -> Effect UnitStart the simulation animation loop
#tick Source
tick :: forall row linkRow. Simulation row linkRow -> Effect NumberRun a single tick of the simulation Returns the new alpha value
#reheat Source
reheat :: forall row linkRow. Simulation row linkRow -> Effect UnitReheat the simulation (set alpha to 1)
#setCallbacks Source
setCallbacks :: forall row linkRow. SimulationCallbacks -> Simulation row linkRow -> Simulation row linkRowSet the callbacks for a simulation (new multi-callback API) Note: This replaces any callbacks passed to createWithCallbacks
#getCallbacks Source
getCallbacks :: forall row linkRow. Simulation row linkRow -> Maybe SimulationCallbacksGet the current callbacks (if any)
#isRunning Source
isRunning :: forall row linkRow. Simulation row linkRow -> Effect BooleanCheck if the simulation is running
#getAlpha Source
getAlpha :: forall row linkRow. Simulation row linkRow -> Effect NumberGet the current alpha value
#getNodes Source
getNodes :: forall row linkRow. Simulation row linkRow -> Effect (Array (SimulationNode row))Get the current nodes (with updated positions)
#getLinks Source
getLinks :: forall row linkRow. Simulation row linkRow -> Effect (Array { source :: Int, target :: Int | linkRow })Get the current links from the simulation Note: These are the raw links with source/target as Int IDs. For swizzled links (with node references), use swizzleLinksByIndex from Links module.
#PositionMap Source
type PositionMap = Object { x :: Number, y :: Number }Position map type (keyed by node id as string)
#updatePositionsInPlace Source
updatePositionsInPlace :: forall row linkRow. PositionMap -> Simulation row linkRow -> Effect UnitUpdate node positions in place from a position map Mutates the simulation's internal nodes (same objects bound to D3)
#interpolatePositionsInPlace Source
interpolatePositionsInPlace :: forall row linkRow. PositionMap -> PositionMap -> Number -> Simulation row linkRow -> Effect UnitInterpolate node positions in place between start and target Progress should be 0.0 to 1.0 (apply easing before calling)
#pinNodesInPlace Source
pinNodesInPlace :: forall row linkRow. Simulation row linkRow -> Effect UnitPin all nodes at their current positions (fx = x, fy = y) Use before starting a transition to freeze current state
#unpinNodesInPlace Source
unpinNodesInPlace :: forall row linkRow. Simulation row linkRow -> Effect UnitUnpin all nodes (fx = null, fy = null) Use after transition to Grid scene to let forces take over
#pinNodesAtPositions Source
pinNodesAtPositions :: forall row linkRow. PositionMap -> Simulation row linkRow -> Effect UnitPin nodes at specific positions from a position map Sets both x/y and fx/fy - use at end of transition to non-Grid scenes
#updateGridPositionsInPlace Source
updateGridPositionsInPlace :: forall row linkRow. PositionMap -> Simulation row linkRow -> Effect UnitUpdate node gridX/gridY positions in place from a positions map Used to change force targets before reheating the simulation The positions map is keyed by node.id (as string) -> { x, y } where x becomes gridX and y becomes gridY
#updateGridXWithFn Source
updateGridXWithFn :: forall row linkRow. (SimulationNode row -> Number) -> Simulation row linkRow -> Effect UnitUpdate gridX for all nodes using a function Useful for toggle animations where only the X target changes Example: toggle between combined (all same X) and separated (X by department)
#updateGridYWithFn Source
updateGridYWithFn :: forall row linkRow. (SimulationNode row -> Number) -> Simulation row linkRow -> Effect UnitUpdate gridY for all nodes using a function Useful for toggle animations where only the Y target changes
#updateGridXYAndReinit Source
updateGridXYAndReinit :: forall row linkRow. Maybe (SimulationNode row -> Number) -> Maybe (SimulationNode row -> Number) -> ForceHandle -> Maybe ForceHandle -> Simulation row linkRow -> Effect UnitUpdate gridX and/or gridY for all nodes and re-initialize the forces
This is the recommended way to animate force-directed transitions. It handles the D3 force caching issue automatically by:
- Updating gridX/gridY values on nodes
- Re-initializing the ForceXGrid/ForceYGrid forces (so they pick up new targets)
- Reheating the simulation
IMPORTANT: D3's forceX/forceY cache target values at initialization time. Simply updating node.gridX/gridY won't make the force see new values. This function handles that by re-initializing the forces after updating.
Example:
-- Move nodes to new X positions (Y unchanged)
updateGridXYAndReinit
(Just (\node -> departmentX node.department))
Nothing
forceXHandle
Nothing
sim
-- Move nodes to new X and Y positions
updateGridXYAndReinit
(Just xFn)
(Just yFn)
forceXHandle
(Just forceYHandle)
sim
#attachDrag Source
attachDrag :: forall row linkRow. Array Element -> Simulation row linkRow -> Effect UnitAttach simulation-aware drag behavior to node elements
This sets up D3 drag handlers that:
- Reheat the simulation on drag start
- Update fx/fy (fixed position) during drag
- Clear fx/fy on drag end (release node)
Example:
elements <- select ".node" >>= selectAll
attachDrag elements sim
#attachGroupDrag Source
attachGroupDrag :: forall row linkRow. Array Element -> String -> Simulation row linkRow -> Effect UnitAttach drag to transformed group elements (like bubble packs)
Unlike attachDrag, this version takes a container selector to
get pointer coordinates in the correct coordinate space. Use this when
dragging <g> elements that have transform attributes.
Example:
-- For bubble packs inside a zoom group
packElements <- select ".module-pack" >>= selectAll
attachGroupDrag packElements "#zoom-group" sim
#attachPinningDrag Source
attachPinningDrag :: forall row linkRow. Array Element -> Simulation row linkRow -> Effect UnitAttach drag with toggle-pinning behavior
Unlike attachDrag, this version keeps nodes pinned after drag ends.
- First drag: pins the node where you drop it
- Subsequent drags: if you barely move (< 3px), unpins the node
This is useful for force playgrounds and exploration where users want to "fix" certain nodes in place while letting others float freely.
Example:
nodeElements <- select ".node" >>= selectAll
attachPinningDrag nodeElements sim
- Modules
- Hylograph.
Config. Apply - Hylograph.
Config. Force - Hylograph.
Config. Scene - Hylograph.
ForceEngine - Hylograph.
ForceEngine. Core - Hylograph.
ForceEngine. Demo - Hylograph.
ForceEngine. Events - Hylograph.
ForceEngine. Links - Hylograph.
ForceEngine. Registry - Hylograph.
ForceEngine. Render - Hylograph.
ForceEngine. Setup - Hylograph.
ForceEngine. Setup. WASM - Hylograph.
ForceEngine. Simulation - Hylograph.
ForceEngine. Types - Hylograph.
ForceEngine. WASM - Hylograph.
ForceEngine. WASMEngine - Hylograph.
Scene. Engine - Hylograph.
Scene. Handle - Hylograph.
Scene. Rules - Hylograph.
Scene. Types - Hylograph.
Simulation - Hylograph.
Simulation. Emitter - Hylograph.
Simulation. HATS - Hylograph.
Simulation. Scene - Hylograph.
Transition. Consumers - Hylograph.
Transition. Example