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`
-
-
- ${["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`
-
- e.stopPropagation()}>
-
-
-
- Exisiting
-
-
-
-
- Create New
-
-
-
-
-
- g.id)}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ ${this.addGroupPopover()} ${backgroundSVG()}
${this.groupsHTML.reverse()}${this.nodesHTML.reverse()}
-
-
- Select
-
-
-
-
- Delete
-
-
+ ${this.getNodeGroupActions()}
`;
}
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`
+
+
+ ${["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`
+
+ Select
+
+
+
+
+ Delete
+
+ `;
+}