From f8edfa09f0c762e4726d26b1d88f9c3bd9e24d52 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Tue, 17 Oct 2023 15:08:51 -0500 Subject: [PATCH 1/9] Have the container service add itself to the parent like the others --- src/services/containerService.ts | 8 ++++++++ src/services/nodeFlowRunService.ts | 4 +--- src/services/nodeTaskRunService.ts | 4 +--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/services/containerService.ts b/src/services/containerService.ts index 2b372295..dcd1eb90 100644 --- a/src/services/containerService.ts +++ b/src/services/containerService.ts @@ -1,9 +1,17 @@ import { Container } from 'pixi.js' import { Pixels } from '@/models/layout' +type ContainerServiceParameters = { + parent: Container, +} + export class ContainerService { public container = new Container() + public constructor(parameters: ContainerServiceParameters) { + parameters.parent.addChild(this.container) + } + public get position(): Pixels { return this.container.position } diff --git a/src/services/nodeFlowRunService.ts b/src/services/nodeFlowRunService.ts index 2be32aa0..cd5b50b2 100644 --- a/src/services/nodeFlowRunService.ts +++ b/src/services/nodeFlowRunService.ts @@ -20,7 +20,7 @@ export class NodeFlowRunService extends ContainerService implements NodeRenderSe private readonly label: NodeLabelService public constructor(parameters: NodeTaskRunServiceParameters) { - super() + super(parameters) this.positionService = parameters.positionService this.container.name = DEFAULT_NODE_CONTAINER_NAME @@ -34,8 +34,6 @@ export class NodeFlowRunService extends ContainerService implements NodeRenderSe this.label = new NodeLabelService({ parent: this.container, }) - - parameters.parent.addChild(this.container) } public async render(node: RunGraphNode): Promise { diff --git a/src/services/nodeTaskRunService.ts b/src/services/nodeTaskRunService.ts index 1fce5562..b75a5810 100644 --- a/src/services/nodeTaskRunService.ts +++ b/src/services/nodeTaskRunService.ts @@ -20,7 +20,7 @@ export class NodeTaskRunService extends ContainerService implements NodeRenderSe private readonly label: NodeLabelService public constructor(parameters: NodeTaskRunServiceParameters) { - super() + super(parameters) this.positionService = parameters.positionService this.container.eventMode = 'none' @@ -35,8 +35,6 @@ export class NodeTaskRunService extends ContainerService implements NodeRenderSe this.label = new NodeLabelService({ parent: this.container, }) - - parameters.parent.addChild(this.container) } public async render(node: RunGraphNode): Promise { From 55307632c0ffc3d62905270b5eb22653bb6d6c63 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Tue, 17 Oct 2023 15:31:36 -0500 Subject: [PATCH 2/9] Fix -infinity bug --- src/services/nodeOffsetService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/nodeOffsetService.ts b/src/services/nodeOffsetService.ts index 438ded7b..f773b0a7 100644 --- a/src/services/nodeOffsetService.ts +++ b/src/services/nodeOffsetService.ts @@ -22,7 +22,7 @@ export class NodeOffsetService { return 0 } - return Math.max(...values.values()) + return Math.max(...values.values(), 0) } public getTotalOffset(axis: number): number { From 75cd4ac9702e69e378314b2882cf19a2c39ee1f3 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Tue, 17 Oct 2023 15:37:11 -0500 Subject: [PATCH 3/9] Use the container service for the node container --- src/services/nodesContainerService.ts | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/services/nodesContainerService.ts b/src/services/nodesContainerService.ts index f8b24b82..2d5fffdb 100644 --- a/src/services/nodesContainerService.ts +++ b/src/services/nodesContainerService.ts @@ -5,6 +5,7 @@ import { RunGraphNode, RunGraphNodes } from '@/models/RunGraph' import { waitForConfig } from '@/objects/config' import { layout } from '@/objects/layout' import { centerViewport } from '@/objects/viewport' +import { ContainerService } from '@/services/containerService' import { NodeContainerService } from '@/services/nodeContainerService' import { NodePositionService } from '@/services/nodePositionService' import { exhaustive } from '@/utilities/exhaustive' @@ -15,37 +16,32 @@ type NodeParameters = { parent: Container, } -export class NodesContainerService { - public container = new Container() - +export class NodesContainerService extends ContainerService { private readonly runId: string private readonly worker = layoutWorkerFactory(this.onLayoutWorkerMessage.bind(this)) - private readonly position = new NodePositionService() + private readonly positionService = new NodePositionService() private readonly nodes = new Map() public constructor(parameters: NodeParameters) { + super(parameters) + this.runId = parameters.runId - this.initialize(parameters.parent) + this.container.name = DEFAULT_NODES_CONTAINER_NAME this.fetch() } - private initialize(parent: Container): void { - this.container.name = DEFAULT_NODES_CONTAINER_NAME - parent.addChild(this.container) - } - private async fetch(): Promise { const config = await waitForConfig() const data = await config.fetch(this.runId) - this.position.setHorizontalMode({ + this.positionService.setHorizontalMode({ mode: layout.horizontal, startTime: data.start_time, }) - this.position.setVerticalMode({ + this.positionService.setVerticalMode({ mode: layout.vertical, rowHeight: config.styles.nodeHeight, }) @@ -86,7 +82,7 @@ export class NodesContainerService { const service = this.nodes.get(node.id) ?? new NodeContainerService({ kind: node.kind, parent: this.container, - positionService: this.position, + positionService: this.positionService, }) this.nodes.set(node.id, service) @@ -104,7 +100,7 @@ export class NodesContainerService { } const { x } = layout - const y = this.position.getPixelsFromYPosition(layout.y) + const y = this.positionService.getPixelsFromYPosition(layout.y) objects.node.position = { x, y } objects.node.visible = true }) @@ -113,7 +109,6 @@ export class NodesContainerService { centerViewport() } - private onLayoutWorkerMessage({ data }: MessageEvent): void { const { type } = data From 50b2c37309de2a6d46b6a4245cd387003140a791 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Tue, 17 Oct 2023 16:01:10 -0500 Subject: [PATCH 4/9] Allow the nodes container to emit when the layout has been rendered --- src/objects/nodes.ts | 9 ++++++++- src/services/nodesContainerService.ts | 12 +++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/objects/nodes.ts b/src/objects/nodes.ts index b92c16dd..72d5dfb6 100644 --- a/src/objects/nodes.ts +++ b/src/objects/nodes.ts @@ -1,5 +1,5 @@ import { waitForConfig } from '@/objects/config' -import { waitForViewport } from '@/objects/viewport' +import { centerViewport, waitForViewport } from '@/objects/viewport' import { NodesContainerService } from '@/services/nodesContainerService' let service: NodesContainerService | null = null @@ -12,6 +12,13 @@ export async function startNodes(): Promise { runId: config.runId, parent: viewport, }) + + const center = (): void => { + centerViewport() + service?.emitter.off('rendered', center) + } + + service.emitter.on('rendered', center) } export function stopNodes(): void { diff --git a/src/services/nodesContainerService.ts b/src/services/nodesContainerService.ts index 2d5fffdb..ff508d96 100644 --- a/src/services/nodesContainerService.ts +++ b/src/services/nodesContainerService.ts @@ -1,10 +1,10 @@ +import mitt from 'mitt' import { Container } from 'pixi.js' import { DEFAULT_NODES_CONTAINER_NAME, DEFAULT_POLL_INTERVAL } from '@/consts' import { GraphPostLayout, GraphPreLayout, NodePreLayout } from '@/models/layout' import { RunGraphNode, RunGraphNodes } from '@/models/RunGraph' import { waitForConfig } from '@/objects/config' import { layout } from '@/objects/layout' -import { centerViewport } from '@/objects/viewport' import { ContainerService } from '@/services/containerService' import { NodeContainerService } from '@/services/nodeContainerService' import { NodePositionService } from '@/services/nodePositionService' @@ -16,6 +16,13 @@ type NodeParameters = { parent: Container, } +type NodesContainerEvents = { + rendered: void, +} + + +export class NodesContainerService extends ContainerService { + public readonly emitter = mitt() export class NodesContainerService extends ContainerService { private readonly runId: string private readonly worker = layoutWorkerFactory(this.onLayoutWorkerMessage.bind(this)) @@ -105,8 +112,7 @@ export class NodesContainerService extends ContainerService { objects.node.visible = true }) - // this should only happen on the first layout - centerViewport() + this.emitter.emit('rendered') } private onLayoutWorkerMessage({ data }: MessageEvent): void { From 347814e2e29edf761bd91826100ceede99916f21 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Tue, 17 Oct 2023 16:05:00 -0500 Subject: [PATCH 5/9] Let the nodes object handle centering the viewport once everything is loaded --- src/objects/nodes.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/objects/nodes.ts b/src/objects/nodes.ts index 72d5dfb6..69318c03 100644 --- a/src/objects/nodes.ts +++ b/src/objects/nodes.ts @@ -1,3 +1,4 @@ +import { Ticker } from 'pixi.js' import { waitForConfig } from '@/objects/config' import { centerViewport, waitForViewport } from '@/objects/viewport' import { NodesContainerService } from '@/services/nodesContainerService' @@ -13,9 +14,24 @@ export async function startNodes(): Promise { parent: viewport, }) + service.container.alpha = 0 + const center = (): void => { + if (!service) { + return + } + centerViewport() - service?.emitter.off('rendered', center) + + Ticker.shared.addOnce(() => { + if (!service) { + return + } + + service.container.alpha = 1 + }) + + service.emitter.off('rendered', center) } service.emitter.on('rendered', center) From 22c003f2931ef30bb2febabb936da238e37d15ae Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Tue, 17 Oct 2023 16:19:38 -0500 Subject: [PATCH 6/9] Clean up nodes object a bit --- src/objects/events.ts | 2 ++ src/objects/nodes.ts | 37 ++++++++++++++++++++----------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/objects/events.ts b/src/objects/events.ts index 2bf453b4..e07e572e 100644 --- a/src/objects/events.ts +++ b/src/objects/events.ts @@ -7,6 +7,7 @@ import { RequiredGraphConfig } from '@/models/RunGraph' import { ViewportDateRange } from '@/models/viewport' import { Fonts } from '@/objects/fonts' import { NodePositionService } from '@/services/nodePositionService' +import { NodesContainerService } from '@/services/nodesContainerService' type Events = { scalesCreated: NodePositionService, @@ -23,6 +24,7 @@ type Events = { fontsLoaded: Fonts, containerCreated: Container, layoutUpdated: LayoutMode, + nodesCreated: NodesContainerService, } export type EventKey = keyof Events diff --git a/src/objects/nodes.ts b/src/objects/nodes.ts index 69318c03..564e971e 100644 --- a/src/objects/nodes.ts +++ b/src/objects/nodes.ts @@ -1,5 +1,6 @@ import { Ticker } from 'pixi.js' import { waitForConfig } from '@/objects/config' +import { waitForEvent } from '@/objects/events' import { centerViewport, waitForViewport } from '@/objects/viewport' import { NodesContainerService } from '@/services/nodesContainerService' @@ -16,27 +17,29 @@ export async function startNodes(): Promise { service.container.alpha = 0 - const center = (): void => { - if (!service) { - return - } - - centerViewport() - - Ticker.shared.addOnce(() => { - if (!service) { - return - } + service.emitter.on('rendered', center) +} - service.container.alpha = 1 - }) +export function stopNodes(): void { + service = null +} - service.emitter.off('rendered', center) +export async function waitForNodes(): Promise { + if (service) { + return service } - service.emitter.on('rendered', center) + return await waitForEvent('nodesCreated') } -export function stopNodes(): void { - service = null +async function center(): Promise { + const service = await waitForNodes() + + centerViewport() + + Ticker.shared.addOnce(() => { + service.container.alpha = 1 + }) + + service.emitter.off('rendered', center) } \ No newline at end of file From 5a88eb74e38e268e89f4509d07e69c142ffb4712 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Wed, 18 Oct 2023 09:50:10 -0500 Subject: [PATCH 7/9] Convert classes to factories --- src/factories/box.ts | 34 ++++++ src/factories/flowRun.ts | 56 ++++++++++ src/factories/label.ts | 22 ++++ src/factories/node.ts | 59 +++++++++++ src/factories/nodes.ts | 126 +++++++++++++++++++++++ src/factories/offsets.ts | 62 +++++++++++ src/factories/position.ts | 39 +++++++ src/factories/settings.ts | 13 +++ src/factories/taskRun.ts | 55 ++++++++++ src/models/layout.ts | 56 +++------- src/objects/nodes.ts | 11 +- src/services/containerService.ts | 35 ------- src/services/nodeBoxService.ts | 39 ------- src/services/nodeContainerService.ts | 83 --------------- src/services/nodeFlowRunService.ts | 68 ------------ src/services/nodeLabelService.ts | 40 -------- src/services/nodeOffsetService.ts | 53 ---------- src/services/nodePositionService.ts | 142 -------------------------- src/services/nodeTaskRunService.ts | 69 ------------- src/services/nodesContainerService.ts | 129 ----------------------- src/workers/runGraph.ts | 8 +- src/workers/runGraph.worker.ts | 17 +-- 22 files changed, 498 insertions(+), 718 deletions(-) create mode 100644 src/factories/box.ts create mode 100644 src/factories/flowRun.ts create mode 100644 src/factories/label.ts create mode 100644 src/factories/node.ts create mode 100644 src/factories/nodes.ts create mode 100644 src/factories/offsets.ts create mode 100644 src/factories/position.ts create mode 100644 src/factories/settings.ts create mode 100644 src/factories/taskRun.ts delete mode 100644 src/services/containerService.ts delete mode 100644 src/services/nodeBoxService.ts delete mode 100644 src/services/nodeContainerService.ts delete mode 100644 src/services/nodeFlowRunService.ts delete mode 100644 src/services/nodeLabelService.ts delete mode 100644 src/services/nodeOffsetService.ts delete mode 100644 src/services/nodePositionService.ts delete mode 100644 src/services/nodeTaskRunService.ts delete mode 100644 src/services/nodesContainerService.ts diff --git a/src/factories/box.ts b/src/factories/box.ts new file mode 100644 index 00000000..4f5498e4 --- /dev/null +++ b/src/factories/box.ts @@ -0,0 +1,34 @@ +import { differenceInMilliseconds, millisecondsInSecond } from 'date-fns' +import { Graphics } from 'pixi.js' +import { DEFAULT_TIME_COLUMN_SIZE_PIXELS } from '@/consts' +import { RunGraphNode } from '@/models/RunGraph' +import { waitForConfig } from '@/objects/config' + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export async function nodeBoxFactory() { + const config = await waitForConfig() + const box = new Graphics() + + async function render(node: RunGraphNode): Promise { + const { background } = config.styles.node(node) + + const right = node.start_time + const left = node.end_time ?? new Date() + const seconds = differenceInMilliseconds(left, right) / millisecondsInSecond + const boxWidth = seconds * DEFAULT_TIME_COLUMN_SIZE_PIXELS + const boxHeight = config.styles.nodeHeight - config.styles.nodeMargin * 2 + + box.clear() + box.lineStyle(1, 0x0, 1, 2) + box.beginFill(background) + box.drawRoundedRect(0, 0, boxWidth, boxHeight, 4) + box.endFill() + + return await box + } + + return { + box, + render, + } +} \ No newline at end of file diff --git a/src/factories/flowRun.ts b/src/factories/flowRun.ts new file mode 100644 index 00000000..77814a48 --- /dev/null +++ b/src/factories/flowRun.ts @@ -0,0 +1,56 @@ +import { BitmapText, Container, Graphics } from 'pixi.js' +import { DEFAULT_NODE_CONTAINER_NAME } from '@/consts' +import { nodeBoxFactory } from '@/factories/box' +import { nodeLabelFactory } from '@/factories/label' +import { Pixels } from '@/models/layout' +import { RunGraphNode } from '@/models/RunGraph' +import { waitForConfig } from '@/objects/config' + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export async function flowRunContainerFactory() { + const container = new Container() + const { label, render: renderLabel } = await nodeLabelFactory() + const { box, render: renderBox } = await nodeBoxFactory() + + container.addChild(box) + container.addChild(label) + + container.name = DEFAULT_NODE_CONTAINER_NAME + container.eventMode = 'static' + container.cursor = 'pointer' + + async function render(node: RunGraphNode): Promise { + const label = await renderLabel(node) + const box = await renderBox(node) + + label.position = await getLabelPosition(label, box) + + return container + } + + async function getLabelPosition(label: BitmapText, box: Graphics): Promise { + const config = await waitForConfig() + + // todo: this should probably be nodePadding + const margin = config.styles.nodeMargin + const inside = box.width > margin + label.width + margin + const y = box.height / 2 - label.height + + if (inside) { + return { + x: margin, + y, + } + } + + return { + x: box.width + margin, + y, + } + } + + return { + container, + render, + } +} \ No newline at end of file diff --git a/src/factories/label.ts b/src/factories/label.ts new file mode 100644 index 00000000..def7d817 --- /dev/null +++ b/src/factories/label.ts @@ -0,0 +1,22 @@ +import { BitmapText } from 'pixi.js' +import { RunGraphNode } from '@/models/RunGraph' +import { waitForFonts } from '@/objects/fonts' + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export async function nodeLabelFactory() { + const { inter } = await waitForFonts() + const label = inter('', { + fontSize: 12, + }) + + async function render(node: RunGraphNode): Promise { + label.text = node.label + + return await label + } + + return { + label, + render, + } +} \ No newline at end of file diff --git a/src/factories/node.ts b/src/factories/node.ts new file mode 100644 index 00000000..f54ce155 --- /dev/null +++ b/src/factories/node.ts @@ -0,0 +1,59 @@ +import { Container, Ticker } from 'pixi.js' +import { flowRunContainerFactory } from '@/factories/flowRun' +import { taskRunContainerFactory } from '@/factories/taskRun' +import { RunGraphNode } from '@/models/RunGraph' + +export type NodeContainerFactory = Awaited> + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export async function nodeContainerFactory(node: RunGraphNode) { + const { container, render: renderNode } = await getNodeFactory(node) + const cacheKey: string | null = null + + async function render(node: RunGraphNode): Promise { + const currentCacheKey = getNodeCacheKey(node) + + if (currentCacheKey === cacheKey) { + return container + } + + await renderNode(node) + + if (!node.end_time) { + Ticker.shared.addOnce(() => render(node)) + } + + return container + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + async function getNodeFactory(node: RunGraphNode) { + const { kind } = node + + switch (kind) { + case 'task-run': + return await taskRunContainerFactory() + case 'flow-run': + return await flowRunContainerFactory() + default: + const exhaustive: never = kind + throw new Error(`switch does not have case for value: ${exhaustive}`) + } + } + + function getNodeCacheKey(node: RunGraphNode): string { + const keys = Object.keys(node).sort((keyA, keyB) => keyA.localeCompare(keyB)) as (keyof RunGraphNode)[] + const values = keys.map(key => { + const value = node[key] ?? new Date() + + return value.toString() + }) + + return values.join(',') + } + + return { + render, + container, + } +} \ No newline at end of file diff --git a/src/factories/nodes.ts b/src/factories/nodes.ts new file mode 100644 index 00000000..ecb6fe8a --- /dev/null +++ b/src/factories/nodes.ts @@ -0,0 +1,126 @@ +import { Container } from 'pixi.js' +import { DEFAULT_NODES_CONTAINER_NAME, DEFAULT_POLL_INTERVAL } from '@/consts' +import { NodeContainerFactory, nodeContainerFactory } from '@/factories/node' +import { offsetsFactory } from '@/factories/offsets' +import { HorizontalPositionSettings } from '@/factories/position' +import { horizontalSettingsFactory } from '@/factories/settings' +import { NodeLayoutRequest, NodeLayoutResponse, Pixels } from '@/models/layout' +import { RunGraphNode, RunGraphNodes } from '@/models/RunGraph' +import { waitForConfig } from '@/objects/config' +import { exhaustive } from '@/utilities/exhaustive' +import { WorkerLayoutMessage, WorkerMessage, layoutWorkerFactory } from '@/workers/runGraph' + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export async function nodesContainerFactory(runId: string) { + const worker = layoutWorkerFactory(onmessage) + const nodes = new Map() + const container = new Container() + const config = await waitForConfig() + const offsets = offsetsFactory() + + let settings: HorizontalPositionSettings + let layout: NodeLayoutResponse = new Map() + + container.name = DEFAULT_NODES_CONTAINER_NAME + + fetch() + + async function fetch(): Promise { + const data = await config.fetch(runId) + + settings = horizontalSettingsFactory(data.start_time) + + await render(data.nodes) + + if (!data.end_time) { + setTimeout(() => fetch(), DEFAULT_POLL_INTERVAL) + } + } + + async function render(nodes: RunGraphNodes): Promise { + const request: NodeLayoutRequest = new Map() + + for (const [nodeId, node] of nodes) { + // eslint-disable-next-line no-await-in-loop + const { width } = await renderNode(node) + + request.set(nodeId, { + node, + width, + }) + } + + worker.postMessage({ + type: 'layout', + nodes: request, + settings, + }) + } + + async function renderNode(node: RunGraphNode): Promise { + const { render } = await getNodeContainerService(node) + + return await render(node) + } + + function updateLayout(): void { + layout.forEach((position, nodeId) => { + const node = nodes.get(nodeId) + + if (!node) { + console.warn(`Count not find ${nodeId} from layout in nodes`) + return + } + + node.container.position = getActualPosition(position) + }) + } + + async function getNodeContainerService(node: RunGraphNode): Promise { + const existing = nodes.get(node.id) + + if (existing) { + return existing + } + + const response = await nodeContainerFactory(node) + + nodes.set(node.id, response) + container.addChild(response.container) + + return response + } + + function getActualPosition(position: Pixels): Pixels { + const y = offsets.getTotalOffset(position.y) + position.y * config.styles.nodeHeight + const { x } = position + + return { + x, + y, + } + } + + function onmessage({ data }: MessageEvent): void { + const { type } = data + + switch (type) { + case 'layout': + handleLayoutMessage(data) + return + default: + exhaustive(type) + } + } + + function handleLayoutMessage(data: WorkerLayoutMessage): void { + // eslint-disable-next-line prefer-destructuring + layout = data.layout + + updateLayout() + } + + return { + container, + } +} \ No newline at end of file diff --git a/src/factories/offsets.ts b/src/factories/offsets.ts new file mode 100644 index 00000000..d825c5f5 --- /dev/null +++ b/src/factories/offsets.ts @@ -0,0 +1,62 @@ +// Map> +type Offsets = Map | undefined> + +type SetOffsetParameters = { + axis: number, + nodeId: string, + offset: number, +} + +type RemoveOffsetParameters = { + axis: number, + nodeId: string, +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function offsetsFactory() { + const offsets: Offsets = new Map() + + function getOffset(axis: number): number { + const values = offsets.get(axis) + + if (!values) { + return 0 + } + + return Math.max(...values.values(), 0) + } + + function getTotalOffset(axis: number): number { + let value = 0 + + for (let index = 1; index <= axis; index++) { + value += getOffset(index) + } + + return value + } + + function setOffset({ axis, nodeId, offset }: SetOffsetParameters): void { + const value = offsets.get(axis) ?? new Map() + + value.set(nodeId, offset) + + offsets.set(axis, value) + } + + function removeOffset({ axis, nodeId }: RemoveOffsetParameters): void { + offsets.get(axis)?.delete(nodeId) + } + + function clear(): void { + offsets.clear() + } + + return { + getOffset, + getTotalOffset, + setOffset, + removeOffset, + clear, + } +} \ No newline at end of file diff --git a/src/factories/position.ts b/src/factories/position.ts new file mode 100644 index 00000000..475bf5aa --- /dev/null +++ b/src/factories/position.ts @@ -0,0 +1,39 @@ +import { scaleLinear, scaleTime } from 'd3' +import { addSeconds } from 'date-fns' +import { HorizontalMode } from '@/models/layout' + +export type HorizontalPositionSettings = { + mode: HorizontalMode, + startTime: Date, + timeSpan: number, + timeSpanPixels: number, + dagColumnSize: number, +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function horizontalScaleFactory(settings: HorizontalPositionSettings) { + if (settings.mode === 'time') { + return getTimeScale(settings) + } + + return getLinearScale(settings) +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function getTimeScale({ startTime, timeSpan, timeSpanPixels }: HorizontalPositionSettings) { + const start = startTime + const end = addSeconds(start, timeSpan) + + // example: pixelsRange = 20, start = "2023-01-01T00:00:00" + // scale("2023-01-01T00:00:00") = 0 + // scale("2023-01-01T00:00:01") = 20 + // scale("2023-01-01T00:00:05") = 100 + const scale = scaleTime().domain([start, end]).range([0, timeSpanPixels]) + + return scale +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function getLinearScale({ dagColumnSize }: HorizontalPositionSettings) { + return scaleLinear().domain([0, 1]).range([0, dagColumnSize]) +} \ No newline at end of file diff --git a/src/factories/settings.ts b/src/factories/settings.ts new file mode 100644 index 00000000..3b52dbef --- /dev/null +++ b/src/factories/settings.ts @@ -0,0 +1,13 @@ +import { DEFAULT_LINEAR_COLUMN_SIZE_PIXELS, DEFAULT_TIME_COLUMN_SIZE_PIXELS, DEFAULT_TIME_COLUMN_SPAN_SECONDS } from '@/consts' +import { HorizontalPositionSettings } from '@/factories/position' +import { layout } from '@/objects/layout' + +export function horizontalSettingsFactory(startTime: Date): HorizontalPositionSettings { + return { + mode: layout.horizontal, + startTime, + timeSpan: DEFAULT_TIME_COLUMN_SPAN_SECONDS, + timeSpanPixels: DEFAULT_TIME_COLUMN_SIZE_PIXELS, + dagColumnSize: DEFAULT_LINEAR_COLUMN_SIZE_PIXELS, + } +} \ No newline at end of file diff --git a/src/factories/taskRun.ts b/src/factories/taskRun.ts new file mode 100644 index 00000000..70f67fa4 --- /dev/null +++ b/src/factories/taskRun.ts @@ -0,0 +1,55 @@ +import { BitmapText, Container, Graphics } from 'pixi.js' +import { DEFAULT_NODE_CONTAINER_NAME } from '@/consts' +import { nodeBoxFactory } from '@/factories/box' +import { nodeLabelFactory } from '@/factories/label' +import { Pixels } from '@/models/layout' +import { RunGraphNode } from '@/models/RunGraph' +import { waitForConfig } from '@/objects/config' + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export async function taskRunContainerFactory() { + const container = new Container() + const { label, render: renderLabel } = await nodeLabelFactory() + const { box, render: renderBox } = await nodeBoxFactory() + + container.addChild(box) + container.addChild(label) + + container.eventMode = 'none' + container.name = DEFAULT_NODE_CONTAINER_NAME + + async function render(node: RunGraphNode): Promise { + const label = await renderLabel(node) + const box = await renderBox(node) + + label.position = await getLabelPosition(label, box) + + return container + } + + async function getLabelPosition(label: BitmapText, box: Graphics): Promise { + const config = await waitForConfig() + + // todo: this should probably be nodePadding + const margin = config.styles.nodeMargin + const inside = box.width > margin + label.width + margin + const y = box.height / 2 - label.height + + if (inside) { + return { + x: margin, + y, + } + } + + return { + x: box.width + margin, + y, + } + } + + return { + render, + container, + } +} \ No newline at end of file diff --git a/src/models/layout.ts b/src/models/layout.ts index f7c7a827..eaa80921 100644 --- a/src/models/layout.ts +++ b/src/models/layout.ts @@ -1,51 +1,19 @@ -import { RunGraphEdge } from '@/models/RunGraph' +import { RunGraphNode } from '@/models/RunGraph' -export type Position = { x: number | Date, y: number } export type Pixels = { x: number, y: number } - -export type SetVerticalModeWaterfallParameters = { - mode: 'waterfall', - rowHeight: number, -} - -export type SetVerticalModeNearestParentParameters = { - mode: 'nearest-parent', - rowHeight: number, -} - -export type SetVerticalModeParameters = SetVerticalModeWaterfallParameters | SetVerticalModeNearestParentParameters - -export type VerticalMode = SetVerticalModeParameters['mode'] - -export type SetHorizontalModeParameters = SetHorizontalModeTimeParameters | SetHorizontalModeLinearParameters - -export type SetHorizontalModeTimeParameters = { - mode: 'time', - startTime: Date, -} - -export type SetHorizontalModeLinearParameters = { - mode: 'dag', +export type VerticalMode = 'waterfall' | 'nearest-parent' +export type HorizontalMode = 'time' | 'dat' +export type LayoutMode = { + horizontal: HorizontalMode, + vertical: VerticalMode, } -export type HorizontalMode = SetHorizontalModeParameters['mode'] - -export type NodePreLayout = { - x: number, - parents: RunGraphEdge[], - children: RunGraphEdge[], +export type NodeLayoutRequest = Map +}> -export type NodePostLayout = NodePreLayout & { +export type NodeLayoutResponse = Map - -export type LayoutMode = { - horizontal: HorizontalMode, - vertical: VerticalMode, -} \ No newline at end of file +}> \ No newline at end of file diff --git a/src/objects/nodes.ts b/src/objects/nodes.ts index 564e971e..c7948986 100644 --- a/src/objects/nodes.ts +++ b/src/objects/nodes.ts @@ -1,4 +1,5 @@ import { Ticker } from 'pixi.js' +import { nodesContainerFactory } from '@/factories/nodes' import { waitForConfig } from '@/objects/config' import { waitForEvent } from '@/objects/events' import { centerViewport, waitForViewport } from '@/objects/viewport' @@ -9,15 +10,13 @@ let service: NodesContainerService | null = null export async function startNodes(): Promise { const viewport = await waitForViewport() const config = await waitForConfig() + const { container } = await nodesContainerFactory(config.runId) - service = new NodesContainerService({ - runId: config.runId, - parent: viewport, - }) + viewport.addChild(container) - service.container.alpha = 0 + // container.alpha = 0 - service.emitter.on('rendered', center) + // service.emitter.on('rendered', center) } export function stopNodes(): void { diff --git a/src/services/containerService.ts b/src/services/containerService.ts deleted file mode 100644 index dcd1eb90..00000000 --- a/src/services/containerService.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Container } from 'pixi.js' -import { Pixels } from '@/models/layout' - -type ContainerServiceParameters = { - parent: Container, -} - -export class ContainerService { - public container = new Container() - - public constructor(parameters: ContainerServiceParameters) { - parameters.parent.addChild(this.container) - } - - public get position(): Pixels { - return this.container.position - } - - public set position(value: Pixels) { - this.container.position = value - } - - public get visible(): boolean { - return this.container.visible - } - - public set visible(value: boolean) { - this.container.visible = value - } - - public get width(): number { - return this.container.width - } - -} \ No newline at end of file diff --git a/src/services/nodeBoxService.ts b/src/services/nodeBoxService.ts deleted file mode 100644 index c088105e..00000000 --- a/src/services/nodeBoxService.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Container, Graphics } from 'pixi.js' -import { RunGraphNode } from '@/models/RunGraph' -import { waitForConfig } from '@/objects/config' -import { NodePositionService } from '@/services/nodePositionService' - -type NodeBoxServiceParameters = { - parent: Container, - positionService: NodePositionService, -} - -export class NodeBoxService { - private readonly box = new Graphics() - private readonly positionService: NodePositionService - - public constructor(parameters: NodeBoxServiceParameters) { - this.positionService = parameters.positionService - - parameters.parent.addChild(this.box) - } - - public async render(node: RunGraphNode): Promise { - const config = await waitForConfig() - const { background } = config.styles.node(node) - - const { start_time, end_time } = node - const boxLeft = this.positionService.getPixelsFromXPosition(start_time) - const boxRight = this.positionService.getPixelsFromXPosition(end_time ?? new Date()) - const boxWidth = boxRight - boxLeft - const boxHeight = config.styles.nodeHeight - config.styles.nodeMargin * 2 - - this.box.clear() - this.box.lineStyle(1, 0x0, 1, 2) - this.box.beginFill(background) - this.box.drawRoundedRect(0, 0, boxWidth, boxHeight, 4) - this.box.endFill() - - return this.box - } -} \ No newline at end of file diff --git a/src/services/nodeContainerService.ts b/src/services/nodeContainerService.ts deleted file mode 100644 index c527e678..00000000 --- a/src/services/nodeContainerService.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Container, Ticker } from 'pixi.js' -import { NodePreLayout } from '@/models/layout' -import { RunGraphNode, RunGraphNodeKind } from '@/models/RunGraph' -import { ContainerService } from '@/services/containerService' -import { NodeFlowRunService } from '@/services/nodeFlowRunService' -import { NodePositionService } from '@/services/nodePositionService' -import { NodeTaskRunService } from '@/services/nodeTaskRunService' - -type NodeParameters = { - positionService: NodePositionService, - kind: RunGraphNodeKind, - parent: Container, -} - -export type NodeRenderService = ContainerService & { - render: (node: RunGraphNode) => Promise, -} - -export class NodeContainerService { - public readonly node: NodeRenderService - private readonly key: string | undefined - private readonly positionService: NodePositionService - - public constructor(parameters: NodeParameters) { - this.positionService = parameters.positionService - - this.node = this.getNodeService(parameters) - } - - public getLayout(node: RunGraphNode): NodePreLayout { - const { parents, children, start_time } = node - const x = this.positionService.getPixelsFromXPosition(start_time) - const { width } = this.node.container - - return { - x, - width, - parents, - children, - } - } - - public async render(node: RunGraphNode): Promise { - const key = this.getNodeCacheKey(node) - - if (key === this.key) { - return this.node.container - } - - const container = await this.node.render(node) - - if (!node.end_time) { - Ticker.shared.addOnce(() => this.render(node)) - } - - return container - } - - private getNodeService(parameters: NodeParameters): NodeRenderService { - const { kind } = parameters - - switch (kind) { - case 'task-run': - return new NodeTaskRunService(parameters) - case 'flow-run': - return new NodeFlowRunService(parameters) - default: - const exhaustive: never = kind - throw new Error(`switch does not have case for value: ${exhaustive}`) - } - } - - private getNodeCacheKey(node: RunGraphNode): string { - const keys = Object.keys(node).sort((keyA, keyB) => keyA.localeCompare(keyB)) as (keyof RunGraphNode)[] - const values = keys.map(key => { - const value = node[key] ?? new Date() - - return value.toString() - }) - - return values.join(',') - } -} \ No newline at end of file diff --git a/src/services/nodeFlowRunService.ts b/src/services/nodeFlowRunService.ts deleted file mode 100644 index cd5b50b2..00000000 --- a/src/services/nodeFlowRunService.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { BitmapText, Container, Graphics } from 'pixi.js' -import { DEFAULT_NODE_CONTAINER_NAME } from '@/consts' -import { Pixels } from '@/models/layout' -import { RunGraphNode } from '@/models/RunGraph' -import { waitForConfig } from '@/objects/config' -import { ContainerService } from '@/services/containerService' -import { NodeBoxService } from '@/services/nodeBoxService' -import { NodeRenderService } from '@/services/nodeContainerService' -import { NodeLabelService } from '@/services/nodeLabelService' -import { NodePositionService } from '@/services/nodePositionService' - -type NodeTaskRunServiceParameters = { - positionService: NodePositionService, - parent: Container, -} - -export class NodeFlowRunService extends ContainerService implements NodeRenderService { - private readonly positionService: NodePositionService - private readonly box: NodeBoxService - private readonly label: NodeLabelService - - public constructor(parameters: NodeTaskRunServiceParameters) { - super(parameters) - - this.positionService = parameters.positionService - this.container.name = DEFAULT_NODE_CONTAINER_NAME - this.container.visible = false - - this.box = new NodeBoxService({ - parent: this.container, - positionService: this.positionService, - }) - - this.label = new NodeLabelService({ - parent: this.container, - }) - } - - public async render(node: RunGraphNode): Promise { - const box = await this.box.render(node) - const label = await this.label.render({ ...node, label: `${node.label} - FLOW RUN` }) - - label.position = await this.getLabelPositionRelativeToBox(label, box) - - return this.container - } - - public async getLabelPositionRelativeToBox(label: BitmapText, box: Graphics): Promise { - const config = await waitForConfig() - - // todo: this should probably be nodePadding - const margin = config.styles.nodeMargin - const inside = box.width > margin + label.width + margin - const y = box.height / 2 - label.height - - if (inside) { - return { - x: margin, - y, - } - } - - return { - x: box.width + margin, - y, - } - } -} \ No newline at end of file diff --git a/src/services/nodeLabelService.ts b/src/services/nodeLabelService.ts deleted file mode 100644 index 58b2571f..00000000 --- a/src/services/nodeLabelService.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { BitmapText, Container } from 'pixi.js' -import { RunGraphNode } from '..' -import { waitForFonts } from '@/objects/fonts' - -type LabelServiceParameters = { - parent: Container, -} - -export class NodeLabelService { - private label: BitmapText | undefined - private readonly parent: Container - - public constructor(parameters: LabelServiceParameters) { - this.parent = parameters.parent - } - - public async render(node: RunGraphNode): Promise { - const label = await this.getLabel(node) - - label.text = node.label - - return this.label! - } - - private async getLabel(node: RunGraphNode): Promise { - if (this.label) { - return this.label - } - - const { inter } = await waitForFonts() - - this.label = inter(node.label, { - fontSize: 12, - }) - - this.parent.addChild(this.label) - - return this.label - } -} \ No newline at end of file diff --git a/src/services/nodeOffsetService.ts b/src/services/nodeOffsetService.ts deleted file mode 100644 index f773b0a7..00000000 --- a/src/services/nodeOffsetService.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Map> -type Offsets = Map | undefined> - -type SetOffsetParameters = { - axis: number, - nodeId: string, - offset: number, -} - -type RemoveOffsetParameters = { - axis: number, - nodeId: string, -} - -export class NodeOffsetService { - private readonly offsets: Offsets = new Map() - - public getOffset(axis: number): number { - const values = this.offsets.get(axis) - - if (!values) { - return 0 - } - - return Math.max(...values.values(), 0) - } - - public getTotalOffset(axis: number): number { - let value = 0 - - for (let index = 1; index <= axis; index++) { - value += this.getOffset(index) - } - - return value - } - - public setOffset({ axis, nodeId, offset }: SetOffsetParameters): void { - const value = this.offsets.get(axis) ?? new Map() - - value.set(nodeId, offset) - - this.offsets.set(axis, value) - } - - public removeOffset({ axis, nodeId }: RemoveOffsetParameters): void { - this.offsets.get(axis)?.delete(nodeId) - } - - public clear(): void { - this.offsets.clear() - } -} \ No newline at end of file diff --git a/src/services/nodePositionService.ts b/src/services/nodePositionService.ts deleted file mode 100644 index 852e6221..00000000 --- a/src/services/nodePositionService.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { ScaleLinear, ScaleTime, scaleLinear, scaleTime } from 'd3' -import { addSeconds } from 'date-fns' -import { DEFAULT_LINEAR_COLUMN_SIZE_PIXELS, DEFAULT_TIME_COLUMN_SIZE_PIXELS, DEFAULT_TIME_COLUMN_SPAN_SECONDS } from '@/consts' -import { Pixels, Position, SetHorizontalModeParameters, SetHorizontalModeTimeParameters, SetVerticalModeParameters } from '@/models/layout' -import { NodeOffsetService } from '@/services/nodeOffsetService' -import { exhaustive } from '@/utilities/exhaustive' - -type LinearScale = ScaleLinear -type TimeScale = ScaleTime - -export class NodePositionService { - private yAxis: LinearScale | null = null - private xAxis: LinearScale | TimeScale | null = null - private readonly offsets = new NodeOffsetService() - - public setHorizontalMode(parameters: SetHorizontalModeParameters): void { - const { mode } = parameters - - switch (mode) { - case 'time': - this.setXScaleTime(parameters) - break - case 'dag': - this.setXScaleLinear() - break - default: - exhaustive(mode) - } - } - - public setVerticalMode(parameters: SetVerticalModeParameters): void { - const { mode } = parameters - - switch (mode) { - case 'waterfall': - case 'nearest-parent': - this.setYScaleLinear(parameters) - break - default: - exhaustive(mode) - } - } - - public setNodeOffset(...parameters: Parameters): void { - this.offsets.setOffset(...parameters) - } - - public removeNodeOffset(...parameters: Parameters): void { - this.offsets.removeOffset(...parameters) - } - - public getPixelsFromXPosition(value: Position['x']): Pixels['x'] { - if (!this.xAxis) { - throw new Error('Axis not initialized') - } - - return this.xAxis(value) - } - - public getPixelsFromYPosition(value: Position['y']): Pixels['x'] { - if (!this.yAxis) { - throw new Error('Axis not initialized') - } - - return this.yAxis(value) + this.offsets.getTotalOffset(value) - } - - public getPixelsFromPosition(position: Position): Pixels { - return { - y: this.getPixelsFromYPosition(position.y), - x: this.getPixelsFromXPosition(position.x), - } - } - - public getPositionFromXPixels(value: Pixels['x']): Position['x'] { - if (!this.xAxis) { - throw new Error('Axis not initialized') - } - - return this.xAxis.invert(value) - } - - public getPositionFromYPixels(value: number): number { - if (!this.yAxis) { - throw new Error('Axis not initialized') - } - - const maxValue = this.yAxis.invert(value) - const [, rowHeight] = this.yAxis.range() - // todo: this doesn't quite account for the offsets being "empty" space - const range = Array.from({ length: maxValue }, (_element, index): [number, number] => { - const startOffset = this.offsets.getTotalOffset(index) - const start = index * rowHeight + startOffset - - const endOffset = this.offsets.getOffset(index) - const end = start + rowHeight + endOffset - - return [start, end] - }).flat() - - const axis = scaleLinear().domain([0, maxValue]).range(range) - - return axis.invert(value) - } - - public getPositionFromPixels(pixels: Pixels): Position { - const x = this.getPositionFromXPixels(pixels.x) - const y = this.getPositionFromYPixels(pixels.y) - - return { - x, - y, - } - } - - private setXScaleTime({ startTime }: SetHorizontalModeTimeParameters): void { - const start = startTime - const end = addSeconds(start, DEFAULT_TIME_COLUMN_SPAN_SECONDS) - - // example: DEFAULT_COLUMN_SIZE_PIXELS = 20, start = "2023-01-01T00:00:00" - // scale("2023-01-01T00:00:00") = 0 - // scale("2023-01-01T00:00:01") = 20 - // scale("2023-01-01T00:00:05") = 100 - this.xAxis = scaleTime().domain([start, end]).range([0, DEFAULT_TIME_COLUMN_SIZE_PIXELS]) - } - - private setXScaleLinear(): void { - // example: DEFAULT_LINEAR_COLUMN_SIZE_PIXELS = 200 - // scale(0) === 0 - // scale(1) === 200 - // scale(5) === 1000 - this.yAxis = scaleLinear().domain([0, 1]).range([0, DEFAULT_LINEAR_COLUMN_SIZE_PIXELS]) - } - - private setYScaleLinear({ rowHeight }: { rowHeight: number }): void { - // example: rowHeight = 20 - // scale(0) === 0 - // scale(1) === 20 - // scale(5) === 100 - this.yAxis = scaleLinear().domain([0, 1]).range([0, rowHeight]) - } -} \ No newline at end of file diff --git a/src/services/nodeTaskRunService.ts b/src/services/nodeTaskRunService.ts deleted file mode 100644 index b75a5810..00000000 --- a/src/services/nodeTaskRunService.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { BitmapText, Container, Graphics } from 'pixi.js' -import { DEFAULT_NODE_CONTAINER_NAME } from '@/consts' -import { Pixels } from '@/models/layout' -import { RunGraphNode } from '@/models/RunGraph' -import { waitForConfig } from '@/objects/config' -import { ContainerService } from '@/services/containerService' -import { NodeBoxService } from '@/services/nodeBoxService' -import { NodeRenderService } from '@/services/nodeContainerService' -import { NodeLabelService } from '@/services/nodeLabelService' -import { NodePositionService } from '@/services/nodePositionService' - -type NodeTaskRunServiceParameters = { - positionService: NodePositionService, - parent: Container, -} - -export class NodeTaskRunService extends ContainerService implements NodeRenderService { - private readonly positionService: NodePositionService - private readonly box: NodeBoxService - private readonly label: NodeLabelService - - public constructor(parameters: NodeTaskRunServiceParameters) { - super(parameters) - - this.positionService = parameters.positionService - this.container.eventMode = 'none' - this.container.name = DEFAULT_NODE_CONTAINER_NAME - this.container.visible = false - - this.box = new NodeBoxService({ - parent: this.container, - positionService: this.positionService, - }) - - this.label = new NodeLabelService({ - parent: this.container, - }) - } - - public async render(node: RunGraphNode): Promise { - const box = await this.box.render(node) - const label = await this.label.render(node) - - label.position = await this.getLabelPositionRelativeToBox(label, box) - - return this.container - } - - public async getLabelPositionRelativeToBox(label: BitmapText, box: Graphics): Promise { - const config = await waitForConfig() - - // todo: this should probably be nodePadding - const margin = config.styles.nodeMargin - const inside = box.width > margin + label.width + margin - const y = box.height / 2 - label.height - - if (inside) { - return { - x: margin, - y, - } - } - - return { - x: box.width + margin, - y, - } - } -} \ No newline at end of file diff --git a/src/services/nodesContainerService.ts b/src/services/nodesContainerService.ts deleted file mode 100644 index ff508d96..00000000 --- a/src/services/nodesContainerService.ts +++ /dev/null @@ -1,129 +0,0 @@ -import mitt from 'mitt' -import { Container } from 'pixi.js' -import { DEFAULT_NODES_CONTAINER_NAME, DEFAULT_POLL_INTERVAL } from '@/consts' -import { GraphPostLayout, GraphPreLayout, NodePreLayout } from '@/models/layout' -import { RunGraphNode, RunGraphNodes } from '@/models/RunGraph' -import { waitForConfig } from '@/objects/config' -import { layout } from '@/objects/layout' -import { ContainerService } from '@/services/containerService' -import { NodeContainerService } from '@/services/nodeContainerService' -import { NodePositionService } from '@/services/nodePositionService' -import { exhaustive } from '@/utilities/exhaustive' -import { WorkerMessage, layoutWorkerFactory } from '@/workers/runGraph' - -type NodeParameters = { - runId: string, - parent: Container, -} - -type NodesContainerEvents = { - rendered: void, -} - - -export class NodesContainerService extends ContainerService { - public readonly emitter = mitt() -export class NodesContainerService extends ContainerService { - private readonly runId: string - private readonly worker = layoutWorkerFactory(this.onLayoutWorkerMessage.bind(this)) - private readonly positionService = new NodePositionService() - private readonly nodes = new Map() - - public constructor(parameters: NodeParameters) { - super(parameters) - - this.runId = parameters.runId - - this.container.name = DEFAULT_NODES_CONTAINER_NAME - - this.fetch() - } - - private async fetch(): Promise { - const config = await waitForConfig() - const data = await config.fetch(this.runId) - - this.positionService.setHorizontalMode({ - mode: layout.horizontal, - startTime: data.start_time, - }) - - this.positionService.setVerticalMode({ - mode: layout.vertical, - rowHeight: config.styles.nodeHeight, - }) - - await this.render(data.nodes) - - if (!data.end_time) { - setTimeout(() => this.fetch(), DEFAULT_POLL_INTERVAL) - } - } - - private async render(nodes: RunGraphNodes): Promise { - const layout: GraphPreLayout = new Map() - const promises: Promise[] = [] - - nodes.forEach(async node => { - const promise = this.renderNode(node) - - promises.push(promise) - - layout.set(node.id, await promise) - }) - - await Promise.all(promises) - - this.worker.postMessage({ type: 'layout', layout }) - } - - private async renderNode(node: RunGraphNode): Promise { - const service = this.getNodeContainerService(node) - - await service.render(node) - - return service.getLayout(node) - } - - private getNodeContainerService(node: RunGraphNode): NodeContainerService { - const service = this.nodes.get(node.id) ?? new NodeContainerService({ - kind: node.kind, - parent: this.container, - positionService: this.positionService, - }) - - this.nodes.set(node.id, service) - - return service - } - - private layout(layout: GraphPostLayout): void { - layout.forEach((layout, nodeId) => { - const objects = this.nodes.get(nodeId) - - if (!objects) { - console.warn(`Count not find ${nodeId} from layout in graph`) - return - } - - const { x } = layout - const y = this.positionService.getPixelsFromYPosition(layout.y) - objects.node.position = { x, y } - objects.node.visible = true - }) - - this.emitter.emit('rendered') - } - - private onLayoutWorkerMessage({ data }: MessageEvent): void { - const { type } = data - - switch (type) { - case 'layout': - this.layout(data.layout) - return - default: - exhaustive(type) - } - } -} \ No newline at end of file diff --git a/src/workers/runGraph.ts b/src/workers/runGraph.ts index 9869dec6..af3d1d11 100644 --- a/src/workers/runGraph.ts +++ b/src/workers/runGraph.ts @@ -1,4 +1,5 @@ -import { GraphPostLayout, GraphPreLayout } from '@/models/layout' +import { HorizontalPositionSettings } from '@/factories/position' +import { NodeLayoutRequest, NodeLayoutResponse } from '@/models/layout' // eslint-disable-next-line import/default import RunGraphWorker from '@/workers/runGraph.worker?worker' @@ -8,12 +9,13 @@ export type WorkerMessage = WorkerLayoutMessage export type ClientLayoutMessage = { type: 'layout', - layout: GraphPreLayout, + nodes: NodeLayoutRequest, + settings: HorizontalPositionSettings, } export type WorkerLayoutMessage = { type: 'layout', - layout: GraphPostLayout, + layout: NodeLayoutResponse, } export interface IRunGraphWorker extends Omit { diff --git a/src/workers/runGraph.worker.ts b/src/workers/runGraph.worker.ts index 969217dc..10be56d6 100644 --- a/src/workers/runGraph.worker.ts +++ b/src/workers/runGraph.worker.ts @@ -1,4 +1,5 @@ -import { GraphPostLayout } from '@/models/layout' +import { horizontalScaleFactory } from '@/factories/position' +import { NodeLayoutResponse } from '@/models/layout' import { exhaustive } from '@/utilities/exhaustive' import { WorkerMessage, ClientMessage, ClientLayoutMessage } from '@/workers/runGraph' @@ -20,19 +21,21 @@ function post(message: WorkerMessage): void { postMessage(message) } -function handleLayoutMessage({ layout: preLayout }: ClientLayoutMessage): void { +function handleLayoutMessage({ nodes, settings }: ClientLayoutMessage): void { let y = 0 - const postLayout: GraphPostLayout = new Map() + const scale = horizontalScaleFactory(settings) + const layout: NodeLayoutResponse = new Map() - preLayout.forEach((node, key) => { - postLayout.set(key, { - ...node, + nodes.forEach(({ node }, nodeId) => { + layout.set(nodeId, { + x: scale(node.start_time), y: y++, }) }) post({ type: 'layout', - layout: postLayout, + layout, }) } + From 8e7b7f6a4d0a1ec918b6f3db0c631d7697cdcfcf Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Wed, 18 Oct 2023 10:44:00 -0500 Subject: [PATCH 8/9] Get scales and events working --- src/factories/events.ts | 54 +++++++++++++++++++++++++++++++++++++++ src/factories/nodes.ts | 11 ++++++++ src/factories/position.ts | 2 ++ src/objects/events.ts | 10 ++++---- src/objects/index.ts | 7 +++-- src/objects/nodes.ts | 38 ++++++++++++++------------- src/objects/scale.ts | 29 +++++++++++++++++++++ src/objects/scales.ts | 38 --------------------------- src/objects/viewport.ts | 8 +++--- 9 files changed, 128 insertions(+), 69 deletions(-) create mode 100644 src/factories/events.ts create mode 100644 src/objects/scale.ts delete mode 100644 src/objects/scales.ts diff --git a/src/factories/events.ts b/src/factories/events.ts new file mode 100644 index 00000000..88f7b0ea --- /dev/null +++ b/src/factories/events.ts @@ -0,0 +1,54 @@ +// need to use any for function arguments +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Handler = (...payload: T[]) => void +type Events = Record + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function eventsFactory() { + type Event = keyof T + type Handlers = Set + const all = new Map() + + function on(event: E, handler: Handler): () => void { + const existing = all.get(event) + + if (existing) { + existing.add(handler) + } else { + all.set(event, new Set([handler])) + } + + return () => off(event, handler) + } + + function once(event: E, handler: Handler): void { + const callback: Handler = (args) => { + off(event, callback) + handler(args) + } + + on(event, callback) + } + + function off(event: E, handler: Handler): void { + all.get(event)?.delete(handler) + } + + function emit(event: undefined extends T[E] ? E : never): void + function emit(event: E, payload: T[E]): void + function emit(event: E, payload?: T[E]): void { + all.get(event)?.forEach(handler => handler(payload)) + } + + function clear(): void { + all.clear() + } + + return { + on, + off, + once, + emit, + clear, + } +} \ No newline at end of file diff --git a/src/factories/nodes.ts b/src/factories/nodes.ts index ecb6fe8a..5fa17559 100644 --- a/src/factories/nodes.ts +++ b/src/factories/nodes.ts @@ -1,5 +1,6 @@ import { Container } from 'pixi.js' import { DEFAULT_NODES_CONTAINER_NAME, DEFAULT_POLL_INTERVAL } from '@/consts' +import { eventsFactory } from '@/factories/events' import { NodeContainerFactory, nodeContainerFactory } from '@/factories/node' import { offsetsFactory } from '@/factories/offsets' import { HorizontalPositionSettings } from '@/factories/position' @@ -10,6 +11,12 @@ import { waitForConfig } from '@/objects/config' import { exhaustive } from '@/utilities/exhaustive' import { WorkerLayoutMessage, WorkerMessage, layoutWorkerFactory } from '@/workers/runGraph' +export type NodesContainer = Awaited> + +type Events = { + rendered: void, +} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export async function nodesContainerFactory(runId: string) { const worker = layoutWorkerFactory(onmessage) @@ -17,6 +24,7 @@ export async function nodesContainerFactory(runId: string) { const container = new Container() const config = await waitForConfig() const offsets = offsetsFactory() + const events = eventsFactory() let settings: HorizontalPositionSettings let layout: NodeLayoutResponse = new Map() @@ -74,6 +82,8 @@ export async function nodesContainerFactory(runId: string) { node.container.position = getActualPosition(position) }) + + events.emit('rendered') } async function getNodeContainerService(node: RunGraphNode): Promise { @@ -122,5 +132,6 @@ export async function nodesContainerFactory(runId: string) { return { container, + events, } } \ No newline at end of file diff --git a/src/factories/position.ts b/src/factories/position.ts index 475bf5aa..7ab545aa 100644 --- a/src/factories/position.ts +++ b/src/factories/position.ts @@ -10,6 +10,8 @@ export type HorizontalPositionSettings = { dagColumnSize: number, } +export type HorizontalScale = ReturnType + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function horizontalScaleFactory(settings: HorizontalPositionSettings) { if (settings.mode === 'time') { diff --git a/src/objects/events.ts b/src/objects/events.ts index e07e572e..d238ecad 100644 --- a/src/objects/events.ts +++ b/src/objects/events.ts @@ -2,16 +2,16 @@ import mitt from 'mitt' import { Viewport } from 'pixi-viewport' import { Application, Container } from 'pixi.js' import { EffectScope } from 'vue' +import { NodesContainer } from '@/factories/nodes' +import { HorizontalScale } from '@/factories/position' import { LayoutMode } from '@/models/layout' import { RequiredGraphConfig } from '@/models/RunGraph' import { ViewportDateRange } from '@/models/viewport' import { Fonts } from '@/objects/fonts' -import { NodePositionService } from '@/services/nodePositionService' -import { NodesContainerService } from '@/services/nodesContainerService' type Events = { - scalesCreated: NodePositionService, - scalesUpdated: NodePositionService, + scaleCreated: HorizontalScale, + scaleUpdated: HorizontalScale, applicationCreated: Application, applicationResized: Application, stageCreated: HTMLDivElement, @@ -24,7 +24,7 @@ type Events = { fontsLoaded: Fonts, containerCreated: Container, layoutUpdated: LayoutMode, - nodesCreated: NodesContainerService, + nodesCreated: NodesContainer, } export type EventKey = keyof Events diff --git a/src/objects/index.ts b/src/objects/index.ts index 8f29bd2f..b4f8c0d8 100644 --- a/src/objects/index.ts +++ b/src/objects/index.ts @@ -4,7 +4,7 @@ import { startConfig, stopConfig } from '@/objects/config' import { emitter } from '@/objects/events' import { startFonts, stopFonts } from '@/objects/fonts' import { startNodes, stopNodes } from '@/objects/nodes' -import { startScales, stopScales } from '@/objects/scales' +import { startScale, stopScale } from '@/objects/scale' import { startScope, stopScope } from '@/objects/scope' import { startStage, stopStage } from '@/objects/stage' import { startViewport, stopViewport } from '@/objects/viewport' @@ -12,7 +12,6 @@ import { startViewport, stopViewport } from '@/objects/viewport' export * from './application' export * from './stage' export * from './viewport' -export * from './scales' type StartParameters = { stage: HTMLDivElement, @@ -22,7 +21,7 @@ type StartParameters = { export function start({ stage, props }: StartParameters): void { startApplication() startViewport(props) - startScales() + startScale() startNodes() startScope() startFonts() @@ -35,7 +34,7 @@ export function stop(): void { stopApplication() stopViewport() - stopScales() + stopScale() stopStage() stopNodes() stopConfig() diff --git a/src/objects/nodes.ts b/src/objects/nodes.ts index c7948986..830fbe40 100644 --- a/src/objects/nodes.ts +++ b/src/objects/nodes.ts @@ -1,44 +1,46 @@ import { Ticker } from 'pixi.js' -import { nodesContainerFactory } from '@/factories/nodes' +import { NodesContainer, nodesContainerFactory } from '@/factories/nodes' import { waitForConfig } from '@/objects/config' -import { waitForEvent } from '@/objects/events' +import { emitter, waitForEvent } from '@/objects/events' import { centerViewport, waitForViewport } from '@/objects/viewport' -import { NodesContainerService } from '@/services/nodesContainerService' -let service: NodesContainerService | null = null +let nodes: NodesContainer | null = null export async function startNodes(): Promise { const viewport = await waitForViewport() const config = await waitForConfig() - const { container } = await nodesContainerFactory(config.runId) - viewport.addChild(container) + nodes = await nodesContainerFactory(config.runId) - // container.alpha = 0 + viewport.addChild(nodes.container) - // service.emitter.on('rendered', center) + nodes.container.alpha = 0 + + nodes.events.once('rendered', center) + + emitter.emit('nodesCreated', nodes) } export function stopNodes(): void { - service = null + nodes = null } -export async function waitForNodes(): Promise { - if (service) { - return service +export async function waitForNodes(): Promise { + if (nodes) { + return nodes } return await waitForEvent('nodesCreated') } -async function center(): Promise { - const service = await waitForNodes() - +function center(): void { centerViewport() Ticker.shared.addOnce(() => { - service.container.alpha = 1 - }) + if (!nodes) { + return + } - service.emitter.off('rendered', center) + nodes.container.alpha = 1 + }) } \ No newline at end of file diff --git a/src/objects/scale.ts b/src/objects/scale.ts new file mode 100644 index 00000000..9d028040 --- /dev/null +++ b/src/objects/scale.ts @@ -0,0 +1,29 @@ +import { HorizontalScale, horizontalScaleFactory } from '@/factories/position' +import { horizontalSettingsFactory } from '@/factories/settings' +import { waitForConfig } from '@/objects/config' +import { emitter, waitForEvent } from '@/objects/events' + +let scale: HorizontalScale | null = null + +export async function startScale(): Promise { + const config = await waitForConfig() + const data = await config.fetch(config.runId) + const settings = await horizontalSettingsFactory(data.start_time) + + scale = await horizontalScaleFactory(settings) + + emitter.emit('scaleCreated', scale) +} + +export function stopScale(): void { + scale = null +} + + +export async function waitForScale(): Promise { + if (scale) { + return scale + } + + return await waitForEvent('scaleCreated') +} \ No newline at end of file diff --git a/src/objects/scales.ts b/src/objects/scales.ts deleted file mode 100644 index 8ea0cc6d..00000000 --- a/src/objects/scales.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { waitForConfig } from '@/objects/config' -import { emitter, waitForEvent } from '@/objects/events' -import { layout } from '@/objects/layout' -import { NodePositionService } from '@/services/nodePositionService' - -let service: NodePositionService | null = null - -export async function startScales(): Promise { - const config = await waitForConfig() - const data = await config.fetch(config.runId) - - service = new NodePositionService() - - service.setHorizontalMode({ - mode: layout.horizontal, - startTime: data.start_time, - }) - - service.setVerticalMode({ - mode: layout.vertical, - rowHeight: config.styles.nodeHeight, - }) - - emitter.emit('scalesCreated', service) -} - -export function stopScales(): void { - service = null -} - - -export async function waitForScales(): Promise { - if (service) { - return service - } - - return await waitForEvent('scalesCreated') -} \ No newline at end of file diff --git a/src/objects/viewport.ts b/src/objects/viewport.ts index fdfbb0b5..5e72fdb2 100644 --- a/src/objects/viewport.ts +++ b/src/objects/viewport.ts @@ -7,7 +7,7 @@ import { ViewportDateRange } from '@/models/viewport' import { waitForApplication } from '@/objects/application' import { waitForConfig } from '@/objects/config' import { emitter, waitForEvent } from '@/objects/events' -import { waitForScales } from '@/objects/scales' +import { waitForScale } from '@/objects/scale' import { waitForScope } from '@/objects/scope' import { waitForStage } from '@/objects/stage' @@ -143,9 +143,9 @@ async function startViewportDateRange(): Promise { async function updateViewportDateRange(): Promise { const viewport = await waitForViewport() - const scales = await waitForScales() - const left = scales.getPositionFromXPixels(viewport.left) - const right = scales.getPositionFromXPixels(viewport.right) + const scale = await waitForScale() + const left = scale.invert(viewport.left) + const right = scale.invert(viewport.right) if (left instanceof Date && right instanceof Date) { setViewportDateRange([left, right]) From b156203c017c1843474cd6657fbb33b334799ca3 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Wed, 18 Oct 2023 10:45:40 -0500 Subject: [PATCH 9/9] Replace mitt with custom events factory --- package-lock.json | 11 ----------- package.json | 1 - src/objects/events.ts | 4 ++-- src/objects/index.ts | 2 +- 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85836a92..aa48c29f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "date-fns": "2.30.0", "lodash.isequal": "4.5.0", "lodash.merge": "4.6.2", - "mitt": "3.0.1", "pixi-viewport": "5.0.2", "pixi.js": "7.3.1" }, @@ -5179,11 +5178,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -10812,11 +10806,6 @@ "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", "dev": true }, - "mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 7f89f047..310261e4 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "date-fns": "2.30.0", "lodash.isequal": "4.5.0", "lodash.merge": "4.6.2", - "mitt": "3.0.1", "pixi-viewport": "5.0.2", "pixi.js": "7.3.1" } diff --git a/src/objects/events.ts b/src/objects/events.ts index d238ecad..615d5c46 100644 --- a/src/objects/events.ts +++ b/src/objects/events.ts @@ -1,7 +1,7 @@ -import mitt from 'mitt' import { Viewport } from 'pixi-viewport' import { Application, Container } from 'pixi.js' import { EffectScope } from 'vue' +import { eventsFactory } from '@/factories/events' import { NodesContainer } from '@/factories/nodes' import { HorizontalScale } from '@/factories/position' import { LayoutMode } from '@/models/layout' @@ -29,7 +29,7 @@ type Events = { export type EventKey = keyof Events -export const emitter = mitt() +export const emitter = eventsFactory() export function waitForEvent(event: T): Promise { // making ts happy with this is just not worth it IMO since this is diff --git a/src/objects/index.ts b/src/objects/index.ts index b4f8c0d8..103f8d09 100644 --- a/src/objects/index.ts +++ b/src/objects/index.ts @@ -30,7 +30,7 @@ export function start({ stage, props }: StartParameters): void { } export function stop(): void { - emitter.all.clear() + emitter.clear() stopApplication() stopViewport()