Vašek Šraier
4 years ago
13 changed files with 217 additions and 252 deletions
@ -1,18 +1,15 @@ |
|||||
<script lang="ts"> |
<script lang="ts"> |
||||
import type { SimulationLinkDatum } from "d3"; |
import type { TaskEdge } from "./graph"; |
||||
import type { TaskId } from "./graph-types"; |
|
||||
|
|
||||
export let edge: SimulationLinkDatum<TaskId>; |
export let edge: TaskEdge; |
||||
export let showLabelEdge: boolean = false; |
export let showLabelEdge: boolean = false; |
||||
|
|
||||
$: x1 = edge?.source?.x ?? 0; |
$: [x1, y1] = edge?.dependency?.position ?? [0,0]; |
||||
$: y1 = edge?.source?.y ?? 0; |
$: [x2, y2] = edge?.dependee?.position ?? [0, 0]; |
||||
$: x2 = edge?.target?.x ?? 0; |
|
||||
$: y2 = edge?.target?.y ?? 0; |
|
||||
$: dx = x1 - x2 |
$: dx = x1 - x2 |
||||
$: dy = y1 - y2 |
$: dy = y1 - y2 |
||||
</script> |
</script> |
||||
|
|
||||
{#if showLabelEdge || (edge?.target?.task?.type ?? null) != "label"} |
{#if showLabelEdge || (edge?.dependee?.type ?? null) != "label"} |
||||
<path d="m {x2} {y2+0} c 0 0 {dx} {dy-40} {dx} {dy-20}" style="fill:none; stroke: #aaa; stroke-width: 3px" /> |
<path d="m {x2} {y2+0} c 0 0 {dx} {dy-40} {dx} {dy-20}" style="fill:none; stroke: #aaa; stroke-width: 3px" /> |
||||
{/if} |
{/if} |
||||
|
@ -0,0 +1,106 @@ |
|||||
|
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<string, TaskId> { |
||||
|
let nodeMap = new Map<string, TaskId>(); |
||||
|
for (let task of nodes) { |
||||
|
if (task.id in nodeMap) |
||||
|
throw 'duplicate IDs'; |
||||
|
nodeMap.set(task.id, task); |
||||
|
} |
||||
|
return nodeMap; |
||||
|
} |
||||
|
|
||||
|
function taskForce(): d3.Force<TaskId, undefined> { |
||||
|
let myNodes: TaskId[] | null = null; |
||||
|
let deps: Map<string, number> = new Map(); |
||||
|
let idMap: Map<string, TaskId> = 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<TaskId, undefined> = 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<string, [number, number]>) => 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<TaskId>[] = 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(function (d) { |
||||
|
return 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); |
||||
|
} |
@ -1,64 +0,0 @@ |
|||||
import type { SimulationLinkDatum, SimulationNodeDatum } from "d3"; |
|
||||
import type { TaskDescriptor, TasksFile } from "./task-loader"; |
|
||||
import { createTaskMap } from "./task-loader"; |
|
||||
|
|
||||
|
|
||||
export type TaskId = { |
|
||||
id: string; |
|
||||
task: TaskDescriptor; |
|
||||
} & SimulationNodeDatum; |
|
||||
|
|
||||
function toMapById(nodes: TaskId[]): Map<string, TaskId> { |
|
||||
let nodeMap = new Map<string, TaskId>(); |
|
||||
for (let task of nodes) { |
|
||||
if (task.id in nodeMap) |
|
||||
throw 'duplicate IDs'; |
|
||||
nodeMap.set(task.id, task); |
|
||||
} |
|
||||
return nodeMap; |
|
||||
} |
|
||||
|
|
||||
function createNodes(tasks: TasksFile, old?: TaskId[]): TaskId[] { |
|
||||
let m = (old == undefined) ? new Map<string, TaskId>() : toMapById(old); |
|
||||
|
|
||||
let res: TaskId[] = tasks.tasks.map((t) => { |
|
||||
return { id: t.id, task: t }; |
|
||||
}); |
|
||||
|
|
||||
for (let t of res) { |
|
||||
if (tasks.positions.has(t.id)) { |
|
||||
[t.x, t.y] = tasks.positions.get(t.id)! |
|
||||
} |
|
||||
if (m.has(t.id)) { |
|
||||
Object.assign(t, m.get(t.id)) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return res; |
|
||||
} |
|
||||
|
|
||||
export function createNodesAndEdges(tasks: TasksFile, oldNodes?: TaskId[], oldEdges?: SimulationLinkDatum<TaskId>[]): [TaskId[], SimulationLinkDatum<TaskId>[]] { |
|
||||
let nodes = createNodes(tasks, oldNodes); |
|
||||
|
|
||||
// create mapping from ID to node
|
|
||||
let nodeMap = toMapById(nodes); |
|
||||
|
|
||||
let links: SimulationLinkDatum<TaskId>[] = []; |
|
||||
for (const task of tasks.tasks) { |
|
||||
const src = nodeMap.get(task.id)!; |
|
||||
for (const id of task.requires) { |
|
||||
const t = nodeMap.get(id); |
|
||||
|
|
||||
if (t === undefined) throw `missing task with id ${id}`; |
|
||||
|
|
||||
const l: SimulationLinkDatum<TaskId> = |
|
||||
{ |
|
||||
source: src, |
|
||||
target: t |
|
||||
}; |
|
||||
links.push(l); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return [nodes, links]; |
|
||||
} |
|
@ -1,50 +0,0 @@ |
|||||
import type { TaskId } from "./graph-types"; |
|
||||
|
|
||||
/* copied from graph-types.ts */ |
|
||||
function toMapById(nodes: TaskId[]): Map<string, TaskId> { |
|
||||
let nodeMap = new Map<string, TaskId>(); |
|
||||
for (let task of nodes) { |
|
||||
if (task.id in nodeMap) |
|
||||
throw 'duplicate IDs'; |
|
||||
nodeMap.set(task.id, task); |
|
||||
} |
|
||||
return nodeMap; |
|
||||
} |
|
||||
|
|
||||
export function taskForce(): d3.Force<TaskId, undefined> { |
|
||||
let myNodes: TaskId[] | null = null; |
|
||||
let deps: Map<string, number> = new Map(); |
|
||||
let idMap: Map<string, TaskId> = 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<TaskId, undefined> = 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; |
|
||||
} |
|
Reference in new issue