Problem
dag-map renders SVG with data-node-id, data-id, data-edge-from, data-edge-to attributes on elements, which enables external event wiring via DOM queries:
svg.querySelectorAll('circle[data-id]').forEach(el => {
el.addEventListener('click', () => handleClick(el.dataset.id))
})
This works but has significant drawbacks:
-
Re-renders destroy listeners. Every call to renderSVG produces a new SVG string. When the host framework (Svelte, React) sets innerHTML, all manually-attached listeners are lost and must be re-bound.
-
No hover lifecycle. Implementing hover-highlight requires tracking mouseenter/mouseleave on <g> groups, coordinating with dimmed nodes, and cleaning up on re-render. Every consumer reimplements this.
-
Edge interaction is harder. Edge paths have data-edge-from/data-edge-to but edges are thin paths — hard to click without a transparent hit area.
-
Framework coupling. Each consumer writes its own binding layer. The pattern is identical each time.
Context
The FlowTime Svelte UI is moving to a "workbench" paradigm where the DAG topology is a navigation surface — users click nodes to pin them to an inspection panel. This makes node click the single most important interaction primitive. Currently handled by manual DOM wiring in dag-map-view.svelte, but fragile across re-renders when metrics change per time bin.
The dag-map ROADMAP lists "Click/tap events on stations" and "Hover tooltips" as planned. This issue proposes a concrete design.
Proposed Design
Option A: Callback options on renderSVG
Add optional callbacks to render options. Problem: renderSVG returns a string and cannot attach listeners. Would require changing the return type.
Option B: Return a bindEvents helper (recommended)
Keep renderSVG returning a string. Add a separate function:
import { renderSVG, bindEvents } from 'dag-map'
const svgString = renderSVG(dag, layout, options)
container.innerHTML = svgString
// Call after mounting. Returns a cleanup function.
const unbind = bindEvents(container, {
onNodeClick: (nodeId, event) => { ... },
onNodeEnter: (nodeId, event) => { ... },
onNodeLeave: (nodeId, event) => { ... },
onEdgeClick: (fromId, toId, event) => { ... },
})
bindEvents:
- Uses event delegation on the SVG root (single listener per event type, not per element)
- Adds transparent hit areas on edges for easier clicking
- Returns
unbind() for cleanup on re-render
- Can be tree-shaken — consumers who don't need interaction don't import it
Option C: Managed component via dagMap()
Extend dagMap() to accept a container and manage the full lifecycle with an update() method. More powerful but changes the library's character.
Recommendation
Option B is the best fit:
- Keeps
renderSVG as a pure string function (no DOM dependency)
- Works with any framework's mounting strategy
- Event delegation means O(1) listeners regardless of node count
unbind() makes cleanup explicit
- Tree-shakeable
Edge Hit Areas
Each interactive edge path should have an invisible companion:
<path d="..." stroke="transparent" stroke-width="12" pointer-events="stroke"
data-edge-from="A" data-edge-to="B"/>
<path d="..." stroke="#2B8A8E" stroke-width="3" pointer-events="none"
data-edge-from="A" data-edge-to="B"/>
Render hit areas when any onEdge* callback is present (or always — no visual impact).
Problem
dag-map renders SVG with
data-node-id,data-id,data-edge-from,data-edge-toattributes on elements, which enables external event wiring via DOM queries:This works but has significant drawbacks:
Re-renders destroy listeners. Every call to
renderSVGproduces a new SVG string. When the host framework (Svelte, React) setsinnerHTML, all manually-attached listeners are lost and must be re-bound.No hover lifecycle. Implementing hover-highlight requires tracking
mouseenter/mouseleaveon<g>groups, coordinating with dimmed nodes, and cleaning up on re-render. Every consumer reimplements this.Edge interaction is harder. Edge paths have
data-edge-from/data-edge-tobut edges are thin paths — hard to click without a transparent hit area.Framework coupling. Each consumer writes its own binding layer. The pattern is identical each time.
Context
The FlowTime Svelte UI is moving to a "workbench" paradigm where the DAG topology is a navigation surface — users click nodes to pin them to an inspection panel. This makes node click the single most important interaction primitive. Currently handled by manual DOM wiring in
dag-map-view.svelte, but fragile across re-renders when metrics change per time bin.The dag-map ROADMAP lists "Click/tap events on stations" and "Hover tooltips" as planned. This issue proposes a concrete design.
Proposed Design
Option A: Callback options on
renderSVGAdd optional callbacks to render options. Problem:
renderSVGreturns a string and cannot attach listeners. Would require changing the return type.Option B: Return a
bindEventshelper (recommended)Keep
renderSVGreturning a string. Add a separate function:bindEvents:unbind()for cleanup on re-renderOption C: Managed component via
dagMap()Extend
dagMap()to accept a container and manage the full lifecycle with anupdate()method. More powerful but changes the library's character.Recommendation
Option B is the best fit:
renderSVGas a pure string function (no DOM dependency)unbind()makes cleanup explicitEdge Hit Areas
Each interactive edge path should have an invisible companion:
Render hit areas when any
onEdge*callback is present (or always — no visual impact).