diff --git a/package.json b/package.json index fbb88f9..25ec32d 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "_test": "tape 'test/**/*-test.js'", "_prepublish": "npm run test && uglifyjs build/d3-org-chart.js -c -m -o build/d3-org-chart.min.js", "_postpublish": "zip -j build/d3-org-chart.zip -- LICENSE README.md build/d3-org-chart.js build/d3-org-chart.min.js", - "pretest": "rm -rf build && mkdir build && rollup -g d3-selection:d3,d3-array:d3,d3-hierarchy:d3,d3-zoom:d3,d3-flextree:d3,d3-shape:d3,d3-group:d3 -f umd -n d3 -o build/d3-org-chart.js -- index.js", + "pretest": "rm -rf build && mkdir build && rollup -g d3-drag:d3,d3-selection:d3,d3-array:d3,d3-hierarchy:d3,d3-zoom:d3,d3-flextree:d3,d3-shape:d3,d3-group:d3 -f umd -n d3 -o build/d3-org-chart.js -- index.js", "test": "tape 'test/**/*-test.js'", "prepublish": "npm run test && uglifyjs build/d3-org-chart.js -c -m -o build/d3-org-chart.min.js", "postpublish": "zip -j build/d3-org-chart.zip -- LICENSE README.md build/d3-org-chart.js build/d3-org-chart.min.js" @@ -49,6 +49,7 @@ "uglify-js": "3" }, "dependencies": { + "d3-drag": "^3.0.0", "d3-selection": "3", "d3-array": "3", "d3-hierarchy": "3", diff --git a/src/d3-org-chart.js b/src/d3-org-chart.js index 2d4f6a1..530dff2 100644 --- a/src/d3-org-chart.js +++ b/src/d3-org-chart.js @@ -1,13 +1,15 @@ -import { selection, select } from "d3-selection"; +import { selection, select, selectAll } from "d3-selection"; import { max, min, sum, cumsum } from "d3-array"; import { tree, stratify } from "d3-hierarchy"; import { zoom, zoomIdentity } from "d3-zoom"; import { flextree } from 'd3-flextree'; import { linkHorizontal } from 'd3-shape'; +import { drag } from 'd3-drag'; const d3 = { selection, select, + selectAll, max, min, sum, @@ -17,6 +19,11 @@ const d3 = { zoom, zoomIdentity, linkHorizontal, + drag, + data: null, + sourceNode: null, + targetNode: null, + attrs: null, flextree } @@ -287,6 +294,36 @@ export class OrgChart { .attr("marker-start", d => `url(#${d.from + "_" + d.to})`) .attr("marker-end", d => `url(#arrow-${d.from + "_" + d.to})`) }, + linkUpdate: function (d, i, arr) { + d3.select(this) + .attr("stroke", d => d.data._upToTheRootHighlighted ? '#152785' : 'lightgray') + .attr("stroke-width", d => d.data._upToTheRootHighlighted ? 5 : 2) + + if (d.data._upToTheRootHighlighted) { + d3.select(this).raise() + } + }, + nodeUpdate: function (d, i, arr) { + d3.select(this) + .select('.node-rect') + .attr("stroke", d => d.data._highlighted || d.data._upToTheRootHighlighted ? '#152785' : 'none') + .attr("stroke-width", d.data._highlighted || d.data._upToTheRootHighlighted ? 10 : 1) + }, + + nodeWidth: d3Node => 250, + nodeHeight: d => 150, + siblingsMargin: d3Node => 20, + childrenMargin: d => 60, + neightbourMargin: (n1, n2) => 80, + compactMarginPair: d => 100, + compactMarginBetween: (d3Node => 20), + onNodeClick: (d) => d, + svg: null, + dragHandler: null, + refresh: this, + descendants: null, + onDrop: (dropData) => dropData, + onDrag: () => {}, // Link generator for connections linkGroupArc: d3.linkHorizontal().x(d => d.x).y(d => d.y), @@ -678,9 +715,134 @@ export class OrgChart { attrs.firstDraw = false; } + attrs.svg = svg; + + // // add drag and drop + d3.attrs = attrs; + return this; } + dragAttachHandler() { + const attrs = this.getChartState(); + attrs.svg.selectAll(".node").call( + d3.drag() + .on("start", this.dragstarted) + .on("drag", this.dragged) + .on("end", this.dragended) + ); + } + dragstarted(d) { + console.log(d) + d.sourceEvent.stopPropagation(); + d3.select(this).classed("dragging", true); + d3.sourceNode = d; + } + dragged(d, event) { + const x = (d.x) - (event.width / 2); + + // const _x = (event.x - d.x); + // const _y = (event.y - d.y); + // const moveThreshold = 30; + // const isMoved = (_x > -(moveThreshold) && _x < (moveThreshold)) || (_y > -(moveThreshold) && _y < (moveThreshold)); + + // if(!isMoved) return; + + d3.select(this).raise().attr("transform", `translate(${x},${d.y})`); + // set default style + d3.selectAll("rect").attr("fill", "#fff").attr("stroke", "null").attr( + "stroke-width", + "1px" + ); + d3.targetNode = null; + // check nodes overlapping + const cP = { + x0: d.x, + y0: d.y, + x1: d.x + event.width, + y1: d.y + event.height, + }; + + d3.selectAll("g.node:not(.dragging)").filter((d, i) => { + const cPInner = { + x0: d.x, + y0: d.y, + x1: d.x + d.width, + y1: d.y + d.height, + }; + if ( + (cP.x1 > cPInner.x0 && cP.x0 < cPInner.x1) && + (cP.y1 > cPInner.y0 && cP.y0 < cPInner.y1) + ) { + d3.targetNode = d; + return d; + } + }).select("rect").attr("fill", "#e4e1e1").attr("stroke", "#e4e1e1") + .attr("stroke-width", "2px"); + } + dragended(d) { + if (!d3.attrs.data || d3.attrs.data.length == 0) { + console.log("ORG CHART - Data is empty"); + return this; + } + d3.select(this).classed("dragging", false); + + // set default style + d3.selectAll("rect").attr("fill", "#fff").attr("stroke", "null").attr( + "stroke-width", + "1px" + ); + + const x = (d.subject.x) - (d.subject.width / 2); + + d3.select(this).attr("transform", `translate(${x},${(d.subject.y)})`); + + if (d3.sourceNode && d3.targetNode) { + const sourceNodeData = d3.sourceNode.subject.data; + const targetNodeData = d3.targetNode.data; + + const sourceNodeIndex = d3.attrs.data.findIndex((d) => + d.id == sourceNodeData.id + ); + const targetNodeIndex = d3.attrs.data.findIndex((d) => + d.id == targetNodeData.id + ); + + if (targetNodeData.parentId == sourceNodeData.id) { + d3.attrs.data[targetNodeIndex].parentId = + sourceNodeData.parentId; + } else { + const sourceId = sourceNodeData.id; + const sourceParentId = sourceNodeData.parentId; + // get all children of source node + const sourceChildren = d3.attrs.data.filter((d) => + d.parentId == sourceId + ); + + if (sourceChildren) { + // replace parentId of all children with source ParentId + sourceChildren.forEach((d) => { + d.parentId = sourceParentId; + }); + } + } + + d3.attrs.data[sourceNodeIndex].parentId = targetNodeData.id; + + d3.attrs.refresh.updateNodesState(); + + d3.attrs.onDrop({ + source: d3.attrs.data[sourceNodeIndex], + target: d3.attrs.data[targetNodeIndex], + }); + } + // clear current state + d3.sourceNode = null; + d3.targetNode = null; + + // return this; + } + // This function can be invoked via chart.addNode API, and it adds node in tree at runtime addNode(obj) { const attrs = this.getChartState(); @@ -1258,7 +1420,16 @@ export class OrgChart { nodes: centeredNodes }) } - + // This function detects whether current browser is edge + // + // attach drag and drop event + this.dragAttachHandler(); + + const _attrs = this.getChartState(); + const { root } = _attrs; + if (root && root.descendants()) { + this.descendants = root.descendants(); + } } // This function detects whether current browser is edge @@ -1898,10 +2069,19 @@ export class OrgChart { return measurement.width; } + exportData() { + const attrs = this.getChartState(); + if (attrs && attrs.data) { + return attrs.data; + } else { + return null; + } + } + // Clear after moving off from the page clear() { const attrs = this.getChartState(); d3.select(window).on(`resize.${attrs.id}`, null); attrs.svg && attrs.svg.selectAll("*").remove(); } -} \ No newline at end of file +}