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 : ""}