diff --git a/package-lock.json b/package-lock.json index b77af21..331f847 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,8 @@ "": { "dependencies": { "d3": "^7.9.0", - "react-d3-tree": "^3.6.2" + "react-d3-tree": "^3.6.2", + "webcola": "^3.4.0" }, "devDependencies": { "@headlessui/react": "^2.0.0", @@ -4434,6 +4435,46 @@ "loose-envify": "^1.0.0" } }, + "node_modules/webcola": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/webcola/-/webcola-3.4.0.tgz", + "integrity": "sha512-4BiLXjXw3SJHo3Xd+rF+7fyClT6n7I+AR6TkBqyQ4kTsePSAMDLRCXY1f3B/kXJeP9tYn4G1TblxTO+jAt0gaw==", + "license": "MIT", + "dependencies": { + "d3-dispatch": "^1.0.3", + "d3-drag": "^1.0.4", + "d3-shape": "^1.3.5", + "d3-timer": "^1.0.5" + } + }, + "node_modules/webcola/node_modules/d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", + "license": "BSD-3-Clause" + }, + "node_modules/webcola/node_modules/d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "node_modules/webcola/node_modules/d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==", + "license": "BSD-3-Clause" + }, + "node_modules/webcola/node_modules/d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", + "license": "BSD-3-Clause" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index e2a0294..6039a43 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "d3": "^7.9.0", - "react-d3-tree": "^3.6.2" + "react-d3-tree": "^3.6.2", + "webcola": "^3.4.0" } } diff --git a/resources/css/app.css b/resources/css/app.css index 33814bc..9093109 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -2,7 +2,37 @@ @tailwind components; @tailwind utilities; -#treeWrapper { - width: '100%'; +.treeWrapper { height: '100vh'; + flex: 1; + max-width: 70%; +} + +.treeWrapper.sm { + max-width: 30%; +} + +.row { + display: flex; + flex-wrap:wrap; +} + +.node { + stroke: #fff; + stroke-width: 1.5px; + cursor: move; +} + +.link { + stroke: #999; + stroke-width: 3px; + stroke-opacity: 1; +} + +.label { + fill: white; + font-family: Verdana; + font-size: 25px; + text-anchor: middle; + cursor: move; } diff --git a/resources/js/Components/Trees/DAG.tsx b/resources/js/Components/Trees/DAG.tsx new file mode 100644 index 0000000..cc3d13f --- /dev/null +++ b/resources/js/Components/Trees/DAG.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import Tree from "react-d3-tree"; +import { scaleOrdinal } from 'd3'; + +const containerStyles = { + width: '100%', + height: '100vh', +} + +const colorScale = scaleOrdinal() + .domain(['Inquiry', 'Reflection', 'Response']) + .range(['yellow', 'blue', 'green']); // Adjust colors as needed + +interface DAGProps {} + +export default class DAG extends React.Component { + state = {} + + // TODO: Kenji - this breaks clicking on nodes to expand/collapse, and also renders improperly. + // renderNode = (nodeData) => { + // console.log(nodeData); + // const color = colorScale(nodeData.nodeDatum.type); + // return ( + // + // + // {nodeData.nodeDatum.attributes.content} + // + // ); + // }; + + + componentDidMount() { + const dimensions = this.treeContainer.getBoundingClientRect(); + this.setState({ + translate: { + x: dimensions.width / 3, + y: dimensions.height / 4 // TODO: Kenji, why does this need to be /4 for it to be centered? + } + }); + } + + render() { + return ( +
(this.treeContainer = tc)}> + +
+ ); + } +} diff --git a/resources/js/Components/Trees/WebCola.tsx b/resources/js/Components/Trees/WebCola.tsx new file mode 100644 index 0000000..f205486 --- /dev/null +++ b/resources/js/Components/Trees/WebCola.tsx @@ -0,0 +1,75 @@ +import React, { useRef, useEffect, useState } from 'react'; +import * as cola from 'webcola'; +import * as d3 from 'd3'; + +// Based on: https://github.com/tgdwyer/WebCola/blob/master/website/examples/smallnonoverlappinggraph.html +const Tree = ({ nodes, links }) => { + const svgRef = useRef(null); + const width = "100%"; + const height = "100%"; + const color = d3.scaleOrdinal(d3.schemeCategory10); + const colad3 = cola.d3adaptor(d3) + .linkDistance(120) + .avoidOverlaps(true); + + + useEffect(() => { + const svg = d3.select(svgRef.current); // Select SVG element + const containerWidth = svgRef.current.clientWidth; + const containerHeight = svgRef.current.clientHeight; + + // Render nodes and links using colad3 + colad3 + .nodes(nodes) + .links(links) + .size([containerWidth, containerHeight]) + .start(); + + var link = svg.selectAll(".link") + .data(links) + .enter().append("line") + .attr("class", "link"); + + var node = svg.selectAll(".node") + .data(nodes) + .enter().append("rect") + .attr("class", "node") + .attr("width", (d) => d.width) + .attr("height", (d) => d.height) + .attr("rx", 5).attr("ry", 5) + .style("fill", (d) => color(1)) + .call(colad3.drag); + + var label = svg.selectAll(".label") + .data(nodes) + .enter().append("text") + .attr("class", "label") + .text((d) => d.name) + .call(colad3.drag); + + colad3.on('tick', () => { + link + .attr('x1', (d) => d.source.x) + .attr('y1', (d) => d.source.y) + .attr('x2', (d) => d.target.x) + .attr('y2', (d) => d.target.y); + + node + .attr('x', (d) => d.x - d.width / 2) + .attr('y', (d) => d.y - d.height / 2); + + label + .attr('x', (d) => d.x) + .attr('y', function(d) { + const h = this.getBBox().height; + return d.y + h / 4; + }); + }); + }, [nodes, links]); + + return ( + + ) +} + +export default Tree; diff --git a/resources/js/Pages/Dashboard.tsx b/resources/js/Pages/Dashboard.tsx index 03c80d6..775a3a5 100644 --- a/resources/js/Pages/Dashboard.tsx +++ b/resources/js/Pages/Dashboard.tsx @@ -1,11 +1,12 @@ import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { Head } from '@inertiajs/react'; import { PageProps } from '@/types'; -import CenteredTree from '../Components/CenteredTree'; +import DAG from '../Components/Trees/DAG'; +import Tree from '../Components/Trees/WebCola'; // This is a simplified example of an org chart with a depth of 2. // Note how deeper levels are defined recursively via the `children` property. -const sampleInquiryChart = { +const sampleInquiryChart = [{ name: 'Inquiry', attributes: { content: 'What is the colour of the sky?', @@ -28,11 +29,36 @@ const sampleInquiryChart = { { name: 'Response', attributes: { - content: 'The sky is clear.' - }, + content: 'The sky is clear.', + } }, - ], -}; + ] +}] + +// const nodes = [ +// { id: 0, name: 'Root', parent: null }, +// { id: 1, name: 'Child 1', parent: 0 }, +// { id: 2, name: 'Child 2', parent: 0 }, +// // ... additional nodes +// ]; +// const links = nodes.filter(d => d.parent !== null).map(d => ({ source: d.parent, target: d.id })); + +const example = { + "nodes":[ + {"name":"a","width":60,"height":40}, + {"name":"b","width":60,"height":40}, + {"name":"c","width":60,"height":40}, + {"name":"d","width":60,"height":40}, + {"name":"e","width":60,"height":40} + ], + "links":[ + {"source":0,"target":1}, + {"source":1,"target":2}, + {"source":2,"target":0}, + {"source":2,"target":3} + ] +} + export default function Dashboard({ auth }: PageProps) { return ( @@ -47,8 +73,13 @@ export default function Dashboard({ auth }: PageProps) {
You're logged in!
-
- +
+
+ +
+
+ +