import type { SimulationLinkDatum, SimulationNodeDatum } from "d3"; import * as d3 from "d3"; import { createEdges, TaskDescriptor, TasksFile } from "./tasks"; type TaskId = { id: string; task: TaskDescriptor; } & SimulationNodeDatum; function toMapById(nodes: TaskId[]): Map { let nodeMap = new Map(); for (let task of nodes) { if (task.id in nodeMap) throw 'duplicate IDs'; nodeMap.set(task.id, task); } return nodeMap; } function taskForce(): d3.Force { let myNodes: TaskId[] | null = null; let deps: Map = new Map(); let idMap: Map = new Map(); function getNumberOfDeps(task: TaskId): number { if (deps.has(task.id)) return deps.get(task.id)!; if (task.task.requires.length == 0) return 0; let res = 0; for (let r of task.task.requires) { res += getNumberOfDeps(idMap.get(r)!) + 1; } deps.set(task.id, res); return res; } let force: d3.Force = function (alpha: number) { if (myNodes == null) throw 'nodes not initialized'; for (let task of myNodes) { if (task.vy == null) { task.vy = 0 } task.vy += getNumberOfDeps(task) * 25 * alpha; } } force.initialize = function (nodes: TaskId[]) { myNodes = nodes; idMap = toMapById(myNodes); } return force; } /** * * @param nodes * @param edges * @param ticked function that gets run every tick of a running simulation */ export function forceSimulation(tasks: TasksFile, ticked: (positions: Map) => void, repulsionForce?: number) { repulsionForce = repulsionForce ?? -1000; let nodes: TaskId[] = tasks.tasks.map( (t) => { return { id: t.id, task: t, x: (t.position ?? [0, 0])[0], y: (t.position ?? [0, 0])[1] } }) let edges: SimulationLinkDatum[] = createEdges(tasks.tasks).map((e) => { return { // FIXME are we sure its not the other way round? source: nodes.find((n) => n.id == e.dependee.id)!, target: nodes.find((n) => n.id == e.dependency.id)! } }); function tickHandler() { ticked(new Map(nodes.map((n) => [n.id!, [n.x!, n.y!]]))) } // Let's list the force we wanna apply on the network let simulation = d3 .forceSimulation(nodes) // Force algorithm is applied to data.nodes .force( "link", d3 .forceLink>() // This force provides links between nodes .id(d => d.id) // This provide the id of a node .links(edges) // and this the list of links ) .force("charge", d3.forceManyBody().strength(repulsionForce)) // This adds repulsion between nodes. Play with the -400 for the repulsion strength .force("x", d3.forceX()) // attracts elements to the zero X coord .force("y", d3.forceY().strength(0.5)) // attracts elements to the zero Y coord .force("dependencies", taskForce()) .on("tick", tickHandler) .on("end", tickHandler); }