From d0472f7a1d856df48aa26475580e3a18b8bc0037 Mon Sep 17 00:00:00 2001
From: MustangDeng <670124965@qq.com>
Date: Thu, 21 Jul 2022 11:51:32 +0800
Subject: [PATCH] =?UTF-8?q?d3=E7=BB=84=E7=BB=87=E6=9E=B6=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/orgChart/index.jsx | 4 +-
src/components/toolBar/index.jsx | 2 +-
src/components/topBar/index.jsx | 5 -
src/d3-org-chart.js | 1612 +++++++++++++++++++++++++++++
src/pages/company.jsx | 4 +-
src/pages/user.jsx | 13 +-
6 files changed, 1627 insertions(+), 13 deletions(-)
create mode 100644 src/d3-org-chart.js
diff --git a/src/components/orgChart/index.jsx b/src/components/orgChart/index.jsx
index bfa43f3..1ffe036 100644
--- a/src/components/orgChart/index.jsx
+++ b/src/components/orgChart/index.jsx
@@ -1,6 +1,7 @@
import React, { useLayoutEffect, useRef, useEffect } from 'react';
import { OrgChart } from 'd3-org-chart';
import * as d3 from 'd3';
+import { message } from "antd";
export const OrgChartComponent = (props, ref) => {
const d3Container = useRef(null);
@@ -30,7 +31,7 @@ export const OrgChartComponent = (props, ref) => {
.data(props.data)
.nodeWidth(props.nodeWidth)
.nodeHeight(props.nodeHeight)
- .layout("left")
+ .layout("top")
.linkUpdate(function(d, i, arr) {
d3.select(this)
.attr("stroke", "#66BAF5")
@@ -54,6 +55,7 @@ export const OrgChartComponent = (props, ref) => {
} catch(err) {
console.log(err);
+ message.warning(err);
}
}
}, [props.data, d3Container.current]);
diff --git a/src/components/toolBar/index.jsx b/src/components/toolBar/index.jsx
index 5de765c..16fd715 100644
--- a/src/components/toolBar/index.jsx
+++ b/src/components/toolBar/index.jsx
@@ -16,7 +16,7 @@ export default class ToolBar extends React.Component {
constructor(props) {
super(props);
this.state = {
- toolActive: "left"
+ toolActive: "top"
}
}
diff --git a/src/components/topBar/index.jsx b/src/components/topBar/index.jsx
index 1de3824..f0dcdb2 100644
--- a/src/components/topBar/index.jsx
+++ b/src/components/topBar/index.jsx
@@ -94,11 +94,6 @@ export class TopBar extends React.Component {
-
-
-
-
-
diff --git a/src/d3-org-chart.js b/src/d3-org-chart.js
new file mode 100644
index 0000000..73644b2
--- /dev/null
+++ b/src/d3-org-chart.js
@@ -0,0 +1,1612 @@
+import { selection, select } 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';
+
+const d3 = {
+ selection,
+ select,
+ max,
+ min,
+ sum,
+ cumsum,
+ tree,
+ stratify,
+ zoom,
+ zoomIdentity,
+ linkHorizontal,
+}
+export class OrgChart {
+ constructor() {
+ // Exposed variables
+ const attrs = {
+ id: `ID${Math.floor(Math.random() * 1000000)}`, // Id for event handlings
+ firstDraw: true,
+ svgWidth: 800,
+ svgHeight: window.innerHeight - 100,
+ scaleExtent:[0.001, 20],
+ container: "body",
+ defaultTextFill: "#2C3E50",
+ defaultFont: "Helvetica",
+ ctx: document.createElement('canvas').getContext('2d'),
+ data: null,
+ duration: 400,
+ setActiveNodeCentered: true,
+ expandLevel: 3,
+ compact: true,
+ rootMargin: 40,
+ nodeDefaultBackground: 'none',
+ connections: [],
+ lastTransform: { x: 0, y: 0, k: 1 },
+ nodeId: d => d.nodeId || d.id,
+ parentNodeId: d => d.parentNodeId || d.parentId,
+ backgroundColor: 'none',
+ zoomBehavior: null,
+ defs: function (state, visibleConnections) {
+ return `
+ ${visibleConnections.map(conn => {
+ const labelWidth = this.getTextWidth(conn.label, { ctx: state.ctx, fontSize: 2, defaultFont: state.defaultFont });
+ return `
+
+
+ ${conn.label || ''}
+
+
+
+
+
+ `}).join("")}
+
+ `},
+ connectionsUpdate: function (d, i, arr) {
+ d3.select(this)
+ .attr("stroke", d => '#152785')
+ .attr('stroke-linecap', 'round')
+ .attr("stroke-width", d => '5')
+ .attr('pointer-events', 'none')
+ .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,
+ linkGroupArc: d3.linkHorizontal().x(d => d.x).y(d => d.y),
+ // ({ source, target }) => {
+ // return
+ // return `M ${source.x} , ${source.y} Q ${(source.x + target.x) / 2 + 100},${source.y-100} ${target.x}, ${target.y}`;
+ // },
+ nodeContent: d => `
Sample Node(id=${d.id}), override using
+
chart
+ .nodeContent({data}=>{
+ return '' // Custom HTML
+ })
+
+ Or check different
layout examples
+
+
`,
+ layout: "top",// top, left,right, bottom
+ buttonContent: ({ node, state }) => {
+ const icons = {
+ "left": d => d ? `‹
` : `›
`,
+ "bottom": d => d ? `ˬ
` : `ˆ
`,
+ "right": d => d ? `›
` : `‹
`,
+ "top": d => d ? `ˆ
` : `ˬ
`,
+ }
+ return ` ${icons[state.layout](node.children)}
`
+ },
+ layoutBindings: {
+ "left": {
+ "nodeLeftX": node => 0,
+ "nodeRightX": node => node.width,
+ "nodeTopY": node => - node.height / 2,
+ "nodeBottomY": node => node.height / 2,
+ "nodeJoinX": node => node.x + node.width,
+ "nodeJoinY": node => node.y - node.height / 2,
+ "linkJoinX": node => node.x + node.width,
+ "linkJoinY": node => node.y,
+ "linkX": node => node.x,
+ "linkY": node => node.y,
+ "linkCompactXStart": node => node.x + node.width / 2,//node.x + (node.compactEven ? node.width / 2 : -node.width / 2),
+ "linkCompactYStart": node => node.y + (node.compactEven ? node.height / 2 : -node.height / 2),
+ "compactLinkMidX": (node, state) => node.firstCompactNode.x,// node.firstCompactNode.x + node.firstCompactNode.flexCompactDim[0] / 4 + state.compactMarginPair(node) / 4,
+ "compactLinkMidY": (node, state) => node.firstCompactNode.y + node.firstCompactNode.flexCompactDim[0] / 4 + state.compactMarginPair(node) / 4,
+ "linkParentX": node => node.parent.x + node.parent.width,
+ "linkParentY": node => node.parent.y,
+ "buttonX": node => node.width,
+ "buttonY": node => node.height / 2,
+ "centerTransform": ({ root, rootMargin, centerY, scale, centerX }) => `translate(${rootMargin},${centerY}) scale(${scale})`,
+ "compactDimension": {
+ sizeColumn: node => node.height,
+ sizeRow: node => node.width,
+ reverse: arr => arr.slice().reverse()
+ },
+ "nodeFlexSize": ({ height, width, siblingsMargin, childrenMargin, state, node }) => {
+ if (state.compact && node.flexCompactDim) {
+ const result = [node.flexCompactDim[0], node.flexCompactDim[1]]
+ return result;
+ };
+ return [height + siblingsMargin, width + childrenMargin]
+ },
+ "zoomTransform": ({ centerY, scale }) => `translate(${0},${centerY}) scale(${scale})`,
+ "diagonal": this.hdiagonal.bind(this),
+ "swap": d => { const x = d.x; d.x = d.y; d.y = x; },
+ "nodeUpdateTransform": ({ x, y, width, height }) => `translate(${x},${y - height / 2})`,
+ },
+ "top": {
+ "nodeLeftX": node => -node.width / 2,
+ "nodeRightX": node => node.width / 2,
+ "nodeTopY": node => 0,
+ "nodeBottomY": node => node.height,
+ "nodeJoinX": node => node.x - node.width / 2,
+ "nodeJoinY": node => node.y + node.height,
+ "linkJoinX": node => node.x,
+ "linkJoinY": node => node.y + node.height,
+ "linkCompactXStart": node => node.x + (node.compactEven ? node.width / 2 : -node.width / 2),
+ "linkCompactYStart": node => node.y + node.height / 2,
+ "compactLinkMidX": (node, state) => node.firstCompactNode.x + node.firstCompactNode.flexCompactDim[0] / 4 + state.compactMarginPair(node) / 4,
+ "compactLinkMidY": node => node.firstCompactNode.y,
+ "compactDimension": {
+ sizeColumn: node => node.width,
+ sizeRow: node => node.height,
+ reverse: arr => arr,
+ },
+ "linkX": node => node.x,
+ "linkY": node => node.y,
+ "linkParentX": node => node.parent.x,
+ "linkParentY": node => node.parent.y + node.parent.height,
+ "buttonX": node => node.width / 2,
+ "buttonY": node => node.height,
+ "centerTransform": ({ root, rootMargin, centerY, scale, centerX }) => `translate(${centerX},${rootMargin}) scale(${scale})`,
+ "nodeFlexSize": ({ height, width, siblingsMargin, childrenMargin, state, node, compactViewIndex }) => {
+ if (state.compact && node.flexCompactDim) {
+ const result = [node.flexCompactDim[0], node.flexCompactDim[1]]
+ return result;
+ };
+ return [width + siblingsMargin, height + childrenMargin];
+ },
+ "zoomTransform": ({ centerX, scale }) => `translate(${centerX},0}) scale(${scale})`,
+ "diagonal": this.diagonal.bind(this),
+ "swap": d => { },
+ "nodeUpdateTransform": ({ x, y, width, height }) => `translate(${x - width / 2},${y})`,
+
+ },
+ "bottom": {
+ "nodeLeftX": node => -node.width / 2,
+ "nodeRightX": node => node.width / 2,
+ "nodeTopY": node => -node.height,
+ "nodeBottomY": node => 0,
+ "nodeJoinX": node => node.x - node.width / 2,
+ "nodeJoinY": node => node.y - node.height - node.height,
+ "linkJoinX": node => node.x,
+ "linkJoinY": node => node.y - node.height,
+ "linkCompactXStart": node => node.x + (node.compactEven ? node.width / 2 : -node.width / 2),
+ "linkCompactYStart": node => node.y - node.height / 2,
+ "compactLinkMidX": (node, state) => node.firstCompactNode.x + node.firstCompactNode.flexCompactDim[0] / 4 + state.compactMarginPair(node) / 4,
+ "compactLinkMidY": node => node.firstCompactNode.y,
+ "linkX": node => node.x,
+ "linkY": node => node.y,
+ "compactDimension": {
+ sizeColumn: node => node.width,
+ sizeRow: node => node.height,
+ reverse: arr => arr,
+ },
+ "linkParentX": node => node.parent.x,
+ "linkParentY": node => node.parent.y - node.parent.height,
+ "buttonX": node => node.width / 2,
+ "buttonY": node => 0,
+ "centerTransform": ({ root, rootMargin, centerY, scale, centerX, chartHeight }) => `translate(${centerX},${chartHeight - rootMargin}) scale(${scale})`,
+ "nodeFlexSize": ({ height, width, siblingsMargin, childrenMargin, state, node }) => {
+ if (state.compact && node.flexCompactDim) {
+ const result = [node.flexCompactDim[0], node.flexCompactDim[1]]
+ return result;
+ };
+ return [width + siblingsMargin, height + childrenMargin]
+ },
+ "zoomTransform": ({ centerX, scale }) => `translate(${centerX},0}) scale(${scale})`,
+ "diagonal": this.diagonal.bind(this),
+ "swap": d => { d.y = -d.y; },
+ "nodeUpdateTransform": ({ x, y, width, height }) => `translate(${x - width / 2},${y - height})`,
+ },
+ "right": {
+ "nodeLeftX": node => -node.width,
+ "nodeRightX": node => 0,
+ "nodeTopY": node => - node.height / 2,
+ "nodeBottomY": node => node.height / 2,
+ "nodeJoinX": node => node.x - node.width - node.width,
+ "nodeJoinY": node => node.y - node.height / 2,
+ "linkJoinX": node => node.x - node.width,
+ "linkJoinY": node => node.y,
+ "linkX": node => node.x,
+ "linkY": node => node.y,
+ "linkParentX": node => node.parent.x - node.parent.width,
+ "linkParentY": node => node.parent.y,
+ "buttonX": node => 0,
+ "buttonY": node => node.height / 2,
+ "linkCompactXStart": node => node.x - node.width / 2,//node.x + (node.compactEven ? node.width / 2 : -node.width / 2),
+ "linkCompactYStart": node => node.y + (node.compactEven ? node.height / 2 : -node.height / 2),
+ "compactLinkMidX": (node, state) => node.firstCompactNode.x,// node.firstCompactNode.x + node.firstCompactNode.flexCompactDim[0] / 4 + state.compactMarginPair(node) / 4,
+ "compactLinkMidY": (node, state) => node.firstCompactNode.y + node.firstCompactNode.flexCompactDim[0] / 4 + state.compactMarginPair(node) / 4,
+ "centerTransform": ({ root, rootMargin, centerY, scale, centerX, chartWidth }) => `translate(${chartWidth - rootMargin},${centerY}) scale(${scale})`,
+ "nodeFlexSize": ({ height, width, siblingsMargin, childrenMargin, state, node }) => {
+ if (state.compact && node.flexCompactDim) {
+ const result = [node.flexCompactDim[0], node.flexCompactDim[1]]
+ return result;
+ };
+ return [height + siblingsMargin, width + childrenMargin]
+ },
+ "compactDimension": {
+ sizeColumn: node => node.height,
+ sizeRow: node => node.width,
+ reverse: arr => arr.slice().reverse()
+ },
+ "zoomTransform": ({ centerY, scale }) => `translate(${0},${centerY}) scale(${scale})`,
+ "diagonal": this.hdiagonal.bind(this),
+ "swap": d => { const x = d.x; d.x = -d.y; d.y = x; },
+ "nodeUpdateTransform": ({ x, y, width, height }) => `translate(${x - width},${y - height / 2})`,
+ },
+ }
+ };
+
+ this.getChartState = () => attrs;
+
+ // Dynamically set getter and setter functions for Chart class
+ Object.keys(attrs).forEach((key) => {
+ //@ts-ignore
+ this[key] = function (_) {
+ if (!arguments.length) {
+ return attrs[key];
+ } else {
+ attrs[key] = _;
+ }
+ return this;
+ };
+ });
+
+ this.initializeEnterExitUpdatePattern();
+ }
+
+ initializeEnterExitUpdatePattern() {
+ d3.selection.prototype.patternify = function (params) {
+ var container = this;
+ var selector = params.selector;
+ var elementTag = params.tag;
+ var data = params.data || [selector];
+
+ // Pattern in action
+ var selection = container.selectAll("." + selector).data(data, (d, i) => {
+ if (typeof d === "object") {
+ if (d.id) { return d.id; }
+ }
+ return i;
+ });
+ selection.exit().remove();
+ selection = selection.enter().append(elementTag).merge(selection);
+ selection.attr("class", selector);
+ return selection;
+ };
+ }
+
+ // This method retrieves passed node's children IDs (including node)
+ getNodeChildren({ data, children, _children }, nodeStore) {
+ // Store current node ID
+ nodeStore.push(data);
+
+ // Loop over children and recursively store descendants id (expanded nodes)
+ if (children) {
+ children.forEach((d) => {
+ this.getNodeChildren(d, nodeStore);
+ });
+ }
+
+ // Loop over _children and recursively store descendants id (collapsed nodes)
+ if (_children) {
+ _children.forEach((d) => {
+ this.getNodeChildren(d, nodeStore);
+ });
+ }
+
+ // Return result
+ return nodeStore;
+ }
+
+ // This method can be invoked via chart.setZoomFactor API, it zooms to particulat scale
+ initialZoom(zoomLevel) {
+ const attrs = this.getChartState();
+ attrs.lastTransform.k = zoomLevel;
+ return this;
+ }
+
+ render() {
+ //InnerFunctions which will update visuals
+ const attrs = this.getChartState();
+ if (!attrs.data || attrs.data.length == 0) {
+ console.log('ORG CHART - Data is empty')
+ return this;
+ }
+
+ //Drawing containers
+ const container = d3.select(attrs.container);
+ const containerRect = container.node().getBoundingClientRect();
+ if (containerRect.width > 0) attrs.svgWidth = containerRect.width;
+
+ //Calculated properties
+ const calc = {
+ id: `ID${Math.floor(Math.random() * 1000000)}`, // id for event handlings,
+ chartWidth: attrs.svgWidth,
+ chartHeight: attrs.svgHeight
+ };
+ attrs.calc = calc;
+
+ // Calculate max node depth (it's needed for layout heights calculation)
+ calc.centerX = calc.chartWidth / 2;
+ calc.centerY = calc.chartHeight / 2;
+
+ // ******************* BEHAVIORS **********************
+ if (attrs.firstDraw) {
+ const behaviors = {
+ zoom: null
+ };
+
+ // Get zooming function
+ behaviors.zoom = d3.zoom().on("zoom", (event, d) => this.zoomed(event, d)).scaleExtent(attrs.scaleExtent)
+ attrs.zoomBehavior = behaviors.zoom;
+ }
+
+ //****************** ROOT node work ************************
+
+ attrs.flexTreeLayout = flextree({
+ nodeSize: node => {
+ const width = attrs.nodeWidth(node);;
+ const height = attrs.nodeHeight(node);
+ const siblingsMargin = attrs.siblingsMargin(node)
+ const childrenMargin = attrs.childrenMargin(node);
+ return attrs.layoutBindings[attrs.layout].nodeFlexSize({
+ state: attrs,
+ node: node,
+ width,
+ height,
+ siblingsMargin,
+ childrenMargin
+ });
+ }
+ })
+ .spacing((nodeA, nodeB) => nodeA.parent == nodeB.parent ? 0 : attrs.neightbourMargin(nodeA, nodeB));
+
+ this.setLayouts({ expandNodesFirst: false });
+
+ // ************************* DRAWING **************************
+ //Add svg
+ const svg = container
+ .patternify({
+ tag: "svg",
+ selector: "svg-chart-container"
+ })
+ .style('background-color', attrs.backgroundColor)
+ .attr("width", attrs.svgWidth)
+ .attr("height", attrs.svgHeight)
+ .attr("font-family", attrs.defaultFont)
+
+ if (attrs.firstDraw) {
+ svg.call(attrs.zoomBehavior)
+ .on("dblclick.zoom", null)
+ .attr("cursor", "move")
+ }
+
+ attrs.svg = svg;
+
+ //Add container g element
+ const chart = svg
+ .patternify({
+ tag: "g",
+ selector: "chart"
+ })
+
+ // Add one more container g element, for better positioning controls
+ attrs.centerG = chart
+ .patternify({
+ tag: "g",
+ selector: "center-group"
+ })
+
+ attrs.linksWrapper = attrs.centerG.patternify({
+ tag: "g",
+ selector: "links-wrapper"
+ })
+
+ attrs.nodesWrapper = attrs.centerG.patternify({
+ tag: "g",
+ selector: "nodes-wrapper"
+ })
+
+ attrs.connectionsWrapper = attrs.centerG.patternify({
+ tag: "g",
+ selector: "connections-wrapper"
+ })
+
+ attrs.defsWrapper = svg.patternify({
+ tag: "g",
+ selector: "defs-wrapper"
+ })
+
+ if (attrs.firstDraw) {
+ attrs.centerG.attr("transform", () => {
+ return attrs.layoutBindings[attrs.layout].centerTransform({
+ centerX: calc.centerX,
+ centerY: calc.centerY,
+ scale: attrs.lastTransform.k,
+ rootMargin: attrs.rootMargin,
+ root: attrs.root,
+ chartHeight: calc.chartHeight,
+ chartWidth: calc.chartWidth
+ })
+ });
+ }
+
+ attrs.chart = chart;
+
+ // Display tree contenrs
+ this.update(attrs.root);
+
+
+ //######################################### UTIL FUNCS ##################################
+ // This function restyles foreign object elements ()
+
+ d3.select(window).on(`resize.${attrs.id}`, () => {
+ const containerRect = d3.select(attrs.container).node().getBoundingClientRect();
+ attrs.svg.attr('width', containerRect.width)
+ });
+
+ if (attrs.firstDraw) {
+ attrs.firstDraw = false;
+ }
+
+ 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();
+ const nodeFound = attrs.allNodes.filter(({ data }) => attrs.nodeId(data) === attrs.nodeId(obj))[0];
+ const parentFound = attrs.allNodes.filter(({ data }) => attrs.nodeId(data) === attrs.parentNodeId(obj))[0];
+ if (nodeFound) {
+ console.log(`ORG CHART - ADD - Node with id "${attrs.nodeId(obj)}" already exists in tree`)
+ return this;
+ }
+ if (!parentFound) {
+ console.log(`ORG CHART - ADD - Parent node with id "${attrs.parentNodeId(obj)}" not found in the tree`)
+ return this;
+ }
+ if (obj._centered && !obj._expanded) obj._expanded = true;
+ attrs.data.push(obj);
+
+ // Update state of nodes and redraw graph
+ this.updateNodesState();
+
+ return this;
+ }
+
+ addAyncNode(obj) {
+ const attrs = this.getChartState();
+ const nodeFound = attrs.allNodes.filter(({ data }) => attrs.nodeId(data) === attrs.nodeId(obj))[0];
+ const parentFound = attrs.allNodes.filter(({ data }) => attrs.nodeId(data) === attrs.parentNodeId(obj))[0];
+ if (nodeFound) {
+ console.log(`ORG CHART - ADD - Node with id "${attrs.nodeId(obj)}" already exists in tree`)
+ return this;
+ }
+ if (!parentFound) {
+ console.log(`ORG CHART - ADD - Parent node with id "${attrs.parentNodeId(obj)}" not found in the tree`)
+ return this;
+ }
+ obj._expanded = true;
+ attrs.data.push(obj);
+
+ // Update state of nodes and redraw graph
+ this.updateNodesState();
+
+ return this;
+ }
+
+ // This function can be invoked via chart.removeNode API, and it removes node from tree at runtime
+ removeNode(nodeId) {
+ const attrs = this.getChartState();
+ const node = attrs.allNodes.filter(({ data }) => attrs.nodeId(data) == nodeId)[0];
+ if (!node) {
+ console.log(`ORG CHART - REMOVE - Node with id "${nodeId}" not found in the tree`);
+ return this;
+ }
+
+ // Remove all node childs
+ // Retrieve all children nodes ids (including current node itself)
+ node.descendants()
+ .forEach(d => d.data._filteredOut = true)
+
+ const descendants = this.getNodeChildren(node, [], attrs.nodeId);
+ descendants.forEach(d => d._filtered = true)
+
+ // Filter out retrieved nodes and reassign data
+ attrs.data = attrs.data.filter(d => !d._filtered);
+
+ const updateNodesState = this.updateNodesState.bind(this);
+ // Update state of nodes and redraw graph
+ updateNodesState();
+
+ return this;
+ }
+
+ groupBy(array, accessor, aggegator) {
+ const grouped = {}
+ array.forEach(item => {
+ const key = accessor(item)
+ if (!grouped[key]) {
+ grouped[key] = []
+ }
+ grouped[key].push(item)
+ })
+
+ Object.keys(grouped).forEach(key => {
+ grouped[key] = aggegator(grouped[key])
+ })
+ return Object.entries(grouped);
+ }
+ calculateCompactFlexDimensions(root) {
+ const attrs = this.getChartState();
+ root.eachBefore(node => {
+ node.firstCompact = null;
+ node.compactEven = null;
+ node.flexCompactDim = null;
+ node.firstCompactNode = null;
+ })
+ root.eachBefore(node => {
+ if (node.children && node.children.length > 1) {
+ const compactChildren = node.children.filter(d => !d.children);
+ if (compactChildren.length < 2) return;
+ compactChildren.forEach((child, i) => {
+ if (!i) child.firstCompact = true;
+ if (i % 2) child.compactEven = false;
+ else child.compactEven = true;
+ child.row = Math.floor(i / 2);
+ })
+ const evenMaxColumnDimension = d3.max(compactChildren.filter(d => d.compactEven), attrs.layoutBindings[attrs.layout].compactDimension.sizeColumn);
+ const oddMaxColumnDimension = d3.max(compactChildren.filter(d => !d.compactEven), attrs.layoutBindings[attrs.layout].compactDimension.sizeColumn);
+ const columnSize = Math.max(evenMaxColumnDimension, oddMaxColumnDimension) * 2;
+ const rowsMapNew = this.groupBy(compactChildren, d => d.row, reducedGroup => d3.max(reducedGroup, d => attrs.layoutBindings[attrs.layout].compactDimension.sizeRow(d) + attrs.compactMarginBetween(d)));
+ const rowSize = d3.sum(rowsMapNew.map(v => v[1]))
+ compactChildren.forEach(node => {
+ node.firstCompactNode = compactChildren[0];
+ if (node.firstCompact) {
+ node.flexCompactDim = [
+ columnSize + attrs.compactMarginPair(node),
+ rowSize - attrs.compactMarginBetween(node)
+ ];
+ } else {
+ node.flexCompactDim = [0, 0];
+ }
+ })
+ node.flexCompactDim = null;
+ }
+ })
+ }
+
+ calculateCompactFlexPositions(root) {
+ const attrs = this.getChartState();
+ root.eachBefore(node => {
+ if (node.children) {
+ const compactChildren = node.children.filter(d => d.flexCompactDim);
+ const fch = compactChildren[0];
+ if (!fch) return;
+ compactChildren.forEach((child, i, arr) => {
+ if (i == 0) fch.x -= fch.flexCompactDim[0] / 2;
+ if (i & i % 2 - 1) child.x = fch.x + fch.flexCompactDim[0] * 0.25 - attrs.compactMarginPair(child) / 4;
+ else if (i) child.x = fch.x + fch.flexCompactDim[0] * 0.75 + attrs.compactMarginPair(child) / 4;
+ })
+ const centerX = fch.x + fch.flexCompactDim[0] * 0.5;
+ fch.x = fch.x + fch.flexCompactDim[0] * 0.25 - attrs.compactMarginPair(fch) / 4;
+ const offsetX = node.x - centerX;
+ if (Math.abs(offsetX) < 10) {
+ compactChildren.forEach(d => d.x += offsetX);
+ }
+
+ const rowsMapNew = this.groupBy(compactChildren, d => d.row, reducedGroup => d3.max(reducedGroup, d => attrs.layoutBindings[attrs.layout].compactDimension.sizeRow(d)));
+ const cumSum = d3.cumsum(rowsMapNew.map(d => d[1] + attrs.compactMarginBetween(d)));
+ compactChildren
+ .forEach((node, i) => {
+ if (node.row) {
+ node.y = fch.y + cumSum[node.row - 1]
+ } else {
+ node.y = fch.y;
+ }
+ })
+
+ }
+ })
+ }
+
+ // This function basically redraws visible graph, based on nodes state
+ update({ x0, y0, x = 0, y = 0, width, height }) {
+ const attrs = this.getChartState();
+ const calc = attrs.calc;
+
+
+ if (attrs.compact) {
+ this.calculateCompactFlexDimensions(attrs.root);
+ }
+
+ // Assigns the x and y position for the nodes
+ const treeData = attrs.flexTreeLayout(attrs.root);
+
+ // Reassigns the x and y position for the based on the compact layout
+ if (attrs.compact) {
+ this.calculateCompactFlexPositions(attrs.root);
+ }
+
+ const nodes = treeData.descendants();
+
+ // console.table(nodes.map(d => ({ x: d.x, y: d.y, width: d.width, height: d.height, flexCompactDim: d.flexCompactDim + "" })))
+
+ // Get all links
+ const links = treeData.descendants().slice(1);
+ nodes.forEach(attrs.layoutBindings[attrs.layout].swap)
+
+ // Connections
+ const connections = attrs.connections;
+ const allNodesMap = {};
+ attrs.allNodes.forEach(d => allNodesMap[attrs.nodeId(d.data)] = d);
+
+ const visibleNodesMap = {}
+ nodes.forEach(d => visibleNodesMap[attrs.nodeId(d.data)] = d);
+
+ connections.forEach(connection => {
+ const source = allNodesMap[connection.from];
+ const target = allNodesMap[connection.to];
+ connection._source = source;
+ connection._target = target;
+ })
+ const visibleConnections = connections.filter(d => visibleNodesMap[d.from] && visibleNodesMap[d.to]);
+ const defsString = attrs.defs.bind(this)(attrs, visibleConnections);
+ const existingString = attrs.defsWrapper.html();
+ if (defsString !== existingString) {
+ attrs.defsWrapper.html(defsString)
+ }
+
+ // -------------------------- LINKS ----------------------
+ // Get links selection
+ const linkSelection = attrs.linksWrapper
+ .selectAll("path.link")
+ .data(links, (d) => attrs.nodeId(d.data));
+
+ // Enter any new links at the parent's previous position.
+ const linkEnter = linkSelection
+ .enter()
+ .insert("path", "g")
+ .attr("class", "link")
+ .attr("d", (d) => {
+ const xo = attrs.layoutBindings[attrs.layout].linkJoinX({ x: x0, y: y0, width, height });
+ const yo = attrs.layoutBindings[attrs.layout].linkJoinY({ x: x0, y: y0, width, height });
+ const o = { x: xo, y: yo };
+ return attrs.layoutBindings[attrs.layout].diagonal(o, o, o);
+ });
+
+ // Get links update selection
+ const linkUpdate = linkEnter.merge(linkSelection);
+
+ // Styling links
+ linkUpdate
+ .attr("fill", "none")
+
+ // Allow external modifications
+ linkUpdate.each(attrs.linkUpdate);
+
+ // Transition back to the parent element position
+ linkUpdate
+ .transition()
+ .duration(attrs.duration)
+ .attr("d", (d) => {
+ const n = attrs.compact && d.flexCompactDim ?
+ {
+ x: attrs.layoutBindings[attrs.layout].compactLinkMidX(d, attrs),
+ y: attrs.layoutBindings[attrs.layout].compactLinkMidY(d, attrs)
+ } :
+ {
+ x: attrs.layoutBindings[attrs.layout].linkX(d),
+ y: attrs.layoutBindings[attrs.layout].linkY(d)
+ };
+
+ const p = {
+ x: attrs.layoutBindings[attrs.layout].linkParentX(d),
+ y: attrs.layoutBindings[attrs.layout].linkParentY(d),
+ };
+
+ const m = attrs.compact && d.flexCompactDim ? {
+ x: attrs.layoutBindings[attrs.layout].linkCompactXStart(d),
+ y: attrs.layoutBindings[attrs.layout].linkCompactYStart(d),
+ } : n;
+ return attrs.layoutBindings[attrs.layout].diagonal(n, p, m);
+ });
+
+ // Remove any links which is exiting after animation
+ const linkExit = linkSelection
+ .exit()
+ .transition()
+ .duration(attrs.duration)
+ .attr("d", (d) => {
+ const xo = attrs.layoutBindings[attrs.layout].linkJoinX({ x, y, width, height });
+ const yo = attrs.layoutBindings[attrs.layout].linkJoinY({ x, y, width, height });
+ const o = { x: xo, y: yo };
+ return attrs.layoutBindings[attrs.layout].diagonal(o, o);
+ })
+ .remove();
+
+
+ // -------------------------- CONNECTIONS ----------------------
+
+ const connectionsSel = attrs.connectionsWrapper
+ .selectAll("path.connection")
+ .data(visibleConnections)
+
+ // Enter any new connections at the parent's previous position.
+ const connEnter = connectionsSel
+ .enter()
+ .insert("path", "g")
+ .attr("class", "connection")
+ .attr("d", (d) => {
+ const xo = attrs.layoutBindings[attrs.layout].linkJoinX({ x: x0, y: y0, width, height });
+ const yo = attrs.layoutBindings[attrs.layout].linkJoinY({ x: x0, y: y0, width, height });
+ const o = { x: xo, y: yo };
+ return attrs.layoutBindings[attrs.layout].diagonal(o, o);
+ });
+
+
+ // Get connections update selection
+ const connUpdate = connEnter.merge(connectionsSel);
+
+ // Styling connections
+ connUpdate.attr("fill", "none")
+
+ // Transition back to the parent element position
+ connUpdate
+ .transition()
+ .duration(attrs.duration)
+ .attr('d', (d) => {
+ const xs = attrs.layoutBindings[attrs.layout].linkX({ x: d._source.x, y: d._source.y, width: d._source.width, height: d._source.height });
+ const ys = attrs.layoutBindings[attrs.layout].linkY({ x: d._source.x, y: d._source.y, width: d._source.width, height: d._source.height });
+ const xt = attrs.layoutBindings[attrs.layout].linkJoinX({ x: d._target.x, y: d._target.y, width: d._target.width, height: d._target.height });
+ const yt = attrs.layoutBindings[attrs.layout].linkJoinY({ x: d._target.x, y: d._target.y, width: d._target.width, height: d._target.height });
+ return attrs.linkGroupArc({ source: { x: xs, y: ys }, target: { x: xt, y: yt } })
+ })
+
+ // Allow external modifications
+ connUpdate.each(attrs.connectionsUpdate);
+
+ // Remove any links which is exiting after animation
+ const connExit = connectionsSel
+ .exit()
+ .transition()
+ .duration(attrs.duration)
+ .attr('opacity', 0)
+ .remove();
+
+ // -------------------------- NODES ----------------------
+ // Get nodes selection
+ const nodesSelection = attrs.nodesWrapper
+ .selectAll("g.node")
+ .data(nodes, ({ data }) => attrs.nodeId(data));
+
+ // Enter any new nodes at the parent's previous position.
+ const nodeEnter = nodesSelection
+ .enter()
+ .append("g")
+ .attr("class", "node")
+ .attr("transform", (d) => {
+ if (d == attrs.root) return `translate(${x0},${y0})`
+ const xj = attrs.layoutBindings[attrs.layout].nodeJoinX({ x: x0, y: y0, width, height });
+ const yj = attrs.layoutBindings[attrs.layout].nodeJoinY({ x: x0, y: y0, width, height });
+ return `translate(${xj},${yj})`
+ })
+ .attr("cursor", "pointer")
+ .on("click", (event, { data }) => {
+ if ([...event.srcElement.classList].includes("node-button-foreign-object")) {
+ return;
+ }
+ attrs.onNodeClick(attrs.nodeId(data));
+ });
+
+ // Add background rectangle for the nodes
+ nodeEnter
+ .patternify({
+ tag: "rect",
+ selector: "node-rect",
+ data: (d) => [d]
+ })
+
+ // Node update styles
+ const nodeUpdate = nodeEnter
+ .merge(nodesSelection)
+ .style("font", "12px sans-serif");
+
+ // Add foreignObject element inside rectangle
+ const fo = nodeUpdate.patternify({
+ tag: "foreignObject",
+ selector: "node-foreign-object",
+ data: (d) => [d]
+ })
+ .style('overflow', 'visible')
+
+ // Add foreign object
+ fo.patternify({
+ tag: "xhtml:div",
+ selector: "node-foreign-object-div",
+ data: (d) => [d]
+ })
+
+ this.restyleForeignObjectElements();
+
+ // Add Node button circle's group (expand-collapse button)
+ const nodeButtonGroups = nodeEnter
+ .patternify({
+ tag: "g",
+ selector: "node-button-g",
+ data: (d) => [d]
+ })
+ .on("click", (event, d) => this.onButtonClick(event, d));
+
+ nodeButtonGroups.patternify({
+ tag: 'rect',
+ selector: 'node-button-rect',
+ data: (d) => [d]
+ })
+ .attr('opacity', 0)
+ .attr('pointer-events', 'all')
+ .attr('width', 40)
+ .attr('height', 40)
+ .attr('x', -20)
+ .attr('y', -20)
+
+ // Add expand collapse button content
+ const nodeFo = nodeButtonGroups
+ .patternify({
+ tag: "foreignObject",
+ selector: "node-button-foreign-object",
+ data: (d) => [d]
+ })
+ .attr('width', 40)
+ .attr('height', 40)
+ .attr('x', -20)
+ .attr('y', -20)
+ .style('overflow', 'visible')
+ .patternify({
+ tag: "xhtml:div",
+ selector: "node-button-div",
+ data: (d) => [d]
+ })
+ .style('pointer-events', 'none')
+ .style('display', 'flex')
+ .style('width', '100%')
+ .style('height', '100%')
+
+
+
+ // Transition to the proper position for the node
+ nodeUpdate
+ .transition()
+ .attr("opacity", 0)
+ .duration(attrs.duration)
+ .attr("transform", ({ x, y, width, height }) => {
+ return attrs.layoutBindings[attrs.layout].nodeUpdateTransform({ x, y, width, height });
+
+ })
+ .attr("opacity", 1);
+
+ // Style node rectangles
+ nodeUpdate
+ .select(".node-rect")
+ .attr("width", ({ width }) => width)
+ .attr("height", ({ height }) => height)
+ .attr("x", ({ width }) => 0)
+ .attr("y", ({ height }) => 0)
+ .attr("cursor", "pointer")
+ .attr('rx', 3)
+ .attr("fill", attrs.nodeDefaultBackground)
+
+ // Move node button group to the desired position
+ nodeUpdate
+ .select(".node-button-g")
+ .attr("transform", ({ data, width, height }) => {
+ const x = attrs.layoutBindings[attrs.layout].buttonX({ width, height });
+ const y = attrs.layoutBindings[attrs.layout].buttonY({ width, height });
+ return `translate(${x},${y})`
+ })
+ .attr("display", ({ data }) => {
+ return data._directSubordinates > 0 ? null : 'none';
+ })
+ .attr("opacity", ({ children, _children }) => {
+ if (children || _children) {
+ return 1;
+ }
+ return 0;
+ });
+
+ // Restyle node button circle
+ nodeUpdate
+ .select(".node-button-foreign-object .node-button-div")
+ .html((node) => {
+ return attrs.buttonContent({ node, state: attrs })
+ })
+
+ // Restyle button texts
+ nodeUpdate
+ .select(".node-button-text")
+ .attr("text-anchor", "middle")
+ .attr("alignment-baseline", "middle")
+ .attr("fill", attrs.defaultTextFill)
+ .attr("font-size", ({ children }) => {
+ if (children) return 40;
+ return 26;
+ })
+ .text(({ children }) => {
+ if (children) return "-";
+ return "+";
+ })
+ .attr("y", this.isEdge() ? 10 : 0);
+
+ nodeUpdate.each(attrs.nodeUpdate)
+
+ // Remove any exiting nodes after transition
+ const nodeExitTransition = nodesSelection
+ .exit()
+ .attr("opacity", 1)
+ .transition()
+ .duration(attrs.duration)
+ .attr("transform", (d) => {
+ const ex = attrs.layoutBindings[attrs.layout].nodeJoinX({ x, y, width, height });
+ const ey = attrs.layoutBindings[attrs.layout].nodeJoinY({ x, y, width, height });
+ return `translate(${ex},${ey})`
+ })
+ .on("end", function () {
+ d3.select(this).remove();
+ })
+ .attr("opacity", 0);
+
+ // Store the old positions for transition.
+ nodes.forEach((d) => {
+ d.x0 = d.x;
+ d.y0 = d.y;
+ });
+
+ // CHECK FOR CENTERING
+ const centeredNode = attrs.allNodes.filter(d => d.data._centered)[0]
+ if (centeredNode) {
+ const centeredNodes = centeredNode.data._centeredWithDescendants ? centeredNode.descendants().filter((d, i) => i < 7) : [centeredNode]
+ centeredNode.data._centeredWithDescendants = null;
+ centeredNode.data._centered = null;
+ this.fit({
+ animate: true,
+ scale: false,
+ nodes: centeredNodes
+ })
+ }
+
+ }
+
+ // This function detects whether current browser is edge
+ isEdge() {
+ return window.navigator.userAgent.includes("Edge");
+ }
+
+ // Generate horizontal diagonal - play with it here - https://observablehq.com/@bumbeishvili/curved-edges-horizontal-d3-v3-v4-v5-v6
+ hdiagonal(s, t, m) {
+ // Define source and target x,y coordinates
+ const x = s.x;
+ const y = s.y;
+ const ex = t.x;
+ const ey = t.y;
+
+ let mx = m && m.x || x;
+ let my = m && m.y || y;
+
+ // Values in case of top reversed and left reversed diagonals
+ let xrvs = ex - x < 0 ? -1 : 1;
+ let yrvs = ey - y < 0 ? -1 : 1;
+
+ // Define preferred curve radius
+ let rdef = 35;
+
+ // Reduce curve radius, if source-target x space is smaller
+ let r = Math.abs(ex - x) / 2 < rdef ? Math.abs(ex - x) / 2 : rdef;
+
+ // Further reduce curve radius, is y space is more small
+ r = Math.abs(ey - y) / 2 < r ? Math.abs(ey - y) / 2 : r;
+
+ // Defin width and height of link, excluding radius
+ let h = Math.abs(ey - y) / 2 - r;
+ let w = Math.abs(ex - x) / 2 - r;
+
+ // Build and return custom arc command
+ return `
+ M ${mx} ${my}
+ L ${mx} ${y}
+ L ${x} ${y}
+ L ${x + w * xrvs} ${y}
+ C ${x + w * xrvs + r * xrvs} ${y}
+ ${x + w * xrvs + r * xrvs} ${y}
+ ${x + w * xrvs + r * xrvs} ${y + r * yrvs}
+ L ${x + w * xrvs + r * xrvs} ${ey - r * yrvs}
+ C ${x + w * xrvs + r * xrvs} ${ey}
+ ${x + w * xrvs + r * xrvs} ${ey}
+ ${ex - w * xrvs} ${ey}
+ L ${ex} ${ey}
+ `;
+ }
+
+ // Generate custom diagonal - play with it here - https://observablehq.com/@bumbeishvili/curved-edges
+ diagonal(s, t, m) {
+ const x = s.x;
+ const y = s.y;
+ const ex = t.x;
+ const ey = t.y;
+
+ let mx = m && m.x || x;
+ let my = m && m.y || y;
+
+ let xrvs = ex - x < 0 ? -1 : 1;
+ let yrvs = ey - y < 0 ? -1 : 1;
+
+ let rdef = 35;
+ let r = Math.abs(ex - x) / 2 < rdef ? Math.abs(ex - x) / 2 : rdef;
+
+ r = Math.abs(ey - y) / 2 < r ? Math.abs(ey - y) / 2 : r;
+
+ let h = Math.abs(ey - y) / 2 - r;
+ let w = Math.abs(ex - x) - r * 2;
+ //w=0;
+ const path = `
+ M ${mx} ${my}
+ L ${x} ${my}
+ L ${x} ${y}
+ L ${x} ${y + h * yrvs}
+ C ${x} ${y + h * yrvs + r * yrvs} ${x} ${y + h * yrvs + r * yrvs
+ } ${x + r * xrvs} ${y + h * yrvs + r * yrvs}
+ L ${x + w * xrvs + r * xrvs} ${y + h * yrvs + r * yrvs}
+ C ${ex} ${y + h * yrvs + r * yrvs} ${ex} ${y + h * yrvs + r * yrvs
+ } ${ex} ${ey - h * yrvs}
+ L ${ex} ${ey}
+ `;
+ return path;
+ }
+
+ restyleForeignObjectElements() {
+ const attrs = this.getChartState();
+
+ attrs.svg
+ .selectAll(".node-foreign-object")
+ .attr("width", ({ width }) => width)
+ .attr("height", ({ height }) => height)
+ .attr("x", ({ width }) => 0)
+ .attr("y", ({ height }) => 0);
+ attrs.svg
+ .selectAll(".node-foreign-object-div")
+ .style("width", ({ width }) => `${width}px`)
+ .style("height", ({ height }) => `${height}px`)
+ .html(function (d, i, arr) { return attrs.nodeContent.bind(this)(d, i, arr, attrs) })
+ }
+
+ // Toggle children on click.
+ onButtonClick(event, d) {
+ const attrs = this.getChartState();
+ if (attrs.setActiveNodeCentered) {
+ d.data._centered = true;
+ d.data._centeredWithDescendants = true;
+ }
+
+ // If childrens are expanded
+ if (d.children) {
+ //Collapse them
+ d._children = d.children;
+ d.children = null;
+
+ // Set descendants expanded property to false
+ this.setExpansionFlagToChildren(d, false);
+ } else {
+ // Expand children
+ d.children = d._children;
+ d._children = null;
+
+ // Set each children as expanded
+ if (d.children) {
+ d.children.forEach(({ data }) => (data._expanded = true));
+ }
+ }
+
+ // Redraw Graph
+ this.update(d);
+ }
+
+ // This function changes `expanded` property to descendants
+ setExpansionFlagToChildren({ data, children, _children }, flag) {
+ // Set flag to the current property
+ data._expanded = flag;
+
+ // Loop over and recursively update expanded children's descendants
+ if (children) {
+ children.forEach((d) => {
+ this.setExpansionFlagToChildren(d, flag);
+ });
+ }
+
+ // Loop over and recursively update collapsed children's descendants
+ if (_children) {
+ _children.forEach((d) => {
+ this.setExpansionFlagToChildren(d, flag);
+ });
+ }
+ }
+
+
+ // Method which only expands nodes, which have property set "expanded=true"
+ expandSomeNodes(d) {
+ // If node has expanded property set
+ if (d.data._expanded) {
+ // Retrieve node's parent
+ let parent = d.parent;
+
+ // While we can go up
+ while (parent) {
+ // Expand all current parent's children
+ if (parent._children) {
+ parent.children = parent._children;
+ }
+
+ // Replace current parent holding object
+ parent = parent.parent;
+ }
+ }
+
+ // Recursivelly do the same for collapsed nodes
+ if (d._children) {
+ d._children.forEach((ch) => this.expandSomeNodes(ch));
+ }
+
+ // Recursivelly do the same for expanded nodes
+ if (d.children) {
+ d.children.forEach((ch) => this.expandSomeNodes(ch));
+ }
+ }
+
+ // This function updates nodes state and redraws graph, usually after data change
+ updateNodesState() {
+ const attrs = this.getChartState();
+
+ this.setLayouts({ expandNodesFirst: true });
+
+ // Redraw Graphs
+ this.update(attrs.root);
+ }
+
+ setLayouts({ expandNodesFirst = true }) {
+ const attrs = this.getChartState();
+ // Store new root by converting flat data to hierarchy
+ attrs.root = d3
+ .stratify()
+ .id((d) => attrs.nodeId(d))
+ .parentId(d => attrs.parentNodeId(d))(attrs.data);
+
+ attrs.root.each((node, i, arr) => {
+ let width = attrs.nodeWidth(node);
+ let height = attrs.nodeHeight(node);
+ Object.assign(node, { width, height })
+ })
+
+ // Store positions, where children appear during their enter animation
+ attrs.root.x0 = 0;
+ attrs.root.y0 = 0;
+ attrs.allNodes = attrs.root.descendants();
+
+ // Store direct and total descendants count
+ attrs.allNodes.forEach((d) => {
+ Object.assign(d.data, {
+ _directSubordinates: d.children ? d.children.length : 0,
+ _totalSubordinates: d.descendants().length - 1
+ });
+ });
+
+ if (attrs.root.children) {
+ if (expandNodesFirst) {
+ // Expand all nodes first
+ attrs.root.children.forEach(this.expand)
+ attrs.root.children.forEach(item => {
+ item.children.forEach(this.expand)
+ })
+ }
+ // Then collapse them all
+ attrs.root.children.forEach((d) => this.collapse(d));
+
+ // Collapse root if level is 0
+ if (attrs.expandLevel == 0) {
+ attrs.root._children = attrs.root.children;
+ attrs.root.children = null;
+ }
+
+ // Then only expand nodes, which have expanded proprty set to true
+ [attrs.root].forEach((ch) => this.expandSomeNodes(ch));
+ }
+ }
+
+ // Function which collapses passed node and it's descendants
+ collapse(d) {
+ if (d.children) {
+ d._children = d.children;
+ d._children.forEach((ch) => this.collapse(ch));
+ d.children = null;
+ }
+ }
+
+ // Function which expands passed node and it's descendants
+ expand(d) {
+ if (d._children) {
+ d.children = d._children;
+ d.children.forEach((ch) => this.expand(ch));
+ d._children = null;
+ }
+ }
+
+ // Zoom handler function
+ zoomed(event, d) {
+ const attrs = this.getChartState();
+ const chart = attrs.chart;
+
+ // Get d3 event's transform object
+ const transform = event.transform;
+
+ // Store it
+ attrs.lastTransform = transform;
+
+ // Reposition and rescale chart accordingly
+ chart.attr("transform", transform);
+
+ // Apply new styles to the foreign object element
+ if (this.isEdge()) {
+ this.restyleForeignObjectElements();
+ }
+ }
+
+ zoomTreeBounds({ x0, x1, y0, y1, params = { animate: true, scale: true } }) {
+ const { centerG, svgWidth: w, svgHeight: h, svg, zoomBehavior, duration, lastTransform } = this.getChartState()
+ let scaleVal = Math.min(8, 0.9 / Math.max((x1 - x0) / w, (y1 - y0) / h));
+ let identity = d3.zoomIdentity.translate(w / 2, h / 2)
+ identity = identity.scale(params.scale ? scaleVal : lastTransform.k)
+
+ identity = identity.translate(-(x0 + x1) / 2, -(y0 + y1) / 2);
+ // Transition zoom wrapper component into specified bounds
+ svg.transition().duration(params.animate ? duration : 0).call(zoomBehavior.transform, identity);
+ centerG.transition().duration(params.animate ? duration : 0).attr('transform', 'translate(0,0)')
+ }
+
+ fit({ animate = true, nodes, scale = true } = {}) {
+ const attrs = this.getChartState();
+ const { root } = attrs;
+ let descendants = nodes ? nodes : root.descendants();
+ const minX = d3.min(descendants, d => d.x + attrs.layoutBindings[attrs.layout].nodeLeftX(d))
+ const maxX = d3.max(descendants, d => d.x + attrs.layoutBindings[attrs.layout].nodeRightX(d))
+ const minY = d3.min(descendants, d => d.y + attrs.layoutBindings[attrs.layout].nodeTopY(d))
+ const maxY = d3.max(descendants, d => d.y + attrs.layoutBindings[attrs.layout].nodeBottomY(d))
+
+ this.zoomTreeBounds({
+ params: { animate: animate, scale },
+ x0: minX - 50,
+ x1: maxX + 50,
+ y0: minY - 50,
+ y1: maxY + 50,
+ });
+ return this;
+ }
+
+ // This function can be invoked via chart.setExpanded API, it expands or collapses particular node
+ setExpanded(id, expandedFlag = true) {
+
+ const attrs = this.getChartState();
+ // Retrieve node by node Id
+ const node = attrs.allNodes.filter(({ data }) => attrs.nodeId(data) == id)[0];
+
+ if (!node) {
+ console.log(`ORG CHART - ${expandedFlag ? "EXPAND" : "COLLAPSE"} - Node with id (${id}) not found in the tree`)
+ return this;
+ }
+ node.data._expanded = expandedFlag;
+ return this;
+ }
+
+ setCentered(nodeId) {
+ const attrs = this.getChartState();
+ // this.setExpanded(nodeId)
+ const node = attrs.allNodes.filter(d => attrs.nodeId(d.data) === nodeId)[0];
+ if (!node) {
+ console.log(`ORG CHART - CENTER - Node with id (${nodeId}) not found in the tree`)
+ return this;
+ }
+ node.data._centered = true;
+ node.data._expanded = true;
+ return this;
+ }
+
+ setHighlighted(nodeId) {
+ const attrs = this.getChartState();
+ const node = attrs.allNodes.filter(d => attrs.nodeId(d.data) === nodeId)[0];
+ if (!node) {
+ console.log(`ORG CHART - HIGHLIGHT - Node with id (${nodeId}) not found in the tree`);
+ return this
+ }
+ node.data._highlighted = true;
+ node.data._expanded = true;
+ node.data._centered = true;
+ return this;
+ }
+
+ setUpToTheRootHighlighted(nodeId) {
+ const attrs = this.getChartState();
+ const node = attrs.allNodes.filter(d => attrs.nodeId(d.data) === nodeId)[0];
+ if (!node) {
+ console.log(`ORG CHART - HIGHLIGHTROOT - Node with id (${nodeId}) not found in the tree`)
+ return this;
+ }
+ node.data._upToTheRootHighlighted = true;
+ node.data._expanded = true;
+ node.ancestors().forEach(d => d.data._upToTheRootHighlighted = true)
+ return this;
+ }
+
+ clearHighlighting() {
+ const attrs = this.getChartState();
+ attrs.allNodes.forEach(d => {
+ d.data._highlighted = false;
+ d.data._upToTheRootHighlighted = false;
+ })
+ this.update(attrs.root)
+ }
+
+ // It can take selector which would go fullscreen
+ fullscreen(elem) {
+ const attrs = this.getChartState();
+ const el = d3.select(elem || attrs.container).node();
+
+ d3.select(document).on('fullscreenchange.' + attrs.id, function (d) {
+ const fsElement = document.fullscreenElement || document.mozFullscreenElement || document.webkitFullscreenElement;
+ if (fsElement == el) {
+ setTimeout(d => {
+ attrs.svg.attr('height', window.innerHeight - 40);
+ }, 500)
+ } else {
+ attrs.svg.attr('height', attrs.svgHeight)
+ }
+ })
+
+ if (el.requestFullscreen) {
+ el.requestFullscreen();
+ } else if (el.mozRequestFullScreen) {
+ el.mozRequestFullScreen();
+ } else if (el.webkitRequestFullscreen) {
+ el.webkitRequestFullscreen();
+ } else if (el.msRequestFullscreen) {
+ el.msRequestFullscreen();
+ }
+ }
+
+ // Zoom in exposed method
+ zoomIn() {
+ const { svg, zoomBehavior } = this.getChartState();
+ svg.transition().call(zoomBehavior.scaleBy, 1.3);
+ }
+
+ // Zoom out exposed method
+ zoomOut() {
+ const { svg, zoomBehavior } = this.getChartState();
+ svg.transition().call(zoomBehavior.scaleBy, 0.78);
+ }
+
+ toDataURL(url, callback) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function () {
+ var reader = new FileReader();
+ reader.onloadend = function () {
+ callback(reader.result);
+ }
+ reader.readAsDataURL(xhr.response);
+ };
+ xhr.open('GET', url);
+ xhr.responseType = 'blob';
+ xhr.send();
+ }
+
+ exportImg({ full = false, scale = 3, onLoad = d => d, save = true } = {}) {
+ const that = this;
+ const attrs = this.getChartState();
+ const { svg: svgImg, root } = attrs
+ let count = 0;
+ const selection = svgImg.selectAll('img')
+ let total = selection.size()
+
+ const exportImage = () => {
+ const transform = JSON.parse(JSON.stringify(that.lastTransform()));
+ const duration = that.duration();
+ if (full) {
+ that.fit();
+ }
+ const { svg } = that.getChartState()
+
+ setTimeout(d => {
+ that.downloadImage({
+ node: svg.node(), scale, isSvg: false,
+ onAlreadySerialized: d => {
+ that.update(root)
+ },
+ onLoad: onLoad,
+ save
+ })
+ }, full ? duration + 10 : 0)
+ }
+
+ if (total > 0) {
+ selection
+ .each(function () {
+ that.toDataURL(this.src, (dataUrl) => {
+ this.src = dataUrl;
+ if (++count == total) {
+ exportImage();
+ }
+ })
+ })
+ } else {
+ exportImage();
+ }
+
+
+ }
+
+
+
+ exportSvg() {
+ const { svg } = this.getChartState();
+ this.downloadImage({ node: svg.node(), scale: 3, isSvg: true })
+ return this;
+ }
+
+ expandAll() {
+ const { allNodes, root } = this.getChartState();
+ allNodes.forEach(d => d.data._expanded = true);
+ this.render()
+ return this;
+ }
+
+ expandExpandNodes() {
+ const { allNodes, root } = this.getChartState();
+ // allNodes.forEach(d => d.data._expanded = true);
+ allNodes.forEach(d => d.data._expanded = d.data.expand === "1" ? true : false);
+ this.render()
+ return this;
+ }
+
+ collapseAll() {
+ const { allNodes, root } = this.getChartState();
+ allNodes.forEach(d => d.data._expanded = false);
+ this.expandLevel(0)
+ this.render();
+ return this;
+ }
+
+ downloadImage({ node, scale = 2, isSvg = false, save = true, onAlreadySerialized = d => { }, onLoad = d => { } }) {
+ // Retrieve svg node
+ const svgNode = node;
+
+ if (isSvg) {
+ let source = serializeString(svgNode);
+ //add xml declaration
+ source = '\r\n' + source;
+ //convert svg source to URI data scheme.
+ var url = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source);
+ saveAs(url, "graph.svg");
+ onAlreadySerialized()
+ return;
+ }
+ // Get image quality index (basically, index you can zoom in)
+ const quality = scale
+ // Create image
+ const image = document.createElement('img');
+ image.onload = function () {
+ // Create image canvas
+ const canvas = document.createElement('canvas');
+ // Set width and height based on SVG node
+ const rect = svgNode.getBoundingClientRect();
+ canvas.width = rect.width * quality;
+ canvas.height = rect.height * quality;
+ // Draw background
+ const context = canvas.getContext('2d');
+ context.fillStyle = '#FAFAFA';
+ context.fillRect(0, 0, rect.width * quality, rect.height * quality);
+ context.drawImage(image, 0, 0, rect.width * quality, rect.height * quality);
+ // Set some image metadata
+ let dt = canvas.toDataURL('image/png');
+ if (onLoad) {
+ onLoad(dt)
+ }
+ if (save) {
+ // Invoke saving function
+ saveAs(dt, 'graph.png');
+ }
+
+ };
+
+ var url = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(serializeString(svgNode));
+
+ onAlreadySerialized()
+
+ image.src = url// URL.createObjectURL(blob);
+ // This function invokes save window
+ function saveAs(uri, filename) {
+ // create link
+ var link = document.createElement('a');
+ if (typeof link.download === 'string') {
+ document.body.appendChild(link); // Firefox requires the link to be in the body
+ link.download = filename;
+ link.href = uri;
+ link.click();
+ document.body.removeChild(link); // remove the link when done
+ } else {
+ location.replace(uri);
+ }
+ }
+ // This function serializes SVG and sets all necessary attributes
+ function serializeString(svg) {
+ const xmlns = 'http://www.w3.org/2000/xmlns/';
+ const xlinkns = 'http://www.w3.org/1999/xlink';
+ const svgns = 'http://www.w3.org/2000/svg';
+ svg = svg.cloneNode(true);
+ const fragment = window.location.href + '#';
+ const walker = document.createTreeWalker(svg, NodeFilter.SHOW_ELEMENT, null, false);
+ while (walker.nextNode()) {
+ for (const attr of walker.currentNode.attributes) {
+ if (attr.value.includes(fragment)) {
+ attr.value = attr.value.replace(fragment, '#');
+ }
+ }
+ }
+ svg.setAttributeNS(xmlns, 'xmlns', svgns);
+ svg.setAttributeNS(xmlns, 'xmlns:xlink', xlinkns);
+ const serializer = new XMLSerializer();
+ const string = serializer.serializeToString(svg);
+ return string;
+ }
+ }
+
+ // Calculate what size text will take
+ getTextWidth(text, {
+ fontSize = 14,
+ fontWeight = 400,
+ defaultFont = "Helvetice",
+ ctx
+ } = {}) {
+ ctx.font = `${fontWeight || ''} ${fontSize}px ${defaultFont} `
+ const measurement = ctx.measureText(text);
+ return measurement.width;
+ }
+}
diff --git a/src/pages/company.jsx b/src/pages/company.jsx
index 404142e..1a2d4e4 100644
--- a/src/pages/company.jsx
+++ b/src/pages/company.jsx
@@ -188,12 +188,12 @@ export default function companyPage() {
// tool bar start
const handleTopLayoutClick = (progressBtn) => {
progressBtn.current.style.top= 50 + "px";
- orgChart && orgChart.layout('top').render().fit();
+ orgChart && orgChart.layout('top').render();
}
const handleLeftLayoutClick = (progressBtn) => {
progressBtn.current.style.top= 50 + "px";
- orgChart && orgChart.layout('left').render().fit();
+ orgChart && orgChart.layout('left').render();
}
const handleZoomIn = (progressBtn) => {
diff --git a/src/pages/user.jsx b/src/pages/user.jsx
index 118f92b..5f0c6c1 100644
--- a/src/pages/user.jsx
+++ b/src/pages/user.jsx
@@ -6,6 +6,7 @@ import { TopBar } from '../components/topBar';
import ToolBar from '../components/toolBar';
import moment from "moment";
import qs from 'qs';
+import { message } from 'antd';
export default function userPage() {
@@ -172,7 +173,11 @@ export default function userPage() {
topBarSearchRequest = requestData
let api = "/api/bs/hrmorganization/orgchart/userData" + qs.stringify(requestData, {addQueryPrefix: true})
fetch(api).then(res => res.json()).then(data => {
- setData(data.data)
+ if(data.data && data.data.length > 0) {
+ setData(data.data)
+ } else {
+ message.warning("暂无数据");
+ }
})
}
@@ -314,20 +319,20 @@ export default function userPage() {
font-family: Microsoft YaHei-Regular, Microsoft YaHei;
font-weight: 400;
color: #333333;
- ">${d.data.department} / ${d.data.fleaderjob}
+ ">${d.data.department ? d.data.department + " / " : ""}${d.data.fleaderjob}
${d.data.mobile}
+ ">${d.data.mobile ? d.data.mobile : ""}
地址:${d.data.address}
+ ">${d.data.address ? "地址:" + d.data.address : ""}