隐藏组织结构的第一个元素d3.js

Hide the first element of the organizational structure d3.js

我是d3 js的初学者,想对组织结构进行现代化改造,需要隐藏第一个结构元素,示例如图,代码摘自githubhttps://observablehq.com/@bumbeishvili/d3-v5-organization-chart

通过其 parent 属性隐藏顶部元素:

var nodeEnter = nodesSelection.enter().append('g')
  .attr('class', 'node')
  .style('visibility', d => d.parent ? 'visible' : 'hidden')
  ...

使用 depth 属性隐藏顶部元素的链接:

var linkEnter = linkSelection.enter()
  .insert('path', "g")
  .attr("class", "link")
  .style('visibility', d => d.depth > 1 ? 'visible' : 'hidden')
  ...

在代码片段中查看它的工作原理:

    d3.json('https://gist.githubusercontent.com/bumbeishvili/dc0d47bc95ef359fdc75b63cd65edaf2/raw/c33a3a1ef4ba927e3e92b81600c8c6ada345c64b/orgChart.json')
.then(data=>{

Chart()
   .container('.chart-container')
   .data(data)
   .svgWidth(window.innerWidth)
   .svgHeight(window.innerHeight)
   .initialZoom(0.6)
   .onNodeClick(d=> console.log(d+' node clicked'))
   .render()


})


function Chart() {
    // Exposed variables
    var attrs = {
        id: 'ID' + Math.floor(Math.random() * 1000000), // Id for event handlings
        svgWidth: 800, 
        svgHeight: 600,
        marginTop: 0,
        marginBottom: 0,
        marginRight: 0,
        marginLeft: 0,
        container: 'body',
        defaultTextFill: '#2C3E50',
        nodeTextFill:'white',
        defaultFont: 'Helvetica',
        backgroundColor: '#fafafa',
        data: null,
        depth: 180,
        duration: 600,
        strokeWidth: 3,
        dropShadowId: null,
        initialZoom:1,
        onNodeClick:d=>d
    };

    //InnerFunctions which will update visuals
    var updateData;

    //Main chart object
    var main = function() {
        //Drawing containers
        var container = d3.select(attrs.container);
        var containerRect = container.node().getBoundingClientRect();
        if (containerRect.width > 0) attrs.svgWidth = containerRect.width;

        setDropShadowId(attrs);

        //Calculated properties
        var calc = {
            id: null,
            chartTopMargin: null,
            chartLeftMargin: null,
            chartWidth: null,
            chartHeight: null
        };
        calc.id = 'ID' + Math.floor(Math.random() * 1000000); // id for event handlings
        calc.chartLeftMargin = attrs.marginLeft;
        calc.chartTopMargin = attrs.marginTop;
        calc.chartWidth = attrs.svgWidth - attrs.marginRight - calc.chartLeftMargin;
        calc.chartHeight = attrs.svgHeight - attrs.marginBottom - calc.chartTopMargin;
        calc.nodeMaxWidth = d3.max(attrs.data, d => d.width);
        calc.nodeMaxHeight = d3.max(attrs.data, d => d.height);

        attrs.depth = calc.nodeMaxHeight + 100;

        calc.centerX = calc.chartWidth / 2;

        //********************  LAYOUTS  ***********************
        const layouts = {
            treemap: null
        }

        layouts.treemap = d3.tree().size([calc.chartWidth, calc.chartHeight])
            .nodeSize([calc.nodeMaxWidth + 100, calc.nodeMaxHeight + attrs.depth])

        // ******************* BEHAVIORS . **********************
        const behaviors = {
            zoom: null
        }

        behaviors.zoom = d3.zoom().on("zoom", zoomed)
      
        
          

        //****************** ROOT node work ************************
        const root = d3.stratify()
            .id(function(d) {
                return d.nodeId;
            })
            .parentId(function(d) {
                return d.parentNodeId;
            })
            (attrs.data)

        root.x0 = 0;
        root.y0 = 0;
      
        const allNodes = layouts.treemap(root).descendants()
        
        allNodes.forEach(d=>{
          Object.assign(d.data,{
            directSubordinates:d.children?d.children.length:0,
            totalSubordinates:d.descendants().length-1
          })
        })
      
        root.children.forEach(collapse);
        root.children.forEach(expandSomeNodes);
      


        //Add svg
        var svg = container
            .patternify({
                tag: 'svg',
                selector: 'svg-chart-container'
            })
            .attr('width', attrs.svgWidth)
            .attr('height', attrs.svgHeight)
            .attr('font-family', attrs.defaultFont)
            .call(behaviors.zoom)
            .attr('cursor', 'move')
            .style('background-color', attrs.backgroundColor)

        //Add container g element
        var chart = svg
            .patternify({
                tag: 'g',
                selector: 'chart'
            })
            .attr('transform', 'translate(' + calc.chartLeftMargin + ',' + calc.chartTopMargin + ')');

        var centerG = chart.patternify({
                tag: 'g',
                selector: 'center-group'
            })
            .attr('transform', `translate(${calc.centerX},${calc.nodeMaxHeight/2}) scale(${attrs.initialZoom})`)
        
        if(attrs.lastTransform)
          {
            behaviors.zoom
              .scaleBy(chart,attrs.lastTransform.k)
              .translateTo(chart,attrs.lastTransform.x,attrs.lastTransform.y)
          }

        const defs = svg.patternify({
            tag: 'defs',
            selector: 'image-defs'
        });

        const filterDefs = svg.patternify({
            tag: 'defs',
            selector: 'filter-defs'
        });

       var filter = filterDefs.patternify({tag:'filter',selector:'shadow-filter-element'})
        .attr('id', attrs.dropShadowId)
        .attr('y',`${-50}%`)
        .attr('x',`${-50}%`)
        .attr('height', `${200}%`)
        .attr('width',`${200}%`)

     filter.patternify({tag:'feGaussianBlur',selector:'feGaussianBlur-element'})
       .attr('in', 'SourceAlpha')
       .attr('stdDeviation', 3.1)
       .attr('result', 'blur');

     filter.patternify({tag:'feOffset',selector:'feOffset-element'})
       .attr('in', 'blur')
       .attr('result', 'offsetBlur')
       .attr("dx", 4.28)
       .attr("dy", 4.48)
       .attr("x", 8)
       .attr("y", 8)

      filter.patternify({tag:'feFlood',selector:'feFlood-element'})
        .attr("in", "offsetBlur")
        .attr("flood-color",'black')
        .attr("flood-opacity", 0.3)
        .attr("result", "offsetColor");

      filter.patternify({tag:'feComposite',selector:'feComposite-element'})
        .attr("in", "offsetColor")
        .attr("in2", "offsetBlur")
        .attr("operator", "in")
        .attr("result", "offsetBlur");

      var feMerge = filter.patternify({tag:'feMerge',selector:'feMerge-element'})

      feMerge.patternify({tag:'feMergeNode',selector:'feMergeNode-blur'})
        .attr('in', 'offsetBlur')
    
      feMerge.patternify({tag:'feMergeNode',selector:'feMergeNode-graphic'})
        .attr('in', 'SourceGraphic')


        // Display tree contenrs
        update(root)

        // Smoothly handle data updating
        updateData = function() {};

        function setDropShadowId(d) {
            if (d.dropShadowId) return;

            let id = d.id + "-drop-shadow";
            //@ts-ignore
            if (typeof DOM != 'undefined') {
            //@ts-ignore
                id = DOM.uid(d.id).id;
            }
            Object.assign(d, {
                dropShadowId: id
            })
        }

        function rgbaObjToColor(d) {
            return `rgba(${d.red},${d.green},${d.blue},${d.alpha})`
        }

        // Zoom handler func
        function zoomed() {
            var transform = d3.event.transform;
            attrs.lastTransform = transform;
            chart.attr('transform', transform);
        }

        // Toggle children on click.
        function click(d) {
            if (d.children) {
                d._children = d.children;
                d.children = null;
            } else {
                d.children = d._children;
                d._children = null;
            }
            update(d);
        }

        function diagonal(s, t) {
            const x = s.x;
            const y = s.y;
            const ex = t.x;
            const ey = t.y;

            let xrvs = ex - x < 0 ? -1 : 1;
            let yrvs = ey - y < 0 ? -1 : 1;

            let rdef = 35;
            let r = Math.abs(ex - x) / 2 < rdef ? Math.abs(ex - x) / 2 : rdef;

            r = Math.abs(ey - y) / 2 < r ? Math.abs(ey - y) / 2 : r;

            let h = Math.abs(ey - y) / 2 - r;
            let w = Math.abs(ex - x) - r * 2;
            //w=0;
            const path = `
            M ${x} ${y}
            L ${x} ${y+h*yrvs}
            C  ${x} ${y+h*yrvs+r*yrvs} ${x} ${y+h*yrvs+r*yrvs} ${x+r*xrvs} ${y+h*yrvs+r*yrvs}
            L ${x+w*xrvs+r*xrvs} ${y+h*yrvs+r*yrvs}
            C ${ex}  ${y+h*yrvs+r*yrvs} ${ex}  ${y+h*yrvs+r*yrvs} ${ex} ${ey-h*yrvs}
            L ${ex} ${ey}
 `
            return path;
        }

        function collapse(d) {
            if (d.children) {
                d._children = d.children;
                d._children.forEach(collapse);
                d.children = null;
            }
        }
      
      function expandSomeNodes(d){
         if(d.data.expanded){

              let parent = d.parent;
              while(parent){

                if(parent._children){

                  parent.children = parent._children;

                  //parent._children=null;
                }
                parent = parent.parent;
              }
          }
          if(d._children){
            d._children.forEach(expandSomeNodes);
          }
          
      }


        function update(source) {
            //  Assigns the x and y position for the nodes

            const treeData = layouts.treemap(root);

            // Get tree nodes and links
            const nodes = treeData.descendants().map(d => {
                if (d.width) return d;

                let imageWidth = 100;
                let imageHeight = 100;
                let imageBorderColor = 'steelblue';
                let imageBorderWidth = 0;
                let imageRx = 0;
                let imageCenterTopDistance = 0;
                let imageCenterLeftDistance = 0;
                let borderColor = 'steelblue';
                let backgroundColor = 'steelblue';
                let width = d.data.width;
                let height = d.data.height;
                let dropShadowId = `none`
                if(d.data.nodeImage && d.data.nodeImage.shadow){
                    dropShadowId = `url(#${attrs.dropShadowId})`
                }
                if (d.data.nodeImage && d.data.nodeImage.width) {
                    imageWidth = d.data.nodeImage.width
                };
                if (d.data.nodeImage && d.data.nodeImage.height) {
                    imageHeight = d.data.nodeImage.height
                };
                if (d.data.nodeImage && d.data.nodeImage.borderColor) {
                    imageBorderColor = rgbaObjToColor(d.data.nodeImage.borderColor)
                };
                if (d.data.nodeImage && d.data.nodeImage.borderWidth) {
                    imageBorderWidth = d.data.nodeImage.borderWidth
                };
                if (d.data.nodeImage && d.data.nodeImage.centerTopDistance) {
                    imageCenterTopDistance = d.data.nodeImage.centerTopDistance
                };
                if (d.data.nodeImage && d.data.nodeImage.centerLeftDistance) {
                    imageCenterLeftDistance = d.data.nodeImage.centerLeftDistance
                };
                if (d.data.borderColor) {
                    borderColor = rgbaObjToColor(d.data.borderColor);
                }
                if (d.data.backgroundColor) {
                    backgroundColor = rgbaObjToColor(d.data.backgroundColor);
                }
                if (d.data.nodeImage &&
                    d.data.nodeImage.cornerShape.toLowerCase() == "circle") {
                    imageRx = Math.max(imageWidth, imageHeight);
                }
                if (d.data.nodeImage &&
                    d.data.nodeImage.cornerShape.toLowerCase() == "rounded") {
                    imageRx = Math.min(imageWidth, imageHeight) / 6;
                }
                return Object.assign(d, {
                    imageWidth,
                    imageHeight,
                    imageBorderColor,
                    imageBorderWidth,
                    borderColor,
                    backgroundColor,
                    imageRx,
                    width,
                    height,
                    imageCenterTopDistance,
                    imageCenterLeftDistance,
                    dropShadowId
                });
            });

            const links = treeData.descendants().slice(1);

            // Set constant depth for each nodes
            nodes.forEach(d => d.y = d.depth * attrs.depth);

            const patternsSelection = defs.selectAll('.pattern')
                .data(nodes, d => d.id);

            const patternEnterSelection = patternsSelection.enter().append('pattern')

            const patterns = patternEnterSelection
                .merge(patternsSelection)
                .attr('class', 'pattern')
                .attr('height', 1)
                .attr('width', 1)
                .attr('id', d => d.id)

            const patternImages = patterns.patternify({
                    tag: 'image',
                    selector: 'pattern-image',
                    data: d => [d]
                })
                .attr('x', 0)
                .attr('y', 0)
                .attr('height', d => d.imageWidth)
                .attr('width', d => d.imageHeight)
                .attr('xlink:href', d => d.data.nodeImage.url)
                .attr('viewbox', d => `0 0 ${d.imageWidth*2} ${d.imageHeight}`)
                .attr('preserveAspectRatio', 'xMidYMin slice')

            patternsSelection.exit().transition().duration(attrs.duration).remove();

            var linkSelection = centerG.selectAll('path.link')
                .data(links, function(d) {
                    return d.id;
                });

            // Enter any new links at the parent's previous position.
            var linkEnter = linkSelection.enter()
                .insert('path', "g")
                .attr("class", "link")
                .style('visibility', d => 
                d.depth > 1 ? 'visible' : 'hidden')
                .attr('d', function(d) {
                    var o = {
                        x: source.x0,
                        y: source.y0
                    }
                    return diagonal(o, o)
                });

            // UPDATE
            var linkUpdate = linkEnter.merge(linkSelection)

            // Styling links
            linkUpdate
                .attr("fill", "none")
                .attr("stroke-width", d => d.data.connectorLineWidth || 2)
                .attr('stroke', d => {
                    if (d.data.connectorLineColor) {
                        return rgbaObjToColor(d.data.connectorLineColor);
                    }
                    return 'green';
                })
                .attr('stroke-dasharray', d => {
                    if (d.data.dashArray) {
                        return d.data.dashArray;
                    }
                    return '';
                })

            // Transition back to the parent element position
            linkUpdate.transition()
                .duration(attrs.duration)
                .attr('d', function(d) {
                    return diagonal(d, d.parent)
                });

            // Remove any exiting links
            var linkExit = linkSelection.exit().transition()
                .duration(attrs.duration)
                .attr('d', function(d) {
                    var o = {
                        x: source.x,
                        y: source.y
                    }
                    return diagonal(o, o)
                })
                .remove();


            // --------------------------  NODES ----------------------
            // Updating nodes
          
            const nodesSelection = centerG.selectAll('g.node')
                .data(nodes, d => d.id)

            // Enter any new nodes at the parent's previous position.
            var nodeEnter = nodesSelection.enter().append('g')
                .attr('class', 'node')
                .attr("transform", function(d) {
                    return "translate(" + source.x0 + "," + source.y0 + ")";
                })
                .attr('cursor', 'pointer')
                .style('visibility', d => !d.parent ? 'hidden' : 'visible')
                .on('click', function(d){
                   if([...d3.event.srcElement.classList].
                       includes('node-button-circle')){
                     return;
                   }
                   attrs.onNodeClick(d.data.nodeId);
                })

            // Add rectangle for the nodes 
            nodeEnter
                .patternify({
                    tag: 'rect',
                    selector: 'node-rect',
                    data: d => [d]
                })
                .attr('width', 1e-6)
                .attr('height', 1e-6)
                .style("fill", function(d) {
                    return d._children ? "lightsteelblue" : "#fff";
                })
          
            // Add foreignObject element
            const fo = nodeEnter
                .patternify({
                    tag: 'foreignObject',
                    selector: 'node-foreign-object',
                    data: d => [d]
                })
                .attr('width', d=> d.width)
                .attr('height',d=> d.height)
                .attr('x', d=> -d.width/2)
                .attr('y', d=> -d.height/2 )

            // Add foreign object 
            fo.patternify({
                    tag: 'xhtml:div',
                    selector: 'node-foreign-object-div',
                    data: d => [d]
                })
                .style('width', d=> d.width + 'px')
                .style('height',d=> d.height + 'px')
                .style('color', 'white')
                .html(d=>d.data.template)

         nodeEnter
                .patternify({
                    tag: 'image',
                    selector: 'node-icon-image',
                    data: d => [d]
                })
                .attr('width', d=>  d.data.nodeIcon.size)
                .attr('height', d=>  d.data.nodeIcon.size)
                .attr("xlink:href",d=>d.data.nodeIcon.icon)
                .attr('x',d=>-d.width/2+5)
                .attr('y',d=> d.height/2 - d.data.nodeIcon.size-5)
          
           nodeEnter
                .patternify({
                    tag: 'text',
                    selector: 'node-icon-text-total',
                    data: d => [d]
                })
                .text('test')
                .attr('x',d=>-d.width/2+7 )
                .attr('y',d=> d.height/2 - d.data.nodeIcon.size-5)
                //.attr('text-anchor','middle')
                .text(d=>d.data.totalSubordinates + ' Subordinates')
                .attr('fill',attrs.nodeTextFill)
                .attr('font-weight','bold')
          
           nodeEnter
                .patternify({
                    tag: 'text',
                    selector: 'node-icon-text-direct',
                    data: d => [d]
                })
                .text('test')
                .attr('x',d=>-d.width/2+10+d.data.nodeIcon.size)
                .attr('y',d=> d.height/2 - 10)
                .text(d=>d.data.directSubordinates +' Direct ')
                .attr('fill',attrs.nodeTextFill)
                .attr('font-weight','bold')

          
            // Node images
            const nodeImageGroups = nodeEnter.patternify({
                tag: 'g',
                selector: 'node-image-group',
                data: d => [d]
            })

            // Node image rectangle 
            nodeImageGroups
                .patternify({
                    tag: 'rect',
                    selector: 'node-image-rect',
                    data: d => [d]
                })

            // Node button circle group
            const nodeButtonGroups = nodeEnter
                .patternify({
                    tag: 'g',
                    selector: 'node-button-g',
                    data: d => [d]
                })
                .on('click', click)

            // Add button circle 
            nodeButtonGroups
                .patternify({
                    tag: 'circle',
                    selector: 'node-button-circle',
                    data: d => [d]
                })

            // Add button text 
            nodeButtonGroups
                .patternify({
                    tag: 'text',
                    selector: 'node-button-text',
                    data: d => [d]
                })
                .attr('pointer-events','none')



            // Node update styles
            var nodeUpdate = nodeEnter.merge(nodesSelection)
                .style('font', '12px sans-serif')

            // Transition to the proper position for the node
            nodeUpdate.transition()
                .attr('opacity', 0)
                .duration(attrs.duration)
                .attr("transform", function(d) {
                    return "translate(" + d.x + "," + d.y + ")";
                })
                .attr('opacity', 1)

            // Move images to desired positions
            nodeUpdate.selectAll('.node-image-group')
                .attr('transform', d => {
                    let x = -d.imageWidth / 2 - d.width / 2;
                    let y = -d.imageHeight / 2 - d.height / 2;
                    return `translate(${x},${y})`
                })


            nodeUpdate.select('.node-image-rect')
                .attr('fill', d => `url(#${d.id})`)
                .attr('width', d => d.imageWidth)
                .attr('height', d => d.imageHeight)
                .attr('stroke', d => d.imageBorderColor)
                .attr('stroke-width', d => d.imageBorderWidth)
                .attr('rx', d => d.imageRx)
                .attr('y', d => d.imageCenterTopDistance)
                .attr('x', d => d.imageCenterLeftDistance)
                .attr('filter',d=> d.dropShadowId)

            nodeUpdate.select('.node-rect')
                .attr('width', d => d.data.width)
                .attr('height', d => d.data.height)
                .attr('x', d => -d.data.width / 2)
                .attr('y', d => -d.data.height / 2)
                .attr('rx', d => d.data.borderRadius || 0)
                .attr('stroke-width', d => d.data.borderWidth || attrs.strokeWidth)
                .attr('cursor', 'pointer')
                .attr('stroke', d => d.borderColor)
                .style("fill", d => d.backgroundColor)

            nodeUpdate.select('.node-button-g')
                .attr('transform', d => {
                    return `translate(0,${d.data.height/2})`
                })
                .attr('opacity', d => {
                    if (d.children || d._children) {
                        return 1;
                    }
                    return 0;
                })

            nodeUpdate.select('.node-button-circle')
                .attr('r', 16)
                .attr('stroke-width', d => d.data.borderWidth || attrs.strokeWidth)
                .attr('fill', attrs.backgroundColor)
                .attr('stroke', d => d.borderColor)

            nodeUpdate.select('.node-button-text')
                .attr('text-anchor', 'middle')
                .attr('alignment-baseline', 'middle')
                .attr('fill', attrs.defaultTextFill)
                .attr('font-size', d => {
                    if (d.children) return 40;
                    return 26;
                })
                .text(d => {
                    if (d.children) return '-';
                    return '+';
                })

            var nodeExitTransition = nodesSelection.exit()
                .attr('opacity', 1)
                .transition()
                .duration(attrs.duration)
                .attr("transform", function(d) {
                    return "translate(" + source.x + "," + source.y + ")";
                })
                .on('end', function() {
                    d3.select(this).remove();
                })
                .attr('opacity', 0)


            // On exit reduce the node rects size to 0
            nodeExitTransition.selectAll('.node-rect')
                .attr('width', 10)
                .attr('height', 10)
                .attr('x', 0)
                .attr('y', 0);

            nodeExitTransition.selectAll('.node-image-rect')
                .attr('width', 10)
                .attr('height', 10)
                .attr('x', d => d.width / 2)
                .attr('y', d => d.height / 2)

            nodes.forEach(function(d) {
                d.x0 = d.x;
                d.y0 = d.y;
            });


            // debugger;
        }
    };

    d3.selection.prototype.patternify = function(params) {
        var container = this;
        var selector = params.selector;
        var elementTag = params.tag;
        var data = params.data || [selector];

        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;
    };

    Object.keys(attrs).forEach((key) => {
        main[key] = function(_) {
            var string = `attrs['${key}'] = _`;
            if (!arguments.length) {
                return eval(` attrs['${key}'];`);
            }
            eval(string);
            return main;
        };
        return main;
    });

    main['attrs'] = attrs;

    main['data'] = function(value) {
        if (!arguments.length) return attrs.data;
        attrs.data = value;
        if (typeof updateData === 'function') {
            updateData();
        }
        return main;
    };

    main['render'] = function() {
        main();
        return main;
    };

    return main;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="chart-container" style="width:800px; height:600px ">