diff --git a/packages/flow-lineage/src/components/f-dag/add-group.ts b/packages/flow-lineage/src/components/f-dag/add-group.ts new file mode 100644 index 000000000..91b24d974 --- /dev/null +++ b/packages/flow-lineage/src/components/f-dag/add-group.ts @@ -0,0 +1,110 @@ +import { html } from "lit"; +import type { FDag } from "./f-dag"; +import type { FInput, FSelect, FTabNode } from "@ollion/flow-core"; + +export function addGroupPopover(this: FDag) { + return html` + e.stopPropagation()}> + + + + Exisiting + + + + + Create New + + + + + + g.id)} + > + + + + + + + + + + + + + + + + `; +} + +export function handleAddGroup(this: FDag) { + this.addGroupPopoverRef.open = true; +} + +export function addSelectionToGroup(this: FDag, groupid: string) { + this.selectedNodes.forEach(sn => { + sn.group = groupid; + }); + + this.addGroupPopoverRef.open = false; + + this.config.nodes.forEach(n => { + n.x = undefined; + n.y = undefined; + }); + this.config.groups.forEach(n => { + n.x = undefined; + n.y = undefined; + }); + + this.config.links.forEach(l => { + l.from.x = undefined; + l.from.y = undefined; + l.to.x = undefined; + l.to.y = undefined; + }); + + this.selectedNodes = []; + this.addGroupButton.style.display = "none"; + this.requestUpdate(); +} + +export function addToNewGroup(this: FDag) { + const groupIdInput = this.querySelector("#new-group-id")!; + const groupLabelInput = this.querySelector("#new-group-label")!; + + this.config.groups.push({ + id: groupIdInput.value as string, + label: groupLabelInput.value as string, + icon: "i-org" + }); + + this.addSelectionToGroup(groupIdInput.value as string); +} + +export function addToGroup(this: FDag) { + const groupDropdown = this.querySelector(`#f-group-dropdown`)!; + const groupid = groupDropdown.value as string; + + this.addSelectionToGroup(groupid); +} + +export function switchTab(this: FDag, event: PointerEvent) { + const tabNodeElement = event.currentTarget as FTabNode; + + this.groupSelectionTabs.forEach(tab => { + tab.active = false; + }); + tabNodeElement.active = true; +} diff --git a/packages/flow-lineage/src/components/f-dag/background-svg.ts b/packages/flow-lineage/src/components/f-dag/background-svg.ts new file mode 100644 index 000000000..364db0559 --- /dev/null +++ b/packages/flow-lineage/src/components/f-dag/background-svg.ts @@ -0,0 +1,22 @@ +import { html } from "lit"; + +export default function backgroundSVG() { + return html` + + + + + `; +} diff --git a/packages/flow-lineage/src/components/f-dag/compute-placement.ts b/packages/flow-lineage/src/components/f-dag/compute-placement.ts new file mode 100644 index 000000000..b49f4be9c --- /dev/null +++ b/packages/flow-lineage/src/components/f-dag/compute-placement.ts @@ -0,0 +1,242 @@ +import type { FDag } from "./f-dag"; +import buildHierarchy from "./hierarchy-builder"; +import { + CustomPlacementByElement, + CustomPlacementBySection, + FDagElement, + FDagGroup, + HierarchyNode +} from "./types"; + +export default function computePlacement(this: FDag) { + this.groupsHTML = []; + this.nodesHTML = []; + const { roots: rootNodes, customPlacements } = buildHierarchy(this.config); + + const positionNodes = ( + containerId: string, + elements: HierarchyNode[], + x: number, + y: number, + isCollapsed: boolean, + spaceX = 100, + spaceY = 100 + ) => { + const elementIds = elements.map(e => e.id); + const conatinerElementObject = this.getElement(containerId) as FDagGroup; + const layoutDirection = (() => { + if (containerId === "root") { + return this.config.layoutDirection; + } + if (conatinerElementObject.layoutDirection === "vertical") { + return "horizontal"; + } + return "vertical"; + })(); + const nodeLinks = this.config.links.filter( + l => elementIds.includes(l.from.elementId) && elementIds.includes(l.to.elementId) + ); + const roots = new Set(elements); + const nonroots = new Set(); + nodeLinks.forEach(link => { + const fromElement = elements.find(e => e.id === link.from.elementId)!; + if (!nonroots.has(fromElement)) { + roots.add(fromElement); + } + if (!fromElement.next) { + fromElement.next = []; + } + + const toElement = elements.find(e => e.id === link.to.elementId)!; + if (roots.has(toElement)) { + roots.delete(toElement); + } + nonroots.add(toElement); + fromElement.next.push(toElement); + }); + + const initialY = y; + const initialX = x; + let maxX = 0; + let maxY = 0; + const minX = x; + const minY = y; + let section = 0; + const calculateCords = (ns: HierarchyNode[]) => { + const nexts: HierarchyNode[] = []; + let maxWidth = this.defaultElementWidth; + let maxHeight = this.defaultElementHeight; + section += 1; + const nextSection = () => { + if (!isCollapsed) { + if (layoutDirection === "vertical") { + y += maxHeight + spaceY; + x = initialX; + } else { + x += maxWidth + spaceX; + y = initialY; + } + } + }; + + let currentNodeId: string | null; + const isElementPlacement = (elementObject: FDagElement) => + elementObject.placement && + (elementObject.placement as CustomPlacementByElement).elementId === currentNodeId; + const isSectionPlacement = (elementObject: FDagElement) => + elementObject.placement && + (elementObject.placement as CustomPlacementBySection).section === section && + containerId === "root"; + const placeElement = (n: HierarchyNode) => { + const elementObject = this.getElement(n.id); + if ( + !elementObject.placement || + isSectionPlacement(elementObject) || + isElementPlacement(elementObject) + ) { + const customPlacementsByElements = this.getCustomPlacementElementsByElementId( + elementObject.id, + customPlacements + ); + if (customPlacementsByElements.length > 0) { + currentNodeId = elementObject.id; + } + const beforeCustomElements = customPlacementsByElements.filter( + c => c?.placement?.position === "before" + ); + const afterCustomElements = customPlacementsByElements.filter( + c => c?.placement?.position === "after" + ); + beforeCustomElements.forEach(b => { + if (b) placeElement(b); + }); + + if (elementObject.x === undefined) { + elementObject.x = x; + } else { + x = elementObject.x; + } + if (elementObject.y === undefined) { + elementObject.y = y; + } else { + y = elementObject.y; + } + + if (n.type === "group" && n.children && n.children.length > 0) { + const isCollapseRequired = + isCollapsed || Boolean((elementObject as FDagGroup).collapsed); + const { width, height } = positionNodes( + n.id, + n.children, + isCollapseRequired ? x : x + 20, + isCollapseRequired ? y : y + 60, + isCollapseRequired, + (elementObject as FDagGroup).spacing?.x, + (elementObject as FDagGroup).spacing?.y + ); + if (isCollapsed) { + elementObject.hidden = true; + } else { + elementObject.hidden = false; + } + elementObject.width = width < 150 ? 150 : width; + elementObject.height = height + (isCollapseRequired ? 0 : 20); + } else if (isCollapsed) { + elementObject.hidden = true; + } else { + elementObject.hidden = false; + } + + if (!elementObject.width) { + elementObject.width = this.defaultElementWidth; + } + if (!elementObject.height) { + elementObject.height = this.defaultElementHeight; + } + + if (n.type === "group") { + this.groupsHTML.push(this.getNodeGroupTemplate(elementObject, "group")); + } else { + this.nodesHTML.push(this.getNodeGroupTemplate(elementObject)); + } + + if (x + elementObject.width > maxX) { + maxX = x + elementObject.width; + } + if (y + elementObject.height > maxY) { + maxY = y + elementObject.height; + } + + if (!isCollapsed) { + if (layoutDirection === "vertical") { + x += elementObject.width + spaceX; + } else { + y += elementObject.height + spaceY; + } + } + + if (elementObject.width > maxWidth) { + maxWidth = elementObject.width; + } + if (elementObject.height > maxHeight) { + maxHeight = elementObject.height; + } + afterCustomElements.forEach(b => { + if (b) placeElement(b); + }); + currentNodeId = null; + if (n.next) nexts.push(...n.next); + } + }; + const customPlacementsElements = + containerId === "root" ? this.getCustomPlacementElements(section, customPlacements) : []; + + const beforeElements = + containerId === "root" + ? customPlacementsElements.filter(c => c?.placement?.position === "before") + : []; + const afterElements = + containerId === "root" + ? customPlacementsElements.filter(c => c?.placement?.position === "after") + : []; + beforeElements.forEach(b => { + if (b) placeElement(b); + }); + + if (beforeElements.length > 0) { + nextSection(); + maxHeight = this.defaultElementHeight; + maxWidth = this.defaultElementWidth; + } + + const skipTheseElements = [...beforeElements, ...afterElements].map(ba => ba?.id); + ns.filter(n => !skipTheseElements.includes(n.id)).forEach(placeElement); + + if (afterElements.length > 0) { + nextSection(); + maxHeight = this.defaultElementHeight; + maxWidth = this.defaultElementWidth; + } + afterElements.forEach(b => { + if (b) placeElement(b); + }); + nextSection(); + + if (nexts.length > 0) calculateCords(nexts); + }; + calculateCords(Array.from(roots)); + if (isCollapsed) { + return { + width: this.collapsedNodeWidth, + height: this.collapsedNodeHeight + }; + } + + return { + width: maxX - minX + 40, + height: maxY - minY + 60 + }; + }; + + positionNodes("root", rootNodes, 0, 0, false, this.config.spacing?.x, this.config.spacing?.y); +} diff --git a/packages/flow-lineage/src/components/f-dag/link-utils.ts b/packages/flow-lineage/src/components/f-dag/connect-link.ts similarity index 100% rename from packages/flow-lineage/src/components/f-dag/link-utils.ts rename to packages/flow-lineage/src/components/f-dag/connect-link.ts diff --git a/packages/flow-lineage/src/components/f-dag/node-utils.ts b/packages/flow-lineage/src/components/f-dag/drag-nodes-and-groups.ts similarity index 100% rename from packages/flow-lineage/src/components/f-dag/node-utils.ts rename to packages/flow-lineage/src/components/f-dag/drag-nodes-and-groups.ts diff --git a/packages/flow-lineage/src/components/f-dag/draw-links.ts b/packages/flow-lineage/src/components/f-dag/draw-links.ts new file mode 100644 index 000000000..071a2fa1a --- /dev/null +++ b/packages/flow-lineage/src/components/f-dag/draw-links.ts @@ -0,0 +1,148 @@ +import * as d3 from "d3"; +import type { FDag } from "./f-dag"; +import { CoOrdinates, FDagLink } from "./types"; + +export default function drawLinks(this: FDag) { + const getConnectingPosition = ( + id: string, + side: "left" | "right" | "top" | "bottom", + size: number + ) => { + const element = this.querySelector(`#${id}`)!; + let connectionCount: number = Number(element.dataset[side]); + if (!connectionCount) { + connectionCount = 0; + } + let point = size / 2; + if (connectionCount % 2 !== 0) { + point = size / 2 - connectionCount * 12; + } + + element.dataset[side] = `${connectionCount + 1}`; + if (point > size || point < 0) { + return size / 2; + } + return point; + }; + + // cloning because d3 is not re-drawing links + const links = structuredClone(this.config.links); + const svg = d3.select(this.linksSVG); + svg.html(``); + svg + .selectAll("path.dag-line") + .data(links) + .join("path") + .attr("class", "dag-line") + .attr("id", d => { + return `${d.from.elementId}->${d.to.elementId}`; + }) + .attr("d", d => { + const points: CoOrdinates[] = []; + + if (!d.to.x && !d.to.y && !d.from.x && !d.from.y) { + const fromElement = this.getElement(d.from.elementId); + d.from.x = fromElement.x; + d.from.y = fromElement.y; + + const toElement = this.getElement(d.to.elementId); + d.to.x = toElement.x; + d.to.y = toElement.y; + + const fromWidth = fromElement.hidden ? this.collapsedNodeWidth : fromElement.width; + const fromHeight = fromElement.hidden ? this.collapsedNodeHeight : fromElement.height; + const toWidth = toElement.hidden ? this.collapsedNodeWidth : toElement.width; + const toHeight = toElement.hidden ? this.collapsedNodeHeight : toElement.height; + + if (this.config.layoutDirection === "horizontal") { + d.direction = "horizontal"; + if (d.to.x! > d.from.x!) { + d.from.x! += fromWidth!; + d.from.y! += getConnectingPosition(d.from.elementId, "right", fromHeight!); + d.to.y! += getConnectingPosition(d.to.elementId, "left", toHeight!); + } else { + d.from.y! += getConnectingPosition(d.from.elementId, "left", fromHeight!); + d.to.x! += toWidth!; + d.to.y! += getConnectingPosition(d.to.elementId, "right", fromHeight!); + } + } else { + d.direction = "vertical"; + if (d.to.y! > d.from.y!) { + d.from.x! += getConnectingPosition(d.from.elementId, "bottom", fromWidth!); + d.from.y! += fromHeight!; + d.to.x! += getConnectingPosition(d.to.elementId, "top", toWidth!); + } else { + d.from.x! += getConnectingPosition(d.from.elementId, "top", fromWidth!); + d.to.x! += getConnectingPosition(d.to.elementId, "bottom", toWidth!); + d.to.y! += toHeight!; + } + } + } + if (!d.direction) { + if (this.config.layoutDirection === "horizontal") { + d.direction = "horizontal"; + } else { + d.direction = "vertical"; + } + } + points.push({ + x: d.from.x, + y: d.from.y + }); + points.push({ + x: d.to.x, + y: d.to.y + }); + + return this.generatePath(points, d.direction)!.toString(); + }) + .attr("stroke", d => { + const fromElement = this.getElement(d.from.elementId); + + const toElement = this.getElement(d.to.elementId); + if (fromElement.hidden || toElement.hidden) { + return "var(--color-border-secondary)"; + } + return "var(--color-border-default)"; + }) + .attr("stroke-dasharray", d => { + const fromElement = this.getElement(d.from.elementId); + + const toElement = this.getElement(d.to.elementId); + if (fromElement.hidden || toElement.hidden) { + return "4 4"; + } + return "0"; + }); + + svg + .selectAll("text.link-arrow") + .data(links) + .join("text") + .attr("class", "link-arrow") + .attr("id", function (d) { + return `${d.from.elementId}~arrow`; + }) + .attr("stroke", "var(--color-surface-default)") + .attr("stroke-width", "1px") + .attr("dy", 5.5) + .attr("dx", 2) + .append("textPath") + .attr("text-anchor", "end") + + .attr("xlink:href", function (d) { + return `#${d.from.elementId}->${d.to.elementId}`; + }) + .attr("startOffset", "100%") + .attr("fill", d => { + const fromElement = this.getElement(d.from.elementId); + + const toElement = this.getElement(d.to.elementId); + if (fromElement.hidden || toElement.hidden) { + return "var(--color-border-secondary)"; + } + return "var(--color-border-default)"; + }) + + .text("▶"); +} diff --git a/packages/flow-lineage/src/components/f-dag/f-dag.ts b/packages/flow-lineage/src/components/f-dag/f-dag.ts index ffcf2f101..7a1bd7dc0 100644 --- a/packages/flow-lineage/src/components/f-dag/f-dag.ts +++ b/packages/flow-lineage/src/components/f-dag/f-dag.ts @@ -1,32 +1,22 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { - FButton, - FInput, - flowElement, - FPopover, - FRoot, - FSelect, - FTabNode -} from "@ollion/flow-core"; +import { FButton, flowElement, FPopover, FRoot, FTabNode } from "@ollion/flow-core"; import { injectCss } from "@ollion/flow-core-config"; import globalStyle from "./f-dag-global.scss?inline"; import { html, PropertyValueMap, unsafeCSS } from "lit"; import * as d3 from "d3"; import { eventOptions, property, query, queryAll } from "lit/decorators.js"; -import { ifDefined } from "lit/directives/if-defined.js"; + import { dragNestedGroups, dragNode, getTranslateValues, moveElement, updateNodePosition -} from "./node-utils"; +} from "./drag-nodes-and-groups"; import type { - CoOrdinates, CustomPlacementByElement, CustomPlacementBySection, FDagConfig, - FDagElement, FDagGroup, FDagLink, FDagNode, @@ -38,10 +28,22 @@ import { startPlottingLine, updateLinePath, updateLink -} from "./link-utils"; -import buildHierarchy from "./hierarchy-builder"; -import { Keyed, keyed } from "lit/directives/keyed.js"; +} from "./connect-link"; +import { Keyed } from "lit/directives/keyed.js"; import { DirectiveResult } from "lit/directive.js"; +import computePlacement from "./compute-placement"; +import { + addGroupPopover, + addSelectionToGroup, + addToGroup, + addToNewGroup, + handleAddGroup, + switchTab +} from "./add-group"; +import getNodeGroupTemplate from "./get-node-group-template"; +import drawLinks from "./draw-links"; +import backgroundSVG from "./background-svg"; +import getNodeGroupActions from "./node-group-actions"; injectCss("f-dag", globalStyle); @@ -67,11 +69,12 @@ export class FDag extends FRoot { */ @queryAll(`.dag-node[data-node-type="node"]`) allNodes?: HTMLElement[]; + @queryAll(`.gr-selection-tabs`) groupSelectionTabs!: FTabNode[]; /** - * Holds reference of view port + * Holds reference of view port and required elements */ @query(`.dag-view-port`) dagViewPort!: HTMLElement; @@ -84,13 +87,12 @@ export class FDag extends FRoot { @query(`#add-group`) addGroupButton!: FButton; @query(`#add-group-popover`) - addGroupPopover!: FPopover; - - viewPortRect!: DOMRect; + addGroupPopoverRef!: FPopover; createRenderRoot() { return this; } + scale = 1; viewPortTranslate = { x: 0, @@ -127,6 +129,19 @@ export class FDag extends FRoot { dragNestedGroups = dragNestedGroups; dragNode = dragNode; updateNodePosition = updateNodePosition; + computePlacement = computePlacement; + getNodeGroupActions = getNodeGroupActions; + + /** + * Add Group utils + */ + addGroupPopover = addGroupPopover; + handleAddGroup = handleAddGroup; + addToNewGroup = addToNewGroup; + addSelectionToGroup = addSelectionToGroup; + addToGroup = addToGroup; + switchTab = switchTab; + getNodeGroupTemplate = getNodeGroupTemplate; /** * Link utils @@ -137,13 +152,10 @@ export class FDag extends FRoot { dropLine = dropLine; updateLink = updateLink; generatePath = generatePath; + drawLinks = drawLinks; getElement(id: string): FDagNode | FDagGroup { - let elementObj = this.config.nodes.find(n => n.id === id); - if (!elementObj) { - elementObj = this.config.groups.find(n => n.id === id); - } - return elementObj!; + return [...this.config.nodes, ...this.config.groups].find(n => n.id === id)!; } getCustomPlacementElements(section: number, customPlacements: Map) { @@ -173,236 +185,7 @@ export class FDag extends FRoot { protected willUpdate(changedProperties: PropertyValueMap | Map): void { super.willUpdate(changedProperties); - this.groupsHTML = []; - this.nodesHTML = []; - const { roots: rootNodes, customPlacements } = buildHierarchy(this.config); - - const positionNodes = ( - containerId: string, - elements: HierarchyNode[], - x: number, - y: number, - isCollapsed: boolean, - spaceX = 100, - spaceY = 100 - ) => { - const elementIds = elements.map(e => e.id); - const conatinerElementObject = this.getElement(containerId) as FDagGroup; - const layoutDirection = (() => { - if (containerId === "root") { - return this.config.layoutDirection; - } - if (conatinerElementObject.layoutDirection === "vertical") { - return "horizontal"; - } - return "vertical"; - })(); - const nodeLinks = this.config.links.filter( - l => elementIds.includes(l.from.elementId) && elementIds.includes(l.to.elementId) - ); - const roots = new Set(elements); - const nonroots = new Set(); - nodeLinks.forEach(link => { - const fromElement = elements.find(e => e.id === link.from.elementId)!; - if (!nonroots.has(fromElement)) { - roots.add(fromElement); - } - if (!fromElement.next) { - fromElement.next = []; - } - - const toElement = elements.find(e => e.id === link.to.elementId)!; - if (roots.has(toElement)) { - roots.delete(toElement); - } - nonroots.add(toElement); - fromElement.next.push(toElement); - }); - - const initialY = y; - const initialX = x; - let maxX = 0; - let maxY = 0; - const minX = x; - const minY = y; - let section = 0; - const calculateCords = (ns: HierarchyNode[]) => { - const nexts: HierarchyNode[] = []; - let maxWidth = this.defaultElementWidth; - let maxHeight = this.defaultElementHeight; - section += 1; - const nextSection = () => { - if (!isCollapsed) { - if (layoutDirection === "vertical") { - y += maxHeight + spaceY; - x = initialX; - } else { - x += maxWidth + spaceX; - y = initialY; - } - } - }; - - let currentNodeId: string | null; - const isElementPlacement = (elementObject: FDagElement) => - elementObject.placement && - (elementObject.placement as CustomPlacementByElement).elementId === currentNodeId; - const isSectionPlacement = (elementObject: FDagElement) => - elementObject.placement && - (elementObject.placement as CustomPlacementBySection).section === section && - containerId === "root"; - const placeElement = (n: HierarchyNode) => { - const elementObject = this.getElement(n.id); - if ( - !elementObject.placement || - isSectionPlacement(elementObject) || - isElementPlacement(elementObject) - ) { - const customPlacementsByElements = this.getCustomPlacementElementsByElementId( - elementObject.id, - customPlacements - ); - if (customPlacementsByElements.length > 0) { - currentNodeId = elementObject.id; - } - const beforeCustomElements = customPlacementsByElements.filter( - c => c?.placement?.position === "before" - ); - const afterCustomElements = customPlacementsByElements.filter( - c => c?.placement?.position === "after" - ); - beforeCustomElements.forEach(b => { - if (b) placeElement(b); - }); - - if (elementObject.x === undefined) { - elementObject.x = x; - } else { - x = elementObject.x; - } - if (elementObject.y === undefined) { - elementObject.y = y; - } else { - y = elementObject.y; - } - - if (n.type === "group" && n.children && n.children.length > 0) { - const isCollapseRequired = - isCollapsed || Boolean((elementObject as FDagGroup).collapsed); - const { width, height } = positionNodes( - n.id, - n.children, - isCollapseRequired ? x : x + 20, - isCollapseRequired ? y : y + 60, - isCollapseRequired, - (elementObject as FDagGroup).spacing?.x, - (elementObject as FDagGroup).spacing?.y - ); - if (isCollapsed) { - elementObject.hidden = true; - } else { - elementObject.hidden = false; - } - elementObject.width = width < 150 ? 150 : width; - elementObject.height = height + (isCollapseRequired ? 0 : 20); - } else if (isCollapsed) { - elementObject.hidden = true; - } else { - elementObject.hidden = false; - } - - if (!elementObject.width) { - elementObject.width = this.defaultElementWidth; - } - if (!elementObject.height) { - elementObject.height = this.defaultElementHeight; - } - - if (n.type === "group") { - this.groupsHTML.push(this.getNodeHTML(elementObject, "group")); - } else { - this.nodesHTML.push(this.getNodeHTML(elementObject)); - } - - if (x + elementObject.width > maxX) { - maxX = x + elementObject.width; - } - if (y + elementObject.height > maxY) { - maxY = y + elementObject.height; - } - - if (!isCollapsed) { - if (layoutDirection === "vertical") { - x += elementObject.width + spaceX; - } else { - y += elementObject.height + spaceY; - } - } - - if (elementObject.width > maxWidth) { - maxWidth = elementObject.width; - } - if (elementObject.height > maxHeight) { - maxHeight = elementObject.height; - } - afterCustomElements.forEach(b => { - if (b) placeElement(b); - }); - currentNodeId = null; - if (n.next) nexts.push(...n.next); - } - }; - const customPlacementsElements = - containerId === "root" ? this.getCustomPlacementElements(section, customPlacements) : []; - - const beforeElements = - containerId === "root" - ? customPlacementsElements.filter(c => c?.placement?.position === "before") - : []; - const afterElements = - containerId === "root" - ? customPlacementsElements.filter(c => c?.placement?.position === "after") - : []; - beforeElements.forEach(b => { - if (b) placeElement(b); - }); - - if (beforeElements.length > 0) { - nextSection(); - maxHeight = this.defaultElementHeight; - maxWidth = this.defaultElementWidth; - } - - const skipTheseElements = [...beforeElements, ...afterElements].map(ba => ba?.id); - ns.filter(n => !skipTheseElements.includes(n.id)).forEach(placeElement); - - if (afterElements.length > 0) { - nextSection(); - maxHeight = this.defaultElementHeight; - maxWidth = this.defaultElementWidth; - } - afterElements.forEach(b => { - if (b) placeElement(b); - }); - nextSection(); - - if (nexts.length > 0) calculateCords(nexts); - }; - calculateCords(Array.from(roots)); - if (isCollapsed) { - return { - width: this.collapsedNodeWidth, - height: this.collapsedNodeHeight - }; - } - - return { - width: maxX - minX + 40, - height: maxY - minY + 60 - }; - }; - - positionNodes("root", rootNodes, 0, 0, false, this.config.spacing?.x, this.config.spacing?.y); + this.computePlacement(); } handleZoom(event: WheelEvent) { // const chartContainer = event.currentTarget as HTMLElement; @@ -487,180 +270,6 @@ export class FDag extends FRoot { this.nodeActions.style.display = "none"; } - getNodeHTML(element: FDagNode | FDagGroup, type: "node" | "group" = "node") { - if (type === "node") { - const n = element as FDagNode; - // to force re-redner - const nKey = new Date().getTime(); - const width = n.hidden ? this.collapsedNodeWidth : n.width; - const height = n.hidden ? this.collapsedNodeHeight : n.height; - return keyed( - nKey, - html` - ${(() => { - if (n.template) { - return n.template(n); - } - if (this.config.nodeTemplate) { - return this.config.nodeTemplate(n); - } - return html` - ${n.label}`; - })()} - ${["left", "right", "top", "bottom"].map(side => { - return html``; - })} - ` - ); - } else { - const g = element as FDagGroup; - // to force re-redner - const gKey = new Date().getTime(); - return keyed( - gKey, - html` - - - ${g.label} - e.stopPropagation()} - @click=${() => this.toggleGroup(g)} - > - - - - - ${["left", "right", "top", "bottom"].map(side => { - return html``; - })} - ` - ); - } - } - - handleAddGroup() { - this.addGroupPopover.open = true; - } - addToNewGroup() { - const groupIdInput = this.querySelector("#new-group-id")!; - const groupLabelInput = this.querySelector("#new-group-label")!; - - this.config.groups.push({ - id: groupIdInput.value as string, - label: groupLabelInput.value as string, - icon: "i-org" - }); - - this.addSelectionToGroup(groupIdInput.value as string); - } - - addSelectionToGroup(groupid: string) { - this.selectedNodes.forEach(sn => { - sn.group = groupid; - }); - - this.addGroupPopover.open = false; - - this.config.nodes.forEach(n => { - n.x = undefined; - n.y = undefined; - }); - this.config.groups.forEach(n => { - n.x = undefined; - n.y = undefined; - }); - - this.config.links.forEach(l => { - l.from.x = undefined; - l.from.y = undefined; - l.to.x = undefined; - l.to.y = undefined; - }); - - this.selectedNodes = []; - this.addGroupButton.style.display = "none"; - this.requestUpdate(); - } - - addToGroup() { - const groupDropdown = this.querySelector(`#f-group-dropdown`)!; - const groupid = groupDropdown.value as string; - - this.addSelectionToGroup(groupid); - } - - switchTab(event: PointerEvent) { - const tabNodeElement = event.currentTarget as FTabNode; - - this.groupSelectionTabs.forEach(tab => { - tab.active = false; - }); - tabNodeElement.active = true; - } render() { return html` `; } protected updated(changedProperties: PropertyValueMap | Map): void { super.updated(changedProperties); - const getConnectingPosition = ( - id: string, - side: "left" | "right" | "top" | "bottom", - size: number - ) => { - const element = this.querySelector(`#${id}`)!; - let connectionCount: number = Number(element.dataset[side]); - if (!connectionCount) { - connectionCount = 0; - } - let point = size / 2; - if (connectionCount % 2 !== 0) { - point = size / 2 - connectionCount * 12; - } - - element.dataset[side] = `${connectionCount + 1}`; - if (point > size || point < 0) { - return size / 2; - } - return point; - }; - - // cloning because d3 is not re-drawing links - const links = structuredClone(this.config.links); - const svg = d3.select(this.linksSVG); - svg.html(``); - svg - .selectAll("path.dag-line") - .data(links) - .join("path") - .attr("class", "dag-line") - .attr("id", d => { - return `${d.from.elementId}->${d.to.elementId}`; - }) - .attr("d", d => { - const points: CoOrdinates[] = []; - - if (!d.to.x && !d.to.y && !d.from.x && !d.from.y) { - const fromElement = this.getElement(d.from.elementId); - d.from.x = fromElement.x; - d.from.y = fromElement.y; - - const toElement = this.getElement(d.to.elementId); - d.to.x = toElement.x; - d.to.y = toElement.y; - - const fromWidth = fromElement.hidden ? this.collapsedNodeWidth : fromElement.width; - const fromHeight = fromElement.hidden ? this.collapsedNodeHeight : fromElement.height; - const toWidth = toElement.hidden ? this.collapsedNodeWidth : toElement.width; - const toHeight = toElement.hidden ? this.collapsedNodeHeight : toElement.height; - - if (this.config.layoutDirection === "horizontal") { - d.direction = "horizontal"; - if (d.to.x! > d.from.x!) { - d.from.x! += fromWidth!; - d.from.y! += getConnectingPosition(d.from.elementId, "right", fromHeight!); - d.to.y! += getConnectingPosition(d.to.elementId, "left", toHeight!); - } else { - d.from.y! += getConnectingPosition(d.from.elementId, "left", fromHeight!); - d.to.x! += toWidth!; - d.to.y! += getConnectingPosition(d.to.elementId, "right", fromHeight!); - } - } else { - d.direction = "vertical"; - if (d.to.y! > d.from.y!) { - d.from.x! += getConnectingPosition(d.from.elementId, "bottom", fromWidth!); - d.from.y! += fromHeight!; - d.to.x! += getConnectingPosition(d.to.elementId, "top", toWidth!); - } else { - d.from.x! += getConnectingPosition(d.from.elementId, "top", fromWidth!); - d.to.x! += getConnectingPosition(d.to.elementId, "bottom", toWidth!); - d.to.y! += toHeight!; - } - } - } - if (!d.direction) { - if (this.config.layoutDirection === "horizontal") { - d.direction = "horizontal"; - } else { - d.direction = "vertical"; - } - } - points.push({ - x: d.from.x, - y: d.from.y - }); - points.push({ - x: d.to.x, - y: d.to.y - }); - - return this.generatePath(points, d.direction)!.toString(); - }) - .attr("stroke", d => { - const fromElement = this.getElement(d.from.elementId); - - const toElement = this.getElement(d.to.elementId); - if (fromElement.hidden || toElement.hidden) { - return "var(--color-border-secondary)"; - } - return "var(--color-border-default)"; - }) - .attr("stroke-dasharray", d => { - const fromElement = this.getElement(d.from.elementId); - - const toElement = this.getElement(d.to.elementId); - if (fromElement.hidden || toElement.hidden) { - return "4 4"; - } - return "0"; - }); - - svg - .selectAll("text.link-arrow") - .data(links) - .join("text") - .attr("class", "link-arrow") - .attr("id", function (d) { - return `${d.from.elementId}~arrow`; - }) - .attr("stroke", "var(--color-surface-default)") - .attr("stroke-width", "1px") - .attr("dy", 5.5) - .attr("dx", 2) - .append("textPath") - .attr("text-anchor", "end") - - .attr("xlink:href", function (d) { - return `#${d.from.elementId}->${d.to.elementId}`; - }) - .attr("startOffset", "100%") - .attr("fill", d => { - const fromElement = this.getElement(d.from.elementId); - - const toElement = this.getElement(d.to.elementId); - if (fromElement.hidden || toElement.hidden) { - return "var(--color-border-secondary)"; - } - return "var(--color-border-default)"; - }) - - .text("▶"); - void this.updateComplete.then(() => { - this.viewPortRect = this.dagViewPort.getBoundingClientRect(); - }); + this.drawLinks(); this.onmousemove = (event: MouseEvent) => { if (event.buttons === 1) { diff --git a/packages/flow-lineage/src/components/f-dag/get-node-group-template.ts b/packages/flow-lineage/src/components/f-dag/get-node-group-template.ts new file mode 100644 index 000000000..624d63513 --- /dev/null +++ b/packages/flow-lineage/src/components/f-dag/get-node-group-template.ts @@ -0,0 +1,124 @@ +import type { FDag } from "./f-dag"; +import { FDagGroup, FDagNode } from "./types"; +import { keyed } from "lit/directives/keyed.js"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { html } from "lit"; + +export default function getNodeGroupTemplate( + this: FDag, + element: FDagNode | FDagGroup, + type: "node" | "group" = "node" +) { + if (type === "node") { + const n = element as FDagNode; + // to force re-redner + const nKey = new Date().getTime(); + const width = n.hidden ? this.collapsedNodeWidth : n.width; + const height = n.hidden ? this.collapsedNodeHeight : n.height; + return keyed( + nKey, + html` + ${(() => { + if (n.template) { + return n.template(n); + } + if (this.config.nodeTemplate) { + return this.config.nodeTemplate(n); + } + return html` + ${n.label}`; + })()} + ${["left", "right", "top", "bottom"].map(side => { + return html``; + })} + ` + ); + } else { + const g = element as FDagGroup; + // to force re-redner + const gKey = new Date().getTime(); + return keyed( + gKey, + html` + + + ${g.label} + e.stopPropagation()} + @click=${() => this.toggleGroup(g)} + > + + + + + ${["left", "right", "top", "bottom"].map(side => { + return html``; + })} + ` + ); + } +} diff --git a/packages/flow-lineage/src/components/f-dag/hierarchy-builder.ts b/packages/flow-lineage/src/components/f-dag/hierarchy-builder.ts index efed57647..f25f3b3d6 100644 --- a/packages/flow-lineage/src/components/f-dag/hierarchy-builder.ts +++ b/packages/flow-lineage/src/components/f-dag/hierarchy-builder.ts @@ -1,4 +1,4 @@ -import { FDagConfig, FDagElement, FDagGroup, HierarchyNode } from "./types"; +import type { FDagConfig, FDagElement, FDagGroup, HierarchyNode } from "./types"; export default function buildHierarchy(config: FDagConfig) { const nodesMap = new Map(); diff --git a/packages/flow-lineage/src/components/f-dag/node-group-actions.ts b/packages/flow-lineage/src/components/f-dag/node-group-actions.ts new file mode 100644 index 000000000..5a31a94c8 --- /dev/null +++ b/packages/flow-lineage/src/components/f-dag/node-group-actions.ts @@ -0,0 +1,29 @@ +import { html } from "lit"; +import type { FDag } from "./f-dag"; + +export default function getNodeGroupActions(this: FDag) { + return html` `; +}