D3+react realizes the horizontal organization chart that can be enlarged, reduced, refreshed and downloaded

D3+react realizes the horizontal organization chart that can be enlarged, reduced, refreshed and downloaded

The code implementation is shown in the figure:

Conditions, the following modules need to be introduced:
import * as d3 from 'd3'
import saveSvg from 'save svg as png' / / download svg into png image format
The implementation code is as follows:
import React, { Component } from 'react'
import * as d3 from 'd3'
import saveSvg from 'save-svg-as-png'

//Transition time
const duration = 0
export default class modal extends Component {
constructor(props){
super(props)
this.constNode = []; // Large node data
this.constRelation = []; // Small node data
this.resData = {};
this.layoutTree = ‘’; // Generating structure
this.diamonds = ‘’; // Square shape
this.i = 0; // Subscript of display node
this.hasChildNodeArr = []; // Element array in tree
this.originDiamonds = ‘’; // Source object
this.tree = {}; // Component structure
this.rootUp = ‘’; // Initialization starting point
this.rootDown = ‘’; // Initialization starting point
this.svg = ‘’; // Main graph
this.nodeData = {}; // Node information
this.isShowloading = true; // false in the penetration diagram indicates that the content loading is completed
this.isShowDetail = false; // False in the penetration diagram indicates that the content loading is completed
this.isShowExportPng = true / / the Save button is displayed
this.zoom = null;
}

componentDidMount() {
var treeData = [
{
"name": "China",
"children": [
{
"name": "Henan",
"children": [
{
"name": "Zhengzhou",
},
{
"name": "Hebi",
},
]
},
{
"name": "Shandong",
"children": [
{
"name": "Jinan",
},
{
"name": "Texas",
},
]
},
{
"name": "Shanghai",
"children": [
{
"name": "Pudong New Area",
}
]
},
]
}
];
let svgW = document.body.clientWidth
let svgH = document.body.clientHeight
//Square shape
this.diamonds = {
w: 175,
h: 68,
intervalW: 200,
intervalH: 150
}
/// /. size() sets the available size of the tree, so they may be compressed and overlapped according to the spacing between cousin nodes, cousin nodes, etc
/// / using. nodeSize() only means that each node should have such a large space, so they will never overlap!
this.layoutTree = d3.tree().nodeSize([this.diamonds.intervalW, this.diamonds.intervalH])
.separation(() => 0.5)

this.zoom = d3.zoom()
  .scaleExtent([0.5, 3])
  .on("zoom", ()=>{ // zoom event
      this.svg.attr("transform", `${d3.event.transform.translate(svgW / 4,200)} scale(${d3.zoomTransform(d3.select("svg").node()).k})`);
  });
this.svg = d3.select('#lgwTree').append('svg').attr('width', svgW).attr('height', svgH).attr('id', 'treesvg')
  .attr('xmlns', 'http://www.w3.org/2000/svg')
  .call(this.zoom)
  .on('dblclick.zoom', null)
  .attr('style', 'position: relative;z-index: 2;')
  .append('g').attr('id', 'gAll')
  .attr('transform', 'translate(' + (svgW / 4) + ',' + (200) + ')')

this.rootUp = d3.hierarchy(treeData[0])
this.update(this.rootUp);

}
diagonal (d) {
// console.log(d)
//The tree is shown to the right
return "M"+d.source.y+" "+d.source.x+
"L"+(d.source.y+120)+" "+(d.source.x)+
" L"+(d.source.y+120)+" "+(d.target.x)+" L"+
d.target.y+" "+(d.target.x);
//The tree is displayed downward
// return "M"+(d.source.x+10)+" "+d.source.y+
// "L"+(d.source.x+10)+" "+(d.target.y-50)+
// " L"+(d.target.x+10)+" "+(d.target.y-50)+" L"+
// (d.target.x+10)+" "+(d.target.y);
}
click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
this.update(d);
}
update(source) {
let _this=this;
if (source.parents === null) {
source.isOpen = !source.isOpen
}//Initialize deployment

let nodes = this.layoutTree(this.rootUp).descendants();
let links = this.layoutTree(this.rootUp).links();

// Normalize for fixed depth. Set the y coordinate point, each layer accounting for 180px
nodes.forEach(function(d) { d.y = d.depth * 180; });

// Update the nodes... Each node corresponds to a group
var node = this.svg.selectAll("g.node")
    .data(nodes, function(d) { return d.id || (d.id = ++this.i); });//data(): bind an array to the selection set, and each value of the array is bound to each element of the selection set

// Enter any new nodes at the parent's previous position
var nodeEnter = node.enter().append("g")  //Add a g to svg. G is an attribute in svg, which means group. It represents a group of things, such as lines, rects and circles. In fact, the coordinate axis is composed of these things.
    .attr("class", "node") //attr sets the html attribute and style sets the css attribute
    .attr('transform',d => "translate(" +( d.y) + "," + (d.x) + ")")
    .on("click", (d)=>{
      // this.click(d)
    });

// Create circle plus minus sign
nodeEnter.append('circle')
.attr('type', d => d.id || (d.id = 'text' + ++this.i))
.attr('r', d => d._children || d.children ? '9' : '0')
.attr('cy', d => 0)
.attr('cx', d => 55)
.attr('class', 'circle')
.attr('fill', '#fff ') / / fill colors of several circles in the initial display
.attr('stroke', d =>'#128BED ') / / border color of all circles
.on('click', function(d){
  _this.click(d)
  setTimeout(() => {
    if (this.innerHTML === '-') {
      d.isOpen = false
      this.innerHTML = '+'
    } else {
      d.isOpen = true
      this.innerHTML = '-'
    }
  }, 0)
})
// symbols for add and subtract
nodeEnter.append('svg:text')
  .attr('type', d => d.id || (d.id = 'text' + ++this.i))
  .on('click', function(d){
    _this.click(d)
    setTimeout(() => {
      if (this.innerHTML === '+') {
        d.isOpen = false
        this.innerHTML = '-'
      } else {
        d.isOpen = true
        this.innerHTML = '+'
      }
    }, 0)
  })
  .attr('x', 55)
    //eslint-disable-next-line
  .attr('dy', d => 5)
  .attr('text-anchor', 'middle')
  .attr('font-size', '18')
  .attr('fill', '#128BED ') / / color with minus sign inside the circle
  //eslint-disable-next-line
  .text(d => d.data.children?(d.isOpen?'+' : '-'): '')
nodeEnter.append("rect")
  .attr("x",-23)
  .attr("y", -10)
  .attr("width",70)
  .attr("height",22)
  .attr("rx",10)
  .style("fill", "#357CAE");//d represents data, that is, the data bound to an element.
// Add arrow
nodeEnter.append('marker')
      .attr("id","arrow")  
      .attr("markerUnits","strokeWidth")  
      .attr("markerWidth","12")  
      .attr("markerHeight","12")  
      .attr("viewBox","0 0 12 12")   
      .attr("refX",d=>{
        return '38'
      })  
      .attr("refY",6)  
      .attr("orient","auto")
      .attr('stroke-width', 2) //Arrow Width 
      .append('path')
      .attr('d', 'M2,0 L14,6 L2,12 L5,6 L2,0') // Path of arrow
      .attr('fill', '#128BED ') / / arrow color
//Add label
nodeEnter.append("text")
  .attr("x", function(d) { return d.children || d._children ? 13 : 13; })
  .attr("dy", "6")
  .attr("text-anchor", "middle")
  .text(d=>d.data.name)
  .attr('font-size', 12)
  .style("fill", "white")
  .attr('cursor', 'pointer')
  .style("fill-opacity", 1);
// Transition nodes to their new position
//node is the reserved data set, which adds transition animation to the graphics of the original data. The first is the position of the whole group
var nodeUpdate = node.transition()  //Start an animation transition
    .duration(duration)  //Transition delay time, which mainly sets the transition delay of circle nodes with slashes
    .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });

nodeUpdate.select("rect")
          .attr("x",-23)
          .attr("y", -10)
          .attr("width",70)
          .attr("height",22)
          .attr("rx",10)
          .style("fill", "#357CAE");

nodeUpdate.select("text")
  .attr("text-anchor", "middle")
    .style("fill-opacity", 1);

// Transition existing nodes to the parent's new position.
//Finally, process the missing data and add the missing animation
var nodeExit = node.exit().transition()
    .duration(duration)
    .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
    .remove();

// nodeExit.select("circle")
  //   .attr("r", 1e-6);
nodeExit.select("rect")
        .attr("x",-23)
        .attr("y", -10)
        .attr("width",70)
        .attr("height",22)
        .attr("rx",10)
        .style("fill", "#357CAE");

nodeExit.select("text")
  .attr("text-anchor", "middle")
    .style("fill-opacity", 1e-6);

// Update the links... Related to line operation
//Reprocess connection set
var link = this.svg.selectAll("path.link")
    .data(links, function(d) { return d.target.id; });



// Enter any new links at the parent's previous position.
//Add a new connection
link.enter().insert("path", "g")
    .attr("class", "link")
    .attr("d", (d)=> {
      return this.diagonal(d)
    })
  .attr('stroke', '#777 ') / / line color
  .style('fill-opacity', 0) // The transparency of the area offset from the line when the line is drawn
  .attr('marker-end', 'url(#arrow)');

// Transition links to their new position
//Add transition animation to reserved lines
link.transition()
    .duration(duration)
    .attr('d', d => this.diagonal(d))

// Transition existing nodes to the parent's new position.
//Add transition animation to missing lines
link.exit().transition()
    .duration(duration)
    .attr("d", (d) =>{
      return this.diagonal(d)
    })
    .remove();

// Stash the old positions for transition
nodes.forEach(function(d) {
  d.x0 = d.x;
  d.y0 = d.y;
});

}

//Save picture
downloadPng () {
const base64Data = document.getElementById('treesvg')
const result = base64Data.getBBox()
const width = result.width
const height = result.height
const top = result.y
const left=result.x
const backgroundColor = '#fff'
const opts = {
backgroundColor, / / create PNG with the given background color. The default is transparent
left: left - 50, / / specifies the left side of the viewbox location. The default is 0
top: top - 100, / / specifies the top of the viewbox location. The default is 0
width: width + 100, / / specifies the width of the image. If yes is given, or the boundary of the element width, or the CSS of the element width, or the calculated width of 0, the default is,.
//scale: 1, / / change the resolution of the output PNG. The default is 1, the same dimension as the source SVG.
height: height + 200, / / specifies the height of the image. If the given is, or the boundary of the element height, or the CSS of the element height, or the calculated height of 0, the default is,.
encoderType: 'image/png',
Encoderoptions: 1, / / the number between 0 and 1 indicates the image quality. The default value is 0.8
}
saveSvg.saveSvgAsPng(base64Data, ${'tree view'} ${'. PNG'}, opts)
}
//Refresh page
refresh=()=>{
console.log('refresh ');
d3.select("svg").call(this.zoom.transform, d3.zoomIdentity);
}
scaleBig(){
console.log('zoom in ');
this.zoom.scaleBy(d3.select("svg"), 1.1); / / the zoom event will be triggered after the method is executed
}
scaleSmall(){
console.log('zoom out ');
this.zoom.scaleBy(d3.select("svg"), 0.9); / / the zoom event will be triggered after the method is executed
}
render() {}
}

**Note: * * the code is redundant or insufficient, please give more advice...

Posted by Molarmite on Tue, 19 Oct 2021 22:10:03 -0700