diff --git a/src/components/orgChart/index.jsx b/src/components/orgChart/index.jsx index 6c4cdd8..20fb829 100644 --- a/src/components/orgChart/index.jsx +++ b/src/components/orgChart/index.jsx @@ -6,15 +6,27 @@ * @FilePath: /org-chart-frant/src/components/orgChart/index.jsx * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE */ -import React, { useLayoutEffect, useRef, useEffect } from 'react'; -import { OrgChart } from 'd3-org-chart'; +import React, { useLayoutEffect, useRef, useEffect, memo } from 'react'; +import { OrgChart } from '../../d3-org-chart.js'; import * as d3 from 'd3'; import { message } from 'antd'; +import moment from 'moment'; -export const OrgChartComponent = (props, ref) => { +export const OrgChartComponent = memo((props, ref) => { const d3Container = useRef(null); + const chartBackup = useRef(null); let chart = null; - + const { data = [{}] } = props; + const { + requestRes = { + date: moment(new Date()).format('YYYY-MM-DD'), + fclass: '0', + root: '0', + level: '3', + fisvitual: '0', + }, + } = props; + const { level = '3' } = requestRes; function addNode(node) { chart.addNode(node); } @@ -23,9 +35,9 @@ export const OrgChartComponent = (props, ref) => { // We need to manipulate DOM useLayoutEffect(() => { - if (props.data && d3Container.current) { + if (d3Container.current) { if (!chart) { - chart = new OrgChart({ expandLevel: 3 }); + chart = new OrgChart({ expandLevel: +level == 1 ? 0 : +level }); window.chart = chart; } props.setChart(chart); @@ -34,7 +46,7 @@ export const OrgChartComponent = (props, ref) => { chart .container(d3Container.current) - .data(props.data) + .data(data) .nodeWidth(props.nodeWidth) .nodeHeight(props.nodeHeight) .layout('top') @@ -52,13 +64,13 @@ export const OrgChartComponent = (props, ref) => { .buttonContent(props.buttonContent) .nodeContent(props.nodeContent) .render(); - chart .setCentered(chart.getChartState().root.id) .expandExpandNodes() .render(); + chartBackup.current = chart; chart.onButtonClick = (event, d) => { - buttonClick.bind(chart)(event, d); + buttonClick.bind(chartBackup.current)(event, d); props.onButtonClick && props.onButtonClick(event, d); }; } catch (err) { @@ -66,11 +78,11 @@ export const OrgChartComponent = (props, ref) => { message.warning(err); } } - }, [props.data, d3Container.current]); + }, [data]); return (
); -}; +}); diff --git a/src/components/toolBar/index.jsx b/src/components/toolBar/index.jsx index 16fd715..3efd8e4 100644 --- a/src/components/toolBar/index.jsx +++ b/src/components/toolBar/index.jsx @@ -1,78 +1,104 @@ -import React from 'react' -import add from './img/add.png' -import decrease from './img/decrease.png' +import React from 'react'; +import add from './img/add.png'; +import decrease from './img/decrease.png'; import styles from './index.less'; import top from './img/top.png'; import left from './img/left.png'; -import topActive from './img/top_active.png' -import leftActive from './img/left_active.png' +import topActive from './img/top_active.png'; +import leftActive from './img/left_active.png'; export default class ToolBar extends React.Component { - progressBtn = React.createRef() - start = false; - clientY = 0 - originalY = 0 - top = 50 - constructor(props) { - super(props); - this.state = { - toolActive: "top" - } - } + progressBtn = React.createRef(); + start = false; + clientY = 0; + originalY = 0; + top = 50; + constructor(props) { + super(props); + this.state = { + toolActive: 'top', + }; + } - handleMouseDown(e) { - this.clientY = e.clientY - this.originalY = e.clientY - this.top = parseInt(this.progressBtn.current.style.top) - this.start = true; - } + handleMouseDown(e) { + this.clientY = e.clientY; + this.originalY = e.clientY; + this.top = parseInt(this.progressBtn.current.style.top); + this.start = true; + } - handleMouseMove(e) { - if(this.start && e.clientY - this.clientY > 10) { - this.props.onZoomOut() - this.clientY = e.clientY - } else if(this.start && e.clientY - this.clientY < - 10) { - this.props.onZoomIn() - this.clientY = e.clientY - } - if(this.start) { - let offset = e.clientY - this.originalY - console.log("offset:", offset); - if((this.top + offset) < 0) { - offset = 0 - this.top - } - if((this.top + offset) > 100) { - offset = 100 - this.top - } - console.log("(this.top + offset):", (this.top + offset)) - this.progressBtn.current.style.top = (this.top + offset) + "px"; - } + handleMouseMove(e) { + if (this.start && e.clientY - this.clientY > 10) { + this.props.onZoomOut(); + this.clientY = e.clientY; + } else if (this.start && e.clientY - this.clientY < -10) { + this.props.onZoomIn(); + this.clientY = e.clientY; } - - handleMouseUp(e) { - this.start = false; - console.log("this.start:", this.start) - + if (this.start) { + let offset = e.clientY - this.originalY; + if (this.top + offset < 0) { + offset = 0 - this.top; + } + if (this.top + offset > 100) { + offset = 100 - this.top; + } + this.progressBtn.current.style.top = this.top + offset + 'px'; } - - render() { - return ( -
- {this.props.onZoomIn(this.progressBtn)}}/> -
-
{this.handleMouseDown(e)}} - onMouseMove={(e) => {this.handleMouseMove(e)}} - onMouseUp={(e) => {this.handleMouseUp(e)}} - onMouseLeave={(e) => {this.handleMouseUp(e)}} - >
-
-
- {this.props.onZoomOut(this.progressBtn)}}/> + } - {this.setState({toolActive: "top"}); this.props.onTopLayoutClick(this.progressBtn)}}/> - {this.setState({toolActive: "left"}); this.props.onLeftLayoutClick(this.progressBtn)}}/> -
- ) - } -} \ No newline at end of file + handleMouseUp(e) { + this.start = false; + console.log('this.start:', this.start); + } + + render() { + return ( +
+ { + this.props.onZoomIn(this.progressBtn); + }} + /> +
+
{this.handleMouseDown(e)}} + // onMouseMove={(e) => {this.handleMouseMove(e)}} + // onMouseUp={(e) => {this.handleMouseUp(e)}} + // onMouseLeave={(e) => {this.handleMouseUp(e)}} + >
+
+
+ { + this.props.onZoomOut(this.progressBtn); + }} + /> + + { + this.setState({ toolActive: 'top' }); + this.props.onTopLayoutClick(this.progressBtn); + }} + /> + { + this.setState({ toolActive: 'left' }); + this.props.onLeftLayoutClick(this.progressBtn); + }} + /> +
+ ); + } +} diff --git a/src/components/topBar/index.jsx b/src/components/topBar/index.jsx index c43acb9..6a82d68 100644 --- a/src/components/topBar/index.jsx +++ b/src/components/topBar/index.jsx @@ -36,6 +36,11 @@ export class TopBar extends React.Component { let requestData = { ...this.state.requestData, ...payload }; this.setState({ requestData }); } + //日期可选择未来 + // disabledDate (current) { + // // return current && current >moment().subtract(1, "days"); + // return current && current > moment().endOf("day"); + // } handleExportMenuClick(e) { this.props.onExport(e.key == '1' ? 'png' : 'pdf'); @@ -80,8 +85,10 @@ export class TopBar extends React.Component { 数据日期: - + diff --git a/src/d3-org-chart.js b/src/d3-org-chart.js index 73644b2..ba08742 100644 --- a/src/d3-org-chart.js +++ b/src/d3-org-chart.js @@ -1,1612 +1,1980 @@ -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 { 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, -} + 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 || ''} + 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: 0, + 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("")} + `; + }) + .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

+ `; + }, + 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; +
`, + layout: 'top', // top, left,right, bottom + buttonContent: ({ node, state }) => { + const icons = { + left: (d) => + d + ? `
` + : `
`, + bottom: (d) => + d + ? `
ˬ
` + : `
ˆ
`, + right: (d) => + d + ? `
` + : `
`, + top: (d) => + d + ? `
ˆ
` + : `
ˬ
`, }; - } - - // 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); - }); + return `
${icons[ + state.layout + ](node.children)}
`; + }, + layoutBindings: { + left: { + nodeLeftX: (node) => node.width / 0.4, + 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, + // "nodeTopY": node => node.height/0.4, + 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] = _; } - - // Loop over _children and recursively store descendants id (collapsed nodes) - if (_children) { - _children.forEach((d) => { - this.getNodeChildren(d, nodeStore); - }); + 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 result - return nodeStore; + 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); + }); } - // 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; + // Loop over _children and recursively store descendants id (collapsed nodes) + if (_children) { + _children.forEach((d) => { + this.getNodeChildren(d, nodeStore); + }); } - 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); - + // 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; + } - //######################################### UTIL FUNCS ################################## - // This function restyles foreign object elements () + //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; + } - d3.select(window).on(`resize.${attrs.id}`, () => { - const containerRect = d3.select(attrs.container).node().getBoundingClientRect(); - attrs.svg.attr('width', containerRect.width) + //****************** 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'); + } - if (attrs.firstDraw) { - attrs.firstDraw = false; - } - - return this; + 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, + }); + }); } - // 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); + attrs.chart = chart; - // Update state of nodes and redraw graph - this.updateNodesState(); + // Display tree contenrs + this.update(attrs.root); - return this; - } + //######################################### UTIL FUNCS ################################## + // This function restyles foreign object elements () - 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); + d3.select(window).on(`resize.${attrs.id}`, () => { + const containerRect = d3 + .select(attrs.container) + .node() + .getBoundingClientRect(); + attrs.svg.attr('width', containerRect.width); + }); - // Update state of nodes and redraw graph - this.updateNodesState(); + if (attrs.firstDraw) { + attrs.firstDraw = false; + } - return this; + 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; } - // 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)); } - // 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 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; + } + }); + } + }); + } - const updateNodesState = this.updateNodesState.bind(this); - // Update state of nodes and redraw graph - updateNodesState(); + // This function basically redraws visible graph, based on nodes state + update({ x0, y0, x = 0, y = 0, width, height }, type) { + const attrs = this.getChartState(); + const calc = attrs.calc; - return this; + if (attrs.compact) { + this.calculateCompactFlexDimensions(attrs.root); } - groupBy(array, accessor, aggegator) { - const grouped = {} - array.forEach(item => { - const key = accessor(item) - if (!grouped[key]) { - grouped[key] = [] - } - grouped[key].push(item) - }) + // Assigns the x and y position for the nodes + const treeData = attrs.flexTreeLayout(attrs.root); - Object.keys(grouped).forEach(key => { - grouped[key] = aggegator(grouped[key]) - }) - return Object.entries(grouped); + // Reassigns the x and y position for the based on the compact layout + if (attrs.compact) { + this.calculateCompactFlexPositions(attrs.root); } - 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; - } - }) - } - }) + 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); } - // 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); - } + // -------------------------- 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 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) + 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; } - - // -------------------------- 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; + 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, }); - - // 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 - }) + }) + .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 === null)[0]; + 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; + // if (type == 'buttonClick') { + // this.fit({ + // animate: true, + // scale: false, + // nodes: centeredNodes, + // transform: false, + // }); + // } else { + // this.fit(); + // } + console.log(44444, centeredNodes); + this.fit({ + animate: true, + scale: false, + nodes: centeredNodes, + transform: false, + }); } + } - // This function detects whether current browser is edge - isEdge() { - return window.navigator.userAgent.includes("Edge"); - } + // 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; + // 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; + 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; + // 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; + // 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; + // 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; + // 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; + // 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 ` + // 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} + 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} + 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; + // 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 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 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; + 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; + 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 = ` + 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} + 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} + C ${ex} ${y + h * yrvs + r * yrvs} ${ex} ${ + y + h * yrvs + r * yrvs + } ${ex} ${ey - h * yrvs} L ${ex} ${ey} `; - return path; + 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 d.id ? 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; } - 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) }) + // 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)); + } } - // 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; + // Redraw Graph + this.update(d, 'buttonClick'); + } - // Set descendants expanded property to false - this.setExpansionFlagToChildren(d, false); - } else { - // Expand children - d.children = d._children; - d._children = null; + // This function changes `expanded` property to descendants + setExpansionFlagToChildren({ data, children, _children }, flag) { + // Set flag to the current property + data._expanded = flag; - // Set each children as expanded - if (d.children) { - d.children.forEach(({ data }) => (data._expanded = true)); - } - } - - // Redraw Graph - this.update(d); + // Loop over and recursively update expanded children's descendants + if (children) { + children.forEach((d) => { + this.setExpansionFlagToChildren(d, flag); + }); } - // 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); - }); - } + // 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)); + } + + // 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; } - } - - // 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)); - } + // Replace current parent holding object + parent = parent.parent; + } } - // 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; - } + // Recursivelly do the same for collapsed nodes + if (d._children) { + d._children.forEach((ch) => this.expandSomeNodes(ch)); } - // 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; - } + // Recursivelly do the same for expanded nodes + if (d.children) { + d.children.forEach((ch) => this.expandSomeNodes(ch)); } - - // 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(); - } + } + + // 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)); } - - 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)') + } + + // 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; } - - 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; + } + + // 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; } + } - // 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]; + // Zoom handler function + zoomed(event, d) { + const attrs = this.getChartState(); + const chart = attrs.chart; - 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; - } + // Get d3 event's transform object + const transform = event.transform; - 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; - } + // Store it + attrs.lastTransform = transform; - 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; - } + // Reposition and rescale chart accordingly + chart.attr('transform', transform); - 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; + // Apply new styles to the foreign object element + if (this.isEdge()) { + this.restyleForeignObjectElements(); } - - clearHighlighting() { - const attrs = this.getChartState(); - attrs.allNodes.forEach(d => { - d.data._highlighted = false; - d.data._upToTheRootHighlighted = false; - }) - this.update(attrs.root) + } + + 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, transform = true } = {}) { + const attrs = this.getChartState(); + const { root } = attrs; + let descendants = nodes ? nodes : root.descendants(); + console.log(11111111111, nodes, descendants, scale); + 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), + ); + console.log('333,minY', minX, maxX, minY, maxY); + this.zoomTreeBounds({ + params: { animate: animate, scale }, + x0: transform == 'export' ? minX - 700 : minX - 50, + x1: transform == 'export' ? maxX + 0 : maxX - 50, + y0: transform == false ? minY - 250 : minY - 0, + y1: transform == false ? maxY + 350 : maxY + 250, + }); + 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; } - - // 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(); - } + 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; } - - // Zoom in exposed method - zoomIn() { - const { svg, zoomBehavior } = this.getChartState(); - svg.transition().call(zoomBehavior.scaleBy, 1.3); + 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; } - - // Zoom out exposed method - zoomOut() { - const { svg, zoomBehavior } = this.getChartState(); - svg.transition().call(zoomBehavior.scaleBy, 0.78); + 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; } - - 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(); + 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(); } - - 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 { + } + + // 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 = 12, 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({ transform: 'export' }); + } + 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(); - } - - - } - - - - 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; + } + }); + }); + } else { + exportImage(); } - - 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; + } + + 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; } - - collapseAll() { - const { allNodes, root } = this.getChartState(); - allNodes.forEach(d => d.data._expanded = false); - this.expandLevel(0) - this.render(); - return this; + // 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); + } } - - 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; + // 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; - } + } + + // 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 a47abd9..c360017 100644 --- a/src/pages/company.jsx +++ b/src/pages/company.jsx @@ -9,6 +9,7 @@ import moment from 'moment'; import qs from 'qs'; import { message } from 'antd'; +let active = 'top'; export default function companyPage() { const [data, setData] = useState(null); const [sliderProgress, setSliderProgress] = useState(50); @@ -70,13 +71,15 @@ export default function companyPage() { // 获取部门图片 function getDepartmentImage() { let index = Math.floor(Math.random() * 8) + 1; - return `./img/department/${index}.png`; + // return `./img/department/${index}.png`; + return `./img/department/1.png`; } // 获取部门图片 function getSubcompanyImage() { let index = Math.floor(Math.random() * 3) + 1; - return `./img/subcompany/${index}.png`; + // return `./img/subcompany/${index}.png`; + return `./img/subcompany/2.png`; } // 获取数据 @@ -94,9 +97,9 @@ export default function companyPage() { // ButtonContent渲染 const buttonContentRender = ({ node, state }) => { if (node.children) { - return `
ˆ
`; + return `
ˆ
`; } else { - return `
ˬ
`; + return `
ˆ
`; } }; @@ -147,22 +150,14 @@ export default function companyPage() { letter-spacing: 1px; margin-top: 10px; ">${d.data.fname}
-
COMPANY_GROUP
`; } else if (d.data.ftype == 1) { return `
-
+
-
`; } else if (d.data.ftype == 2) { return ` -
-
+
+
+
+
(addNodeChildFunc = click)} onNodeClick={onNodeClick} data={data} + onButtonClick={onButtonClick} buttonContent={buttonContentRender} nodeWidth={nodeWidthRender} nodeHeight={nodeHeightRender} diff --git a/src/pages/user.jsx b/src/pages/user.jsx index 0933ac1..d8aebae 100644 --- a/src/pages/user.jsx +++ b/src/pages/user.jsx @@ -7,9 +7,17 @@ import ToolBar from '../components/toolBar'; import moment from 'moment'; import qs from 'qs'; import { message } from 'antd'; - +import jsPDF from 'jspdf'; +let active = 'top'; export default function userPage() { const [data, setData] = useState(null); + const [requestRes, setRequestRes] = useState({ + date: moment(new Date()).format('YYYY-MM-DD'), + fclass: '0', + root: '0', + level: '3', + fisvitual: '0', + }); const [progressTop, setProgressTop] = useState(50); let addNodeChildFunc = null; let orgChart = null; @@ -79,13 +87,15 @@ export default function userPage() { // 获取数据 useEffect(() => { + document.cookie = + 'ecology_JSessionid=aaa1QNMWge48Bh-3oq6oy; JSESSIONID=aaa1QNMWge48Bh-3oq6oy; loginidweaver=1; languageidweaver=7; loginuuids=1; __randcode__=0bef9a3b-51e8-452a-8558-5080bdee23ba'; d3.json( // "/user/data" '/api/bs/hrmorganization/orgchart/userData?fclass=0&root=0&date=' + moment(new Date()).format('YYYY-MM-DD'), ).then((data) => { setData(data.data); - setHasRight(data.hasRight); + setHasRight(data?.hasRight); }); }, [true]); @@ -110,21 +120,32 @@ export default function userPage() { // tool bar start const handleTopLayoutClick = (progressBtn) => { progressBtn.current.style.top = 50 + 'px'; - orgChart && orgChart.layout('top').render(); + orgChart && + orgChart + .setCentered(orgChart.getChartState().root.id) + .layout('top') + .render(); + active = 'top'; }; const handleLeftLayoutClick = (progressBtn) => { progressBtn.current.style.top = 50 + 'px'; - orgChart && orgChart.layout('left').render(); + orgChart && + orgChart + .layout('left') + .setCentered(orgChart.getChartState().root.id) + .render(); + active = 'left'; }; const handleZoomIn = (progressBtn) => { if (progressBtn) { let top = parseInt(progressBtn.current.style.top) - 10; - if (top <= 0) { - top = 30; + if (top >= 0) { + progressBtn.current.style.top = top + 'px'; + } else { + return; } - progressBtn.current.style.top = top + 'px'; } orgChart && orgChart.zoomIn(); }; @@ -132,10 +153,11 @@ export default function userPage() { const handleZoomOut = (progressBtn) => { if (progressBtn) { let top = parseInt(progressBtn.current.style.top) + 10; - if (top >= 100) { - top = 70; + if (top <= 100) { + progressBtn.current.style.top = top + 'px'; + } else { + return; } - progressBtn.current.style.top = top + 'px'; } orgChart && orgChart.zoomOut(); }; @@ -147,6 +169,7 @@ export default function userPage() { function downloadPdf(chart) { chart.exportImg({ save: false, + full: true, onLoad: (base64) => { var pdf = new jsPDF(); var img = new Image(); @@ -182,14 +205,31 @@ export default function userPage() { fetch(api) .then((res) => res.json()) .then((data) => { - if (data.data && data.data.length > 0) { - setData(data.data); - } else { - message.warning('暂无数据'); + if (data.data) { + if (!data.data.length) { + setData([{}]); + message.warning('暂无数据'); + } else { + setData(data?.data); + } } }); }; - + useEffect(() => { + if (active == 'left') { + orgChart && + orgChart + .setCentered(orgChart.getChartState().root.id) + .layout('left') + .render(); + } else { + orgChart && + orgChart + .setCentered(orgChart.getChartState().root.id) + .layout('top') + .render(); + } + }, [data]); // top bar end const nodeContentRender = (d, i, arr, state) => { @@ -204,9 +244,16 @@ export default function userPage() { let jobtitleUrl = `/spa/organization/static/index.html#/main/organization/jobExtend/${d.data.fobjid}`; // 人员地址 let userUrl = `/spa/hrm/index_mobx.html#/main/hrm/card/cardInfo/${d.data.fleader}`; - - // 通讯录 - let addressBookUrl = '/spa/hrm/index_mobx.html#/main/hrm/addressBook'; + // 岗位人员地址 + let postUserUrl = `/spa/hrm/index_mobx.html#/main/hrm/card/cardInfo/${d.data.fobjid}`; + //集团通讯录 + let addressBookGroupUrl = `/spa/hrm/index_mobx.html#/main/hrm/addressBook/?virtualtype=${d.data.fecid}`; + // 分部通讯录 + let addressBookUrl = `/spa/hrm/index_mobx.html#/main/hrm/addressBook/?subcompanyid1=${d.data.fecid}`; + // 部门通讯录 + let addressBookDepartmentUrl = `/spa/hrm/index_mobx.html#/main/hrm/addressBook/?departmentid=${d.data.fecid}`; + // 岗位通讯录 + let addressBookPostUrl = `/spa/hrm/index_mobx.html#/main/hrm/addressBook/?departmentid=${d.data.fobjparentId}`; if (d.data.ftype == 0 || d.data.ftype == 1 || d.data.ftype == 2) { return `
@@ -218,7 +265,7 @@ export default function userPage() { top: -8px; background: #F7F9FD; z-index: 100; - padding: 0px 10px; + padding: 0px 10px; font-size: 16px; font-family: Microsoft YaHei-Bold, Microsoft YaHei; font-weight: bold; @@ -236,36 +283,58 @@ export default function userPage() { -
-
- +
+
+
+
+ +
-
+
+
${d.data.fleadername}
+ font-size: 13px; + font-family: Microsoft YaHei-Bold, Microsoft YaHei; + font-weight: bold; + color: #333333; + margin-bottom: 9px;" + >${d.data.fleadername}
${d.data.fname} / ${d.data.fleaderjob}
-
-
编制: ${ + white-space:nowrap; + overflow:hidden; + text-overflow:ellipsis;" + title="${d.data.fname}${ + d.data.fleaderjob ? `/${d.data.fleaderjob}` : '' + }">${d.data.fname}${ + d.data.fleaderjob ? `/${d.data.fleaderjob}` : '' + }
+
+
+
编制: ${ d.data.fplan }
-
在岗: ${ - d.data.fonjob - }
+
在岗: ${d.data.fonjob}
@@ -281,19 +350,20 @@ export default function userPage() { top: -8px; background: #F7F9FD; z-index: 100; - padding: 0px 10px; + padding: 0px 10px; font-size: 16px; font-family: Microsoft YaHei-Bold, Microsoft YaHei; font-weight: bold; color: #000000; - " onclick="window.open('${ - jobtitleUrl + d.data.fnumber - }', '_blank')">${d.data.fname} + " onclick="window.open('${jobtitleUrl}', '_blank')">${d.data.fname} -
+
+
+ +
${d.data.fname}
+ " onclick="window.open('${jobtitleUrl}', '_blank')">${d.data.fname}
+ " > 编制:${d.data.fplan} - 在岗:${d.data.fonjob} + 在岗:${d.data.fonjob}
@@ -320,27 +388,54 @@ export default function userPage() {
`; } else if (d.data.ftype == 4) { return `
-
+
-
-
- +
+
+
-
-
${d.data.fname}
+
+ + +
+ +
+
+
${d.data.fname}
+
${ + d.data.fleaderlv + ? d.data.fleaderst + ? `(${d.data.fleaderlv}/` + : `(${d.data.fleaderlv})` + : '' + }${d.data.fleaderst ? `${d.data.fleaderst})` : ''}
+
handleTopLayoutClick(progressBtn)} - onLeftLayoutClick={(progressBtn) => - handleLeftLayoutClick(progressBtn) - } + onTopLayoutClick={handleTopLayoutClick} + onLeftLayoutClick={handleLeftLayoutClick} onZoomOut={(progressBtn) => handleZoomOut(progressBtn)} onZoomIn={(progressBtn) => handleZoomIn(progressBtn)} /> @@ -411,6 +505,7 @@ export default function userPage() { nodeWidth={nodeWidthRender} nodeHeight={nodeHeightRender} nodeContent={nodeContentRender} + requestRes={requestRes} />
)