Spaces:
Running
Running
| <script src="https://d3js.org/d3.v7.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/d3-org-chart@3.1.0"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/d3-flextree@2.1.2/build/d3-flextree.js"></script> | |
| <style type="text/css"> | |
| .hide { | |
| display: none; | |
| } | |
| .drag-enabled:not(.dragging-active) .node.draggable { | |
| stroke: grey; | |
| stroke-width: 3px; | |
| stroke-dasharray: 2px; | |
| } | |
| .drag-enabled.dragging-active .droppable { | |
| stroke: green; | |
| stroke-width: 3px; | |
| stroke-dasharray: 5px; | |
| } | |
| .node.dragging { | |
| stroke-dasharray: 0 ; | |
| stroke-width: 0 ; | |
| } | |
| .node.dragging .content-container { | |
| background-color: #ffffff; | |
| } | |
| </style> | |
| <script> | |
| var chart = null; | |
| let dragNode; | |
| let dropNode; | |
| let dragEnabled = false; | |
| let dragStartX; | |
| let dragStartY; | |
| let isDragStarting = false; | |
| let undoActions = []; | |
| let redoActions = []; | |
| // This is the data used - https://github.com/bumbeishvili/sample-data/blob/main/data-oracle.csv | |
| d3.csv( | |
| 'new.csv', | |
| ).then((data) => { | |
| console.log(data); | |
| chart = new d3.OrgChart() | |
| .nodeHeight((d) => 85 + 25) | |
| .nodeWidth((d) => 220 + 2) | |
| .childrenMargin((d) => 50) | |
| .compactMarginBetween((d) => 35) | |
| .compactMarginPair((d) => 30) | |
| .neighbourMargin((a, b) => 20) | |
| .nodeContent(function (d, i, arr, state) { | |
| return generateContent(d); | |
| }) | |
| .nodeEnter(function (node) { | |
| d3.select(this).call( | |
| d3 | |
| .drag() | |
| .filter(function (x, node) { | |
| return dragEnabled && this.classList.contains('draggable'); | |
| }) | |
| .on('start', function (d, node) { | |
| onDragStart(this, d, node); | |
| }) | |
| .on('drag', function (dragEvent, node) { | |
| onDrag(this, dragEvent); | |
| }) | |
| .on('end', function (d) { | |
| onDragEnd(this, d); | |
| }) | |
| ); | |
| }) | |
| .nodeUpdate(function (d) { | |
| if (d.id === '102' || d.id === '120' || d.id === '124') { | |
| d3.select(this).classed('droppable', false); | |
| } else { | |
| d3.select(this).classed('droppable', true); | |
| } | |
| if (d.id === '101') { | |
| d3.select(this).classed('draggable', false); | |
| } else { | |
| d3.select(this).classed('draggable', true); | |
| } | |
| }) | |
| .container('.chart-container') | |
| .data(data) | |
| .render(); | |
| }); | |
| function onDragStart(element, dragEvent, node) { | |
| dragNode = node; | |
| const width = dragEvent.subject.width; | |
| const half = width / 2; | |
| const x = dragEvent.x - half; | |
| dragStartX = x; | |
| dragStartY = parseFloat(dragEvent.y); | |
| isDragStarting = true; | |
| d3.select(element).classed('dragging', true); | |
| } | |
| function onDrag(element, dragEvent) { | |
| if (!dragNode) { | |
| return; | |
| } | |
| const state = chart.getChartState(); | |
| const g = d3.select(element); | |
| // This condition is designed to run at the start of a drag only | |
| if (isDragStarting) { | |
| isDragStarting = false; | |
| document | |
| .querySelector('.chart-container') | |
| .classList.add('dragging-active'); | |
| // This sets the Z-Index above all other nodes, by moving the dragged node to be the last-child. | |
| g.raise(); | |
| const descendants = dragEvent.subject.descendants(); | |
| const linksToRemove = [...(descendants || []), dragEvent.subject]; | |
| const nodesToRemove = descendants.filter( | |
| (x) => x.data.id !== dragEvent.subject.id | |
| ); | |
| // Remove all links associated with the dragging node | |
| state['linksWrapper'] | |
| .selectAll('path.link') | |
| .data(linksToRemove, (d) => state.nodeId(d)) | |
| .remove(); | |
| // Remove all descendant nodes associated with the dragging node | |
| if (nodesToRemove) { | |
| state['nodesWrapper'] | |
| .selectAll('g.node') | |
| .data(nodesToRemove, (d) => state.nodeId(d)) | |
| .remove(); | |
| } | |
| } | |
| dropNode = null; | |
| const cP = { | |
| width: dragEvent.subject.width, | |
| height: dragEvent.subject.height, | |
| left: dragEvent.x, | |
| right: dragEvent.x + dragEvent.subject.width, | |
| top: dragEvent.y, | |
| bottom: dragEvent.y + dragEvent.subject.height, | |
| midX: dragEvent.x + dragEvent.subject.width / 2, | |
| midY: dragEvent.y + dragEvent.subject.height / 2, | |
| }; | |
| // eslint-disable-next-line @typescript-eslint/no-this-alias | |
| const allNodes = d3.selectAll('g.node:not(.dragging)'); | |
| allNodes.select('rect').attr('fill', 'none'); | |
| allNodes | |
| .filter(function (d2, i) { | |
| const cPInner = { | |
| left: d2.x, | |
| right: d2.x + d2.width, | |
| top: d2.y, | |
| bottom: d2.y + d2.height, | |
| }; | |
| if ( | |
| cP.midX > cPInner.left && | |
| cP.midX < cPInner.right && | |
| cP.midY > cPInner.top && | |
| cP.midY < cPInner.bottom && | |
| this.classList.contains('droppable') | |
| ) { | |
| dropNode = d2; | |
| return d2; | |
| } | |
| }) | |
| .select('rect') | |
| .attr('fill', '#e4e1e1'); | |
| dragStartX += parseFloat(dragEvent.dx); | |
| dragStartY += parseFloat(dragEvent.dy); | |
| g.attr('transform', 'translate(' + dragStartX + ',' + dragStartY + ')'); | |
| } | |
| function onDragEnd(element, dragEvent) { | |
| document | |
| .querySelector('.chart-container') | |
| .classList.remove('dragging-active'); | |
| if (!dragNode) { | |
| return; | |
| } | |
| d3.select(element).classed('dragging', false); | |
| if (!dropNode) { | |
| chart.render(); | |
| return; | |
| } | |
| if (dragEvent.subject.parent.id === dropNode.id) { | |
| chart.render(); | |
| return; | |
| } | |
| d3.select(element).remove(); | |
| const data = chart.getChartState().data; | |
| const node = data?.find((x) => x.id === dragEvent.subject.id); | |
| const oldParentId = node.parentId; | |
| node.parentId = dropNode.id; | |
| redoActions = []; | |
| undoActions.push({ | |
| id: dragEvent.subject.id, | |
| parentId: oldParentId, | |
| }); | |
| dropNode = null; | |
| dragNode = null; | |
| chart.render(); | |
| updateDragActions(); | |
| } | |
| function enableDrag() { | |
| dragEnabled = true; | |
| document.querySelector('.chart-container').classList.add('drag-enabled'); | |
| document.getElementById('enableDragButton').classList.add('hide'); | |
| document.getElementById('dragActions').classList.remove('hide'); | |
| } | |
| function disableDrag() { | |
| dragEnabled = false; | |
| document.querySelector('.chart-container').classList.remove('drag-enabled'); | |
| document.getElementById('enableDragButton').classList.remove('hide'); | |
| document.getElementById('dragActions').classList.add('hide'); | |
| undoActions = []; | |
| redoActions = []; | |
| updateDragActions(); | |
| } | |
| function cancelDrag() { | |
| if (undoActions.length === 0) { | |
| disableDrag(); | |
| return; | |
| } | |
| const data = chart.getChartState().data; | |
| undoActions.reverse().forEach((action) => { | |
| const node = data.find((x) => x.id === action.id); | |
| node.parentId = action.parentId; | |
| }); | |
| disableDrag(); | |
| chart.render(); | |
| } | |
| function undo() { | |
| const action = undoActions.pop(); | |
| if (action) { | |
| const node = chart.getChartState().data.find((x) => x.id === action.id); | |
| const currentParentId = node.parentId; | |
| const previousParentId = action.parentId; | |
| action.parentId = currentParentId; | |
| node.parentId = previousParentId; | |
| redoActions.push(action); | |
| chart.render(); | |
| updateDragActions(); | |
| } | |
| } | |
| function redo() { | |
| const action = redoActions.pop(); | |
| if (action) { | |
| const node = chart.getChartState().data.find((x) => x.id === action.id); | |
| const currentParentId = node.parentId; | |
| const previousParentId = action.parentId; | |
| action.parentId = currentParentId; | |
| node.parentId = previousParentId; | |
| undoActions.push(action); | |
| chart.render(); | |
| updateDragActions(); | |
| } | |
| } | |
| function updateDragActions() { | |
| if (undoActions.length > 0) { | |
| const undoButton = document.getElementById('undoButton'); | |
| undoButton.disabled = false; | |
| } else { | |
| undoButton.disabled = true; | |
| } | |
| if (redoActions.length > 0) { | |
| const redoButton = document.getElementById('redoButton'); | |
| redoButton.disabled = false; | |
| } else { | |
| redoButton.disabled = true; | |
| } | |
| } | |
| function generateContent(d) { | |
| const color = '#FFFFFF'; | |
| const imageDiffVert = 25 + 2; | |
| return ` | |
| <div class="node-container" style=' | |
| width:${d.width}px; | |
| height:${d.height}px; | |
| padding-top:${imageDiffVert - 2}px; | |
| padding-left:1px; | |
| padding-right:1px'> | |
| <div class="content-container" style="font-family: 'Inter', sans-serif; margin-left:-1px;width:${ | |
| d.width - 2 | |
| }px;height:${ | |
| d.height - imageDiffVert | |
| }px;border-radius:10px;border: ${ | |
| d.data._highlighted || d.data._upToTheRootHighlighted | |
| ? '5px solid #E27396"' | |
| : '1px solid #E4E2E9"' | |
| } > | |
| <div style="display:flex;justify-content:flex-end;margin-top:5px;margin-right:8px">#${ | |
| d.data.id | |
| }</div> | |
| <div style="margin-top:${ | |
| -imageDiffVert - 20 | |
| }px;margin-left:${15}px;border-radius:100px;width:50px;height:50px;" ></div> | |
| <div style="margin-top:${ | |
| -imageDiffVert - 20 | |
| }px;"> <img src=" ${ | |
| d.data.image | |
| }" style="margin-left:${20}px;border-radius:100px;width:40px;height:40px;" /></div> | |
| <div style="font-size:15px;color:#08011E;margin-left:20px;margin-top:10px"> ${ | |
| d.data.name | |
| } </div> | |
| <div style="color:#716E7B;margin-left:20px;margin-top:3px;font-size:10px;"> ${ | |
| d.data.position | |
| } </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| </script> | |
| <!-- | |
| This is the code which is used to show of feature , other parts of the code are part of the boilerplate | |
| --> | |
| <button id="enableDragButton" onclick="enableDrag()">Organize</button> | |
| <div id="dragActions" class="hide"> | |
| <button id="finishDrag" onclick="disableDrag()">Done</button> | |
| <button id="undoButton" disabled onclick="undo()">Undo</button> | |
| <button id="redoButton" disabled onclick="redo()">Redo</button> | |
| <button id="cancelDrag" onclick="cancelDrag()">Cancel</button> | |
| </div> | |
| <!-- | |
| End of adding node functionality | |
| --> | |
| <div class="chart-container"></div> | |