Module

Hylograph.Simulation

Package
purescript-hylograph-simulation
Repository
afcondon/purescript-hylograph-simulation

Hylograph Force Simulation

High-level API for running force-directed simulations. Supports both D3.js and WASM physics engines with the same interface.

Quick Start

import Hylograph.Simulation (runSimulation, Engine(..), SimulationEvent(..))
import Hylograph.Simulation.Emitter (subscribe)

main = do
  -- Run simulation - returns handle AND event emitter
  { handle, events } <- runSimulation
    { engine: D3  -- or WASM (same config!)
    , setup: setup "physics"
        [ manyBody "charge" # withStrength (static (-30.0))
        , center "center"
        ]
    , nodes: myNodes
    , links: []
    , container: "#my-svg"
    , alphaMin: 0.001
    }

  -- Subscribe to events (framework-agnostic)
  unsubscribe <- subscribe events \event -> case event of
    Tick { alpha } -> log $ "Alpha: " <> show alpha
    Completed -> log "Simulation converged!"
    _ -> pure unit

  -- Get nodes for rendering (use HATS or your own rendering)
  nodes <- handle.getNodes

  -- Update data (GUP semantics, auto-reheat)
  result <- handle.updateData newNodes newLinks

Framework Integration

Halogen: Use toHalogenEmitter from Hylograph.Simulation.Halogen React: Use subscribe in a useEffect hook with cleanup Vanilla: Just call subscribe directly

#runSimulation Source

runSimulation :: forall r. SimulationConfig r -> Effect (SimulationResult r)

Run a force simulation with Hylograph visualization.

Returns both a handle for controlling the simulation AND an event emitter for subscribing to simulation events. This design is framework-agnostic:

  • The emitter works with Halogen, React, or vanilla JS
  • Events include Tick, Started, Stopped, and Completed
  • The same code works for both D3 and WASM engines Note: No Ord constraint required! We use renderTreeKeyed with a key function based on node.id, which doesn't require Ord on the datum type. This allows SimulationNode r (an extensible record) to work without needing Ord derivation.

#SimulationResult Source

type SimulationResult :: Row Type -> Typetype SimulationResult r = { events :: SimulationEmitter, handle :: SimulationHandle r }

Result returned by runSimulation

  • handle: Control the simulation (update data, stop, start, etc.)
  • events: Subscribe to simulation events (Tick, Completed, etc.)

#SimulationConfig Source

type SimulationConfig :: Row Type -> Typetype SimulationConfig r = { alphaMin :: Number, container :: String, engine :: Engine, links :: Array { source :: Int, target :: Int }, nodes :: Array (SimulationNode r), setup :: Setup (SimulationNode r) }

Configuration for running a simulation

Note: There's no onComplete callback - use the events emitter instead. This keeps the API framework-agnostic.

Rendering is handled externally via the Tick event and handle.getNodes. Use HATS or any other rendering approach to render node positions on each tick.

#Engine Source

data Engine

Physics engine selection

Constructors

#SimulationHandle Source

type SimulationHandle :: Row Type -> Typetype SimulationHandle r = { getAlpha :: Effect Number, getNodes :: Effect (Array (SimulationNode r)), interpolatePositions :: PositionMap -> PositionMap -> Number -> Effect Unit, pinNodes :: Effect Unit, reheat :: Effect Unit, start :: Effect Unit, stop :: Effect Unit, unpinNodes :: Effect Unit, updateData :: Array (SimulationNode r) -> Array { source :: Int, target :: Int } -> Effect { links :: GUPLinkResult (), nodes :: GUPResult (SimulationNode r) }, updateSetup :: Setup (SimulationNode r) -> Effect Unit }

Handle for controlling the simulation

#PositionMap Source

type PositionMap = PositionMap

Position map for interpolation - keyed by node ID (as string)

#initWASM Source

initWASM :: String -> Aff Unit

Initialize the WASM physics engine. Call once at application startup before using Engine.WASM.

Example: initWASM "./pkg/force_kernel.js"

Re-exports from Hylograph.ForceEngine.Setup

#Setup Source

type Setup node = { forces :: Array (ForceConfig node), name :: String, params :: SetupParams }

Complete setup: forces + simulation params

#GUPResult Source

type GUPResult node = { entered :: Array node, exited :: Array node, updated :: Array node }

Result of a node data update with enter/update/exit categorization

  • entered: Nodes newly added to simulation (use for enter animations)
  • updated: Existing nodes that were modified (simulation state preserved)
  • exited: Nodes removed from simulation (use for exit animations)

#GUPLinkResult Source

type GUPLinkResult :: Row Type -> Typetype GUPLinkResult linkRow = { entered :: Array { source :: NodeID, target :: NodeID | linkRow }, exited :: Array { source :: NodeID, target :: NodeID | linkRow }, updated :: Array { source :: NodeID, target :: NodeID | linkRow } }

Result of a link data update with enter/update/exit categorization Links are keyed by (source, target) pair

#withY Source

withY :: forall node. Value node Number -> ForceConfig node -> ForceConfig node

Set Y position/target (for Center, PositionY, Radial)

#withX Source

withX :: forall node. Value node Number -> ForceConfig node -> ForceConfig node

Set X position/target (for Center, PositionX, Radial)

#withTheta Source

withTheta :: forall node. Number -> ForceConfig node -> ForceConfig node

Set theta (Barnes-Hut approximation, for ManyBody)

#withStrength Source

withStrength :: forall node. Value node Number -> ForceConfig node -> ForceConfig node

Set strength (works for all force types)

#withRadius Source

withRadius :: forall node. Value node Number -> ForceConfig node -> ForceConfig node

Set radius (for Collide, Radial)

#withIterations Source

withIterations :: forall node. Int -> ForceConfig node -> ForceConfig node

Set iterations (for Collide, Link)

#withFilter Source

withFilter :: forall node. (node -> Boolean) -> ForceConfig node -> ForceConfig node

Add a filter predicate (force only applies to matching nodes)

#withDistanceMin Source

withDistanceMin :: forall node. Number -> ForceConfig node -> ForceConfig node

Set distance min (for ManyBody)

#withDistanceMax Source

withDistanceMax :: forall node. Number -> ForceConfig node -> ForceConfig node

Set distance max (for ManyBody)

#withDistance Source

withDistance :: forall node. Value node Number -> ForceConfig node -> ForceConfig node

Set distance (for Link)

#static Source

static :: forall node a. a -> Value node a

Create a static value

#setup Source

setup :: forall node. String -> Array (ForceConfig node) -> Setup node

Create a setup with default params

#radial Source

radial :: forall node. String -> ForceConfig node

Create a radial force Default: radius 100, center (0,0), strength 0.1

#positionY Source

positionY :: forall node. String -> ForceConfig node

Create a Y-positioning force Default: y = 0, strength 0.1

#positionX Source

positionX :: forall node. String -> ForceConfig node

Create an X-positioning force Default: x = 0, strength 0.1

#manyBody Source

manyBody :: forall node. String -> ForceConfig node

Create a many-body (charge) force Default: repulsive with strength -30

#dynamic Source

dynamic :: forall node a. (node -> a) -> Value node a

Create a dynamic (per-node) value

#collide Source

collide :: forall node. String -> ForceConfig node

Create a collision force Default: radius 1, strength 1

#center Source

center :: forall node. String -> ForceConfig node

Create a centering force Default: center at (0, 0), strength 1

Re-exports from Hylograph.ForceEngine.Simulation

#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 }

Re-exports from Hylograph.Simulation.Emitter

#SimulationEvent Source

data SimulationEvent

Event types emitted by simulations

These are the same regardless of whether D3 or WASM is running the physics.

Constructors

Instances

#SimulationEmitter Source

newtype SimulationEmitter

Framework-agnostic event emitter

This is intentionally opaque - use subscribe to listen for events. The internal representation is a list of listeners with unique IDs.

#subscribe Source

subscribe :: SimulationEmitter -> (SimulationEvent -> Effect Unit) -> Effect Unsubscribe

Subscribe to events

Returns an unsubscribe function that removes the listener.

unsubscribe <- subscribe emitter \event -> case event of
  Tick { alpha } -> log $ "Alpha: " <> show alpha
  Completed -> log "Simulation converged!"
  _ -> pure unit

-- Later, to stop listening:
unsubscribe