diff --git a/packages/flow-lineage/package.json b/packages/flow-lineage/package.json index 65d95002e..b11330898 100644 --- a/packages/flow-lineage/package.json +++ b/packages/flow-lineage/package.json @@ -22,13 +22,14 @@ "dependencies": { "@ollion/flow-core": "workspace:*", "@ollion/flow-core-config": "workspace:*", - "d3": "^7.6.1", + "d3": "^7.8.4", + "d3-dag": "^1.1.0", "lit": "^3.1.0" }, "devDependencies": { "@custom-elements-manifest/analyzer": "^0.5.7", "@open-wc/testing": "^3.1.5", - "@types/d3": "^7.4.0", + "@types/d3": "^7.4.3", "@types/jest": "29.5.5", "@web/dev-server-esbuild": "^0.3.0", "@web/test-runner": "^0.13.30", diff --git a/packages/flow-lineage/src/components/f-dag/f-dag-global.scss b/packages/flow-lineage/src/components/f-dag/f-dag-global.scss new file mode 100644 index 000000000..7f2764f30 --- /dev/null +++ b/packages/flow-lineage/src/components/f-dag/f-dag-global.scss @@ -0,0 +1,34 @@ +f-dag { + display: flex; + overflow: auto; + + foreignObject { + overflow: visible; + } + + foreignObject { + cursor: pointer; + overflow: visible; + > * { + position: fixed !important; + } + * { + transform-origin: 0% 0%; + } + } +} + +f-div[direction="column"] { + > f-dag { + width: 100%; + flex: 1 0 auto; + } +} + +f-div[direction="row"] { + > f-dag { + flex: 1 0; + max-width: 100%; + height: 100%; + } +} diff --git a/packages/flow-lineage/src/components/f-dag/f-dag.ts b/packages/flow-lineage/src/components/f-dag/f-dag.ts new file mode 100644 index 000000000..350c28fd2 --- /dev/null +++ b/packages/flow-lineage/src/components/f-dag/f-dag.ts @@ -0,0 +1,290 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { flowElement, FRoot } 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 { ref, createRef, Ref } from "lit/directives/ref.js"; +import * as d3 from "d3"; +import * as d3dag from "d3-dag"; + +injectCss("f-dag", globalStyle); +// Renders attribute names of parent element to textContent + +@flowElement("f-dag") +export class FDag extends FRoot { + /** + * css loaded from scss file + */ + static styles = [unsafeCSS(globalStyle)]; + + createRenderRoot() { + return this; + } + + svgElement: Ref = createRef(); + + render() { + return html` + + + + + + + `; + } + protected updated(_changedProperties: PropertyValueMap | Map): void { + // ----- // + // Setup // + // ----- // + + /** + * get transform for arrow rendering + * + * This transform takes anything with points (a graph link) and returns a + * transform that puts an arrow on the last point, aligned based off of the + * second to last. + */ + function arrowTransform({ + points + }: { + points: readonly (readonly [number, number])[]; + }): string { + const [[x1, y1], [x2, y2]] = points.slice(-2); + const angle = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI + 90; + return `translate(${x2}, ${y2}) rotate(${angle})`; + } + + // our raw data to render + const data = [ + { + id: "0", + parentIds: ["8"] + }, + { + id: "1", + parentIds: [] + }, + { + id: "2", + parentIds: [] + }, + { + id: "3", + parentIds: ["11"] + }, + { + id: "4", + parentIds: ["12"] + }, + { + id: "5", + parentIds: ["18"] + }, + { + id: "6", + parentIds: ["9", "15", "17"] + }, + { + id: "7", + parentIds: ["3", "17", "20", "21"] + }, + { + id: "8", + parentIds: [] + }, + { + id: "9", + parentIds: ["4"] + }, + { + id: "10", + parentIds: ["16", "21"] + }, + { + id: "11", + parentIds: ["2"] + }, + { + id: "12", + parentIds: ["21"] + }, + { + id: "13", + parentIds: ["4", "12"] + }, + { + id: "14", + parentIds: ["1", "8"] + }, + { + id: "15", + parentIds: [] + }, + { + id: "16", + parentIds: ["0"] + }, + { + id: "17", + parentIds: ["19"] + }, + { + id: "18", + parentIds: ["9"] + }, + { + id: "19", + parentIds: [] + }, + { + id: "20", + parentIds: ["13"] + }, + { + id: "21", + parentIds: [] + } + ]; + + // create our builder and turn the raw data into a graph + const builder = d3dag.graphStratify(); + const graph = builder(data); + // -------------- // + // Compute Layout // + // -------------- // + + // set the layout functions + const nodeRadius = 40; + const nodeSize = [nodeRadius * 2, nodeRadius * 2] as const; + // this truncates the edges so we can render arrows nicely + const shape = d3dag.tweakShape(nodeSize, d3dag.shapeEllipse); + // use this to render our edges + const line = d3.line().curve(d3.curveMonotoneY); + // here's the layout operator, uncomment some of the settings + const layout = d3dag + .sugiyama() + //.grid() + //.zherebko() + //@ts-ignore + .nodeSize(nodeSize) + .gap([nodeRadius, nodeRadius]) + .tweaks([shape]); + + // actually perform the layout and get the final size + const { width, height } = layout(graph); + + // --------- // + // Rendering // + // --------- // + + // colors + // const steps = graph.nnodes() - 1; + // const interp = d3.interpolateRainbow; + // const colorMap = new Map( + // [...graph.nodes()] + // .sort((a, b) => a.y - b.y) + // .map((node, i) => [node.data.id, interp(i / steps)]) + // ); + + // global + const svg = d3 + .select(this.svgElement.value as SVGSVGElement) + // pad a little for link thickness + .style("width", Math.max(width, this.offsetWidth)) + .style("height", Math.max(height, this.offsetHeight)); + + // nodes + svg + .select("#nodes") + .selectAll("g") + .data(graph.nodes()) + .join(enter => + enter + .append("g") + .attr("transform", ({ x, y }) => `translate(${x - nodeRadius}, ${y - nodeRadius})`) + .append("foreignObject") + .attr("width", nodeRadius * 2) + .attr("height", nodeRadius * 2) + .html(d => { + return `${d.data.id}`; + }) + ); + + // // link gradients + // svg + // .select("#defs") + // .selectAll("linearGradient") + // .data(graph.links()) + // .join(enter => + // enter + // .append("linearGradient") + // .attr("id", ({ source, target }) => + // encodeURIComponent(`${source.data.id}--${target.data.id}`) + // ) + // .attr("gradientUnits", "userSpaceOnUse") + // .attr("x1", ({ points }) => points[0][0]) + // .attr("x2", ({ points }) => points[points.length - 1][0]) + // .attr("y1", ({ points }) => points[0][1]) + // .attr("y2", ({ points }) => points[points.length - 1][1]) + // .call(enter => { + // enter + // .append("stop") + // .attr("class", "grad-start") + // .attr("offset", "0%") + // .attr("stop-color", ({ source }) => colorMap.get(source.data.id)!); + // enter + // .append("stop") + // .attr("class", "grad-stop") + // .attr("offset", "100%") + // .attr("stop-color", ({ target }) => colorMap.get(target.data.id)!); + // }) + // ); + + // link paths + svg + .select("#links") + .selectAll("path") + .data(graph.links()) + .join( + enter => + enter + .append("path") + .attr("d", ({ points }) => line(points)) + .attr("fill", "none") + .attr("stroke-width", 1.5) + .attr("stroke", "var(--color-border-default)") + // .attr("stroke", ({ source, target }) => `url(#${source.data.id}--${target.data.id})`) + ); + + // Arrows + const arrowSize = 80; + // const arrowLen = Math.sqrt((4 * arrowSize) / Math.sqrt(3)); + const arrow = d3.symbol().type(d3.symbolTriangle).size(arrowSize); + svg + .select("#arrows") + .selectAll("path") + .data(graph.links()) + .join( + enter => + enter + .append("path") + .attr("d", arrow) + .attr("fill", "var(--color-border-default)") + // .attr("fill", ({ target }) => colorMap.get(target.data.id)!) + .attr("transform", arrowTransform) + // .attr("stroke", "white") + // .attr("stroke-width", 1) + // .attr("stroke-dasharray", `${arrowLen},${arrowLen}`) + ); + } +} + +/** + * Required for typescript + */ +declare global { + interface HTMLElementTagNameMap { + "f-dag": FDag; + } +} diff --git a/packages/flow-lineage/src/index.ts b/packages/flow-lineage/src/index.ts index a1c5d1af3..4b7659f3e 100644 --- a/packages/flow-lineage/src/index.ts +++ b/packages/flow-lineage/src/index.ts @@ -1,5 +1,7 @@ export * from "./components/f-lineage/f-lineage"; export * from "./components/f-lineage/lineage-types"; +export * from "./components/f-dag/f-dag"; + import { version } from "./../package.json"; console.log( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73c2d5c77..d060ee52b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -484,8 +484,11 @@ importers: specifier: workspace:* version: link:../flow-core-config d3: - specifier: ^7.6.1 + specifier: ^7.8.4 version: 7.8.5 + d3-dag: + specifier: ^1.1.0 + version: 1.1.0 lit: specifier: ^3.1.0 version: 3.1.1 @@ -497,7 +500,7 @@ importers: specifier: ^3.1.5 version: 3.2.2 '@types/d3': - specifier: ^7.4.0 + specifier: ^7.4.3 version: 7.4.3 '@types/jest': specifier: 29.5.5 @@ -7786,6 +7789,15 @@ packages: d3-array: 3.2.4 dev: false + /d3-dag@1.1.0: + resolution: {integrity: sha512-N8IxsIHcUaIxLrV3cElTC47kVJGFiY3blqSuJubQhyhYBJs0syfFPTnRSj2Cq0LBxxi4mzJmcqCvHIv9sPdILQ==} + dependencies: + d3-array: 3.2.4 + javascript-lp-solver: 0.4.24 + quadprog: 1.6.1 + stringify-object: 5.0.0 + dev: false + /d3-delaunay@6.0.4: resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} engines: {node: '>=12'} @@ -9432,6 +9444,11 @@ packages: engines: {node: '>=12.17'} dev: true + /get-own-enumerable-keys@1.0.0: + resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==} + engines: {node: '>=14.16'} + dev: false + /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -10150,6 +10167,11 @@ packages: engines: {node: '>=0.12.0'} dev: true + /is-obj@3.0.0: + resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} + engines: {node: '>=12'} + dev: false + /is-path-cwd@2.2.0: resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} engines: {node: '>=6'} @@ -10185,6 +10207,11 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-regexp@3.1.0: + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} + dev: false + /is-shared-array-buffer@1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} dependencies: @@ -10353,6 +10380,10 @@ packages: minimatch: 3.1.2 dev: true + /javascript-lp-solver@0.4.24: + resolution: {integrity: sha512-5edoDKnMrt/u3M6GnZKDDIPxOyFOg+WrwDv8mjNiMC2DePhy2H9/FFQgf4ggywaXT1utvkxusJcjQUER72cZmA==} + dev: false + /jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -12630,6 +12661,11 @@ packages: side-channel: 1.0.4 dev: true + /quadprog@1.6.1: + resolution: {integrity: sha512-fN5Jkcjlln/b3pJkseDKREf89JkKIyu6cKIVXisgL6ocKPQ0yTp9n6NZUAq3otEPPw78WZMG9K0o9WsfKyMWJw==} + engines: {node: '>=8.x'} + dev: false + /querystring@0.2.0: resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} engines: {node: '>=0.4.x'} @@ -13618,6 +13654,15 @@ packages: safe-buffer: 5.2.1 dev: true + /stringify-object@5.0.0: + resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} + engines: {node: '>=14.16'} + dependencies: + get-own-enumerable-keys: 1.0.0 + is-obj: 3.0.0 + is-regexp: 3.1.0 + dev: false + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} diff --git a/stories/flow-lineage/f-dag.stories.ts b/stories/flow-lineage/f-dag.stories.ts new file mode 100644 index 000000000..8c61b57cf --- /dev/null +++ b/stories/flow-lineage/f-dag.stories.ts @@ -0,0 +1,12 @@ +import { Meta, Story } from "@storybook/web-components"; +import { html } from "lit-html"; + +export default { + title: "@ollion/flow-lineage/f-dag" +} as Meta>; + +export const Basic = { + render: () => { + return html` `; + } +};