添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
风流的沙滩裤  ·  Mybatis-Plus ...·  3 天前    · 
讲道义的松鼠  ·  Input Group | ...·  1 周前    · 
难过的盒饭  ·  Jmeter ...·  1 周前    · 
豪爽的帽子  ·  在C#项目中调用c++ ...·  1 月前    · 
淡定的地瓜  ·  [Server bundle]: add ...·  3 月前    · 
if (source?.children?.length) { showType === "down" ? this .setNewData( this .oldTreeItem, source.nodeId, []) : this .setNewDataUp( this .oldTreeItem, source.nodeId, []); source.children = []; setTimeout(() => { this .init2( this .oldTreeItem); }, 0 ); } else { // 展开 if (!source. data .children.length && source. data .investment) { const data = { id: source. data .id, //节点id nodeType: showType === "up" ? 1 : 2 , //获取节点id的父节点还是子节点 1 父节点 2 子节点 queryNode( data ).then((res) => { showType === "down" ? this .setNewData( this .oldTreeItem, source.nodeId, res. data ) : this .setNewDataUp( this .oldTreeItem, source.nodeId, res. data ); source.children = res. data ; setTimeout(() => { this .init2( this .oldTreeItem); }, 0 ); setNewData(oldTree, nodeId, data ) { if (!(oldTree.children instanceof Array)) return null ; for (let i of oldTree.children) { if (i.nodeId === nodeId) { i.children = JSON.parse(JSON.stringify( data )); return ; } else { this .setNewData(i, nodeId, data ); setNewDataUp(oldTree, nodeId, data ) { if (oldTree.nodeId === nodeId) { oldTree.parents = JSON.parse(JSON.stringify( data )); } else { let temptree = oldTree?.parents?.length ? oldTree.parents : oldTree.children; for (let i of temptree) { if (i.nodeId === nodeId) { i.children = JSON.parse(JSON.stringify( data )); return ; } else { this .setNewDataUp(i, nodeId, data );

2. 根节点不能展开收起操作

d3.hierarchy – 从给定的层次结构数据构造一个根节点并为各个节点指定深度等属性.

treeItem 数据举例

rootUp,rootDown 数据举例

初始化的时候,将全部数据分为 up , down 两个类型,在最终渲染后,根节点的type就是 down

所以无法区分根节点类型,添加不了扩展符号。

 init2(treeItem) {
   this.oldTreeItem = JSON.parse(JSON.stringify(treeItem));
   let upTree = null;
   let downTree = null;
   // 拷贝树的数据
   Object.keys(treeItem).map((item) => {
     if (item === "parents") {
       upTree = JSON.parse(JSON.stringify(treeItem));
       upTree.children = treeItem[item];
       upTree.parents = null;
     } else if (item === "children") {
       downTree = JSON.parse(JSON.stringify(treeItem));
       downTree.children = treeItem[item];
       downTree.parents = null;
   // hierarchy 返回新的结构 x0,y0初始化起点坐标
   this.rootUp = d3.hierarchy(upTree, (d) => d.children;);
   this.rootUp.x0 = 0;
   this.rootUp.y0 = 0;
   this.rootDown = d3.hierarchy(downTree, (d) => d.children);
   this.rootDown.x0 = 0;
   this.rootDown.y0 = 0;
   // 上 和 下 结构
   let treeArr = [
       data: this.rootUp,
       type: "up",
       data: this.rootDown,
       type: "down",
   treeArr.map((item) => {
      // 控制展示根节点上下两层数据
     item.data.data.children.forEach(this.collapse);
      // 渲染
     this.update(item.data, item.type, item.data);

结论:遇到根节点,如果有可扩展值就默认多展示一层,不去操作这个特殊的根节点。

3. 缩放拖拽抖动问题

根本原因是原来使用的方法在缩放时去记录了原始位置,对原位置进行操作,但拖拽时图片会从当前画布的(0,0)坐标开始滑动,视觉上给人一种抖动的感觉。

 // 缩放监听(写在svg初始化方法中)
  this.zoom = d3.zoom()
     .scaleExtent([1, 3]) 
     .on("zoom", (e) => {
       this.scaleData = d3.event.transform.k; // 缩放值
       this.svg.attr(
         "transform",
         d3.event.transform.translate(svgW / 2, svgH / 2)
   this.svg.call(this.zoom);
 // 核心代码
 setRange(type) {
   this.zoom.scaleBy(this.svg, type === "big" ? 1.2 : 0.8);

存在的问题:当缩放范围最小值小于1时,拖拽图片图片会在原地轻微抖动

解决方法:缩放最小值设置为1。

4. 图片下载不全

zoom有缩放移动功能,导致下载的图片有2种情况:

  • 下载当前视图的svg(只下载页面呈现部分)
  • 无视缩放,移动位置,下载整个svg
  • 缩放问题引起的缩放值取值不正确,导致下载图片,等比缩放时比例错误,下载图片不全。

    参数zoomClassName: 元素g , 包含可缩放移动的数据。

       svgDownloadAll(svg, zoomClassName) {
       //得到svg的真实大小
       var box = svg.getBBox(),
         x = box.x,
         y = box.y,
         width = box.width,
         height = box.height;
       if (zoomClassName) {
         //查找zoomObj
         var zoomObj = svg.getElementById(zoomClassName.replace(/\./g, ""));
         if (!zoomObj) {
           alert("zoomObj不存在");
           return false;
         /*------这里是处理svg缩放的--------*/
         var transformMath = zoomObj.getAttribute("transform"),
           scaleMath = zoomObj.getAttribute("transform");
         if (transformMath || scaleMath) {
           var transformObj = transformMath.match(
             /translate\(([^,]*),([^,)]*)\)/
           if (transformObj) {
             // 原缩放,移动值 反应用到svg的宽高上
             var translateX = transformObj[1],
               translateY = transformObj[2],
               scale = this.scaleData;
             x = (x - translateX) / scale;
             y = (y - translateY) / scale;
             width = width / Number(scale);
             height = height / Number(scale);
       //克隆svg
       var node = svg.cloneNode(true);
       //重新设置svg的width,height,viewbox
       node.setAttribute("width", width);
       node.setAttribute("height", height);
       node.setAttribute("viewBox", [x, y, width, height]);
       if (zoomClassName) {
         var zoomObj1 = node.getElementById(zoomClassName.replace(/\./g, ""));
         /*-------------清除缩放元素的缩放-------------*/
         zoomObj1.setAttribute("transform", "translate(0,0) scale(1)");
       this.downloadSvgFn(node);
    

    下载流程:

    将svg转成字符串 点击查看

    转换之后,将svg字符串变成image的src

    用canvas绘制image

    将dataurl 转成 blob

    模拟点击事件,下载blob

     downloadSvgFn(svg) {
       let that = this;
       var serializer = new XMLSerializer();
       var source =
         '<?xml version="1.0" standalone="no"?>\r\n' +
         serializer.serializeToString(svg);
       var image = new Image();
       image.src =
         "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source);
       image.onload = function () {
         var width = this.naturalWidth,
           height = this.naturalHeight;
         var canvas = document.createElement("canvas");
         canvas.width = width;
         canvas.height = height;
         var context = canvas.getContext("2d");
         context.rect(0, 0, width, height);
         context.fillStyle = "#fff";
         context.fill();
         context.drawImage(image, 0, 0);
         var imgSrc = canvas.toDataURL("image/jpg", 1);
         that.base64DownloadFile(imgSrc);
     base64DownloadFile(content) {
       let aLink = document.createElement("a");
       let blob = this.base64ToBlob(content); //new Blob([content]);
       let evt = document.createEvent("HTMLEvents");
       evt.initEvent("click", true, true); //initEvent 不加后两个参数在FF下会报错  事件类型,是否冒泡,是否阻止浏览器的默认行为
       aLink.download = "股权架构";
       aLink.href = URL.createObjectURL(blob);
       aLink.click();
     base64ToBlob(code) {
       let parts = code.split(";base64,");
       let contentType = parts[0].split(":")[1];
       let raw = window.atob(parts[1]);
       let rawLength = raw.length;
       let uInt8Array = new Uint8Array(rawLength);
       for (let i = 0; i < rawLength; ++i) {
         uInt8Array[i] = raw.charCodeAt(i);
       return new Blob([uInt8Array], { type: contentType });
    

    5. 下载图片的连接线过粗

    设置一下fill-opacity线条透明度即可。

    6. 全屏与退出全屏

       handleFullScreen() {
        let element = document.documentElement;
        // 判断是否已经是全屏
        // 如果是全屏,退出
        if (this.fullscreen) {
          if (document.exitFullscreen) {
            document.exitFullscreen();
          } else if (document.webkitCancelFullScreen) {
            document.webkitCancelFullScreen();
          } else if (document.mozCancelFullScreen) {
            document.mozCancelFullScreen();
          } else if (document.msExitFullscreen) {
            document.msExitFullscreen();
        } else {
          // 否则,进入全屏
          if (element.requestFullscreen) {
            element.requestFullscreen();
          } else if (element.webkitRequestFullScreen) {
            element.webkitRequestFullScreen();
          } else if (element.mozRequestFullScreen) {
            element.mozRequestFullScreen();
          } else if (element.msRequestFullscreen) {
            // IE11
            element.msRequestFullscreen();
        this.fullscreen = !this.fullscreen;
    

    7. 连接线被遮挡问题(曲线替换为直线)

    最开始使用的是贝塞尔曲线,因为随着层级加深,倾斜角度也越来越高,线条会被矩形模块遮挡,所以替换了直线展示。

  • M x y : 移动到指定坐标,x,y分别为轴坐标点,即为起点
  • C x1 y1 x2 y2 x y : 三次贝塞尔曲线
  • 当前点为起点,xy为终点,起点和x1y1控制曲线起始的斜率,终点和x2y2控制结束的斜率。
  •  diagonal (s, d, showtype) {
        let path
        if (showtype === 'up') {
          path = `M ${s.x} ${-s.y + 24}
        C${s.x} -${(s.y + d.y) * 0.45},
         ${s.x} -${(s.y + d.y) * 0.45},
          ${d.x} -${d.y}`;
        } else {
          path = `M ${s.x} ${s.y}
        C${s.x} ${(s.y + d.y) * 0.45},
         ${s.x} ${(s.y + d.y) * 0.45},
          ${d.x} ${d.y}`;
        return path;
    
  • L x y :在初始位置(M 画的起点)和xy确定的坐标画一条线。
  •  diagonal(s, d, showtype) {
          let path;
          const halfDistance = (d.y - s.y) / 2;
          const halfY = s.y + halfDistance;
          if (showtype === "up") {
            path = `M${s.x} ${-s.y + 30}
            L${s.x},${-halfY} L${d.x},${-halfY} L${d.x}, ${-d.y}`;
          } else {
            path = `M${s.x} ${s.y}
            L${s.x},${halfY} L${d.x},${halfY} L${d.x},${d.y}`;
          return path;
    

    svg path路径指令

    8. 弹窗展示公司信息

    点击公司,展示公司详情。

    核心知识点:svgRect.getBoundingClientRect()

     showDialog(d) {
          if (d.data.subjectType === 1 || d.data.subjectType === 3) {
            const FormDatas = new FormData();
            FormDatas.append("id", d.data.id);
            this.$axios({
              url: "/equityrelation/queryCompanyInfoByNodeId",
              method: "post",
              data: FormDatas,
            }).then((res) => {
              if (res.data.code === 200) {
                const svgRect = document.querySelector(`rect[type="${d.nodeId}"]`);
                var rt = svgRect.getBoundingClientRect();
                document
                  .getElementById("dialog")
                  .setAttribute(
                    "style",
                    `display:block;top:${rt.bottom}px;left:${rt.right}px`
                const {
                  companyName,
                  companyZhName,
                  companyRegNo,
                  registerDate,
                  registerAmount,
                  registerCurrencyCode,
                  auth,
                } = res.data.data;
                this.dialogData = {
                  auth,
                  companyName,
                  companyZhName,
                  companyRegNo,
                  registerDate,
                  registerAmount,
                  registerCurrencyCode,
              } else {
                this.$message.error(res.data.message);
    

    四、从0开发一个股权穿透图

    举例的代码是来自 d3 股权穿透图干货这篇内容,运行可直接看效果,写的很好。

    相关知识点:

  • d3.tree – 创建一个新的整齐(同深度节点对齐)的树布局.
  • d3.zoom – 创建一个缩放交互.
  • d3.select – 从文档中选取一个元素.
  • d3.hierarchy – 从给定的层次结构数据构造一个根节点并为各个节点指定深度等属性.
  • zoom.scaleExtent – 设置可缩放系数大小.
  • transform.translate – 根据指定的值平移当前坐标变换.
  • transition.remove – 在过渡结束后移除选中的元素.
  • tree.nodeSize – 设置节点尺寸.
  • tree.separation – 设置两个相邻的节点之间的间距.
  • node.descendants – 从当前节点开始返回其后代节点数组.
  • selection.selectAll -从每个被选中的元素中选择多个后代元素.
  • selection.data – 将元素与数据绑定.
  • selection.enter – 获取需要插入的选择集(数据个数大于元素个数)的占位符.
  • selection.exit – 获取多余的元素的选择集(数据个数小于元素个数).
  • selection.on – 添加或移除事件监听器.
  • selection.attr – 设置或获取属性.
  • selection.style – 获取或设置样式属性.
  • selection.text – 设置或获取文本内容.
  • selection.append – 创建、添加并返回一个新的元素.
  • selection.insert – 创建、插入并返回一个新的元素.
  • selection.remove – 从文档中移除元素.
  • transition.duration – 指定每个过渡元素的过渡时间(毫秒).
  • <template>
        <div class="penetrate-chart">
            <div class="bt-group">
                <button class="reset" @click="resetSvg">重置</button>
            </div>
        </div>
    </template>
    <script>
      // 过渡时间
      const DURATION = 0
      // 加减符号半径
      const SYMBOLA_S_R = 9
      // 公司
      const COMPANY = 0
      const PERSON = 1
      export default {
        props: {},
        components: {},
        data () {
          return {
            layoutTree: '',
            diamonds: '',
            d3: this.$d3,
            i: 0,
            hasChildNodeArr: [],
            originDiamonds: '',
            diagonalUp: '',
            diagonalDown: '',
            tree: {"name":"多多包","children":[{"name":"一卡通公司","type":0},{"name":"一卡通公司2","type":0,"children":[{"name":"小公司","type":0,"children":[{"name":"小小小","type":0,"children":[{"type":1,"name":"笑小下"}]}]},{"type":0,"name":"小公司2"}]},{"name":"一卡通公司2333","type":0,"children":[{"type":0,"name":"小公司"},{"type":0,"name":"小公司2"}]},{"type":0,"name":"一卡通公司2222"}],"parents":[{"name":"大公司","type":0,"children":[{"name":"发发委","type":0,"money":"780万元","children":[{"type":0,"money":"780万元","name":"123"}]},{"name":"123发发委","money":"780万元","type":0,"children":[{"money":"780万元","type":0,"name":"123"}]}]},{"name":"多多网","money":"780万元","type":0,"children":[{"type":0,"money":"780万元","name":"发哈哈"}]},{"name":"龙龙投资","money":"780万元","type":0,"children":[{"type":1,"money":"780万元","name":"王林"},{"type":1,"money":"780万元","name":"张峰"},{"type":1,"money":"780万元","name":"侯明"}]}]},
            rootUp: '',
            rootDown: '',
            svg: ''
        mounted () {
          this.init()
        methods: {
          init () {
            let d3 = this.d3
            let svgW = document.body.clientWidth
            let svgH = 500
            // 方块形状
            this.diamonds = {
              w: 145,
              h: 68,
              intervalW: 200,
              intervalH: 150
            // 源头对象
            this.originDiamonds = {
              w: 190
            this.layoutTree = d3.tree().nodeSize([this.diamonds.intervalW, this.diamonds.intervalH]).separation(() => 1);
            // 主图
            this.svg = d3.select('#app').append('svg').attr('width', svgW).attr('height', svgH).attr('id', 'treesvg')
              .call(d3.zoom().scaleExtent([0, 5]).on('zoom', () => {
                // 设置缩放位置以及平移初始位置
                this.svg.attr('transform', d3.event.transform.translate(svgW / 2, svgH / 2));
              .attr('style', 'position: relative;z-index: 2;')
              .append('g').attr('id', 'g').attr('transform', 'translate(' + (svgW / 2) + ',' + (svgH / 2) + ')');
            let upTree = null
            let downTree = null
     // 拷贝树的数据
            Object.keys(this.tree).map(item => {
              if (item === 'parents') {
                upTree = JSON.parse(JSON.stringify(this.tree))
                upTree.children = this.tree[item]
                upTree.parents = null
     } else if (item === 'children') {
                downTree = JSON.parse(JSON.stringify(this.tree))
                downTree.children = this.tree[item]
                downTree.parents = null
            // hierarchy 返回新的结构 x0,y0初始化起点坐标
            this.rootUp = d3.hierarchy(upTree, d => d.children);
            this.rootUp.x0 = 0
            this.rootUp.y0 = 0
            this.rootDown = d3.hierarchy(downTree, d => d.children);
            this.rootDown.x0 = 0
            this.rootDown.y0 = 0;
            // 上 和 下 结构
            let treeArr = [
                data: this.rootUp,
                type: 'up'
                data: this.rootDown,
                type: 'down'
            treeArr.map(item => {
              item.data.children.forEach(this.collapse);
              this.update(item.data, item.type, item.data)
           *[update 函数描述], [click 函数描述]
           *  @param  {[Object]} source 第一次是初始源对象,后面是点击的对象
           *  @param  {[String]} showtype up表示向上 down表示向下
           *  @param  {[Object]} sourceTree 初始源对象
          update (source, showtype, sourceTree) {
            let _this = this
            if (source.parents === null) {
              source.isOpen = !source.isOpen
            let nodes
            if (showtype === 'up') {
              nodes = this.layoutTree(this.rootUp).descendants()
            } else {
              nodes = this.layoutTree(this.rootDown).descendants()
            let links = nodes.slice(1);
            nodes.forEach(d => {
              d.y = d.depth * this.diamonds.intervalH;
            let node = this.svg.selectAll('g.node' + showtype)
              .data(nodes, d => d.id || (d.id = showtype + ++this.i));
            let nodeEnter = node.enter().append('g')
              .attr('class', d => showtype === 'up' && !d.depth ? 'hide-node' : 'node' + showtype)
              .attr('transform', d => showtype === 'up' ? 'translate(' + d.x + ',' + -(d.y) + ')' : 'translate(' + d.x + ',' + d.y + ')')
            // 创建矩形
            nodeEnter.append('rect')
              .attr('type', d => d.id)
              .attr('width', d => d.depth ? this.diamonds.w : this.originDiamonds.w)
              .attr('height', d => d.depth ? (d.type === COMPANY ? this.diamonds.h : this.diamonds.h - 10) : 30)
              .attr('x', d => d.depth ? -this.diamonds.w / 2 : -this.originDiamonds.w / 2)
              .attr('y', d => d.depth ? showtype === 'up' ? -this.diamonds.h / 2 : 0 : -15)
              .attr('stroke', d => d.data.type === COMPANY || !d.depth ? '#FD7D00' : '#7A9EFF')
              .attr('stroke-width', 1)
              .attr('rx', 5)
              .attr('ry', 5)
              .style('fill', d => {
                if (d.data.type === COMPANY || !d.depth) {
                  return d._children ? '#FFF1D7' : (d.depth ? '#fff' : '#FD7D00')
                } else if (d.data.type === PERSON) {
                  return d._children ? '#fff' : (d.depth ? '#fff' : '#7A9EFF')
            // 创建圆 加减
            nodeEnter.append('circle')
              .attr('type', d => d.id || (d.id = showtype + 'text' + ++this.i))
              .attr('r', (d) => d.depth ? (this.hasChildNodeArr.indexOf(d) === -1 ? 0 : SYMBOLA_S_R) : 0)
              .attr('cy', d => d.depth ? showtype === 'up' ? -(SYMBOLA_S_R + this.diamonds.h / 2) : this.diamonds.h : 0)
              .attr('cx', 0)
              .attr('fill', d => d.children ? '#fff' : '#FD7D00')
              .attr('stroke', d => d._children || d.children ? '#FD7D00' : '')
              .on('click', function (d) {
                _this.click(d, showtype, sourceTree)
                setTimeout(() => {
                  if (document.querySelector(`text[type="${d.id}"]`).innerHTML === '-') {
                    d.isOpen = false
                    this.innerHTML = '+'
                    this.setAttribute('fill', '#FD7D00')
                    document.querySelector(`text[type="${d.id}"]`).setAttribute('fill', '#fff')
                    document.querySelector(`rect[type="${d.id}"]`).setAttribute('style', 'fill:#FFF1D7')
                    document.querySelector(`text[type="${d.id}"]`).innerHTML = '+'
                  } else {
                    d.isOpen = true
                    this.setAttribute('fill', '#fff')
                    document.querySelector(`text[type="${d.id}"]`).setAttribute('fill', '#FD7D00')
                    document.querySelector(`rect[type="${d.id}"]`).setAttribute('style', 'fill:#fff')
                    document.querySelector(`text[type="${d.id}"]`).innerHTML = '-'
                }, DURATION)
            // 持股比例
            nodeEnter.append('g')
              .attr('transform', () => 'translate(0,0)')
              .append('text')
              .attr('class', d => !d.depth ? 'proportion-hide' : 'proportion')
              .attr('x', d => d.x > 0 ? (showtype === 'up' ? -30 : 30) : 30)
              .attr('y', showtype === 'up' ? this.diamonds.h : -20)
              .attr('text-anchor', 'middle')
              .attr('fill', d => d.data.type === COMPANY ? '#FD7D00' : '#7A9EFF')
              .text(d => '30%');
            // 公司名称
            // y轴 否表源头的字体距离
            nodeEnter.append('text')
              .attr('class', 'text-style-name')
              .attr('x', 0)
              .attr('y', showtype === 'up' ? -this.diamonds.h / 2 : 0)
              .attr('dy', d => d.depth ? (d.data.name.length > 9 ? '1.5em' : '2em') : '.3em')
              .attr('text-anchor', 'middle')
              .attr('fill', d => d.depth ? '#465166' : '#fff')
              .text(d => (d.data.name.length > 9) ? d.data.name.substr(0, 9) : d.data.name);
            // 名称过长 第二段
            nodeEnter.append('text')
              .attr('class', 'text-style-name')
              .attr('x', 0)
              .attr('y', showtype === 'up' ? -this.diamonds.h / 2 : 0)
              .attr('dy', d => d.depth ? '3em' : '.3em')
              .attr('text-anchor', 'middle')
              .attr('fill', d => d.depth ? '#465166' : '#fff')
              .text(d => d.data.name.substr(9, d.data.name.length));
            // 认缴金额
            nodeEnter.append('text')
              .attr('class', 'text-style-money')
              .attr('x', 0)
              .attr('y', showtype === 'up' ? -this.diamonds.h / 2 : 0)
              .attr('dy', d => d.data.name.substr(9, d.data.name.length).length ? '5em' : '4em')
              .attr('text-anchor', 'middle')
              .attr('fill', d => d.depth ? '#465166' : '#fff')
              .text(d => d.data.money);
            * 绘制箭头
            * @param  {string} markerUnits [设置为strokeWidth箭头会随着线的粗细发生变化]
            * @param {string} viewBox 坐标系的区域
            * @param {number} markerWidth,markerHeight 标识的大小
            * @param {string} orient 绘制方向,可设定为:auto(自动确认方向)和 角度值
            * @param {number} stroke-width 箭头宽度
            * @param {string} d 箭头的路径
            * @param {string} fill 箭头颜色
            * @param {string} id resolved0表示公司 resolved1表示个人
            * 直接用一个marker达不到两种颜色都展示的效果
            nodeEnter.append('marker')
              .attr('id', showtype + 'resolved0')
              .attr('markerUnits', 'strokeWidth')
              .attr('markerUnits', 'userSpaceOnUse')
              .attr('viewBox', '0 -5 10 10')
              .attr('markerWidth', 12)
              .attr('markerHeight', 12)
              .attr('orient', '90')
              .attr('refX', () => showtype === 'up' ? '-5' : '15')
              .attr('stroke-width', 2)
              .attr('fill', 'red')
              .append('path')
              .attr('d', 'M0,-5L10,0L0,5')
              .attr('fill', '#FD7D00');
            nodeEnter.append('marker')
              .attr('id', showtype + 'resolved1')
              .attr('markerUnits', 'strokeWidth')
              .attr('markerUnits', 'userSpaceOnUse')
              .attr('viewBox', '0 -5 10 10')
              .attr('markerWidth', 12)
              .attr('markerHeight', 12)
              .attr('orient', '90')
              .attr('refX', () => showtype === 'up' ? '-5' : '15')
              .attr('stroke-width', 2)
              .attr('fill', 'red')
              .append('path')
              .attr('d', 'M0,-5L10,0L0,5')
              .attr('fill', '#7A9EFF');
            // 将节点转换到它们的新位置。
            let nodeUpdate = node.transition()
              .duration(DURATION)
              .attr('transform', d => showtype === 'up' ? 'translate(' + d.x + ',' + -(d.y) + ')' : 'translate(' + d.x + ',' + (d.y) + ')');
            // 代表是否展开的+-号,function this指向当前dom
            nodeEnter.append('svg:text')
              .attr('type', d => d.id || (d.id = showtype + 'text' + ++this.i))
              .on('click', function (d) {
                _this.click(d, showtype, sourceTree)
                setTimeout(() => {
                  if (this.innerHTML === '-') {
                    d.isOpen = false
                    this.innerHTML = '+'
                    this.setAttribute('fill', '#fff')
                    document.querySelector(`circle[type="${d.id}"]`).setAttribute('fill', '#FD7D00')
                    document.querySelector(`rect[type="${d.id}"]`).setAttribute('style', 'fill:#FFF1D7')
                  } else {
                    d.isOpen = true
                    this.innerHTML = '-'
                    this.setAttribute('fill', '#FD7D00')
                    document.querySelector(`circle[type="${d.id}"]`).setAttribute('fill', '#fff')
                    document.querySelector(`rect[type="${d.id}"]`).setAttribute('style', 'fill:#fff')
                }, DURATION)
              .attr('x', 0)
              .attr('dy', d => d.depth ? (showtype === 'up' ? -(SYMBOLA_S_R / 2 + this.diamonds.h / 2) : this.diamonds.h + 4) : 0)
              .attr('text-anchor', 'middle')
              .attr('fill', d => d._children ? '#fff' : '#FD7D00')
              .text(d => this.hasChildNodeArr.indexOf(d) !== -1 ? (source.depth && d.isOpen ? '-' : '+') : '');
            // 将退出节点转换到父节点的新位置.
            let nodeExit = node.exit().transition()
              .duration(DURATION)
              .attr('transform', () => showtype === 'up' ? 'translate(' + source.x + ',' + -(source.y) + ')' : 'translate(' + source.x + ',' + (parseInt(source.y)) + ')')
              .remove();
            nodeExit.select('rect')
              .attr('width', this.diamonds.w)
              .attr('height', this.diamonds.h)
              .attr('stroke', 'black')
              .attr('stroke-width', 1);
            // 修改线条
            let link = this.svg.selectAll('path.link' + showtype)
              .data(links, d => d.id);
            // 在父级前的位置画线。
            let linkEnter = link.enter().insert('path', 'g')
              .attr('class', 'link' + showtype)
              .attr('marker-start', d => `url(#${showtype}resolved${d.data.type})`)// 根据箭头标记的id号标记箭头
              .attr('stroke', d => d.data.type === COMPANY ? '#FD7D00' : '#7A9EFF')
              .style('fill-opacity', 1)
              .attr('d', () => {
                let o = {x: source.x0, y: source.y0};
                return _this.diagonal(o, o, showtype)
            let linkUpdate = linkEnter.merge(link);
            // 过渡更新位置.
            linkUpdate.transition()
              .duration(DURATION)
              .attr('d', d => _this.diagonal(d, d.parent, showtype));
            // 将退出节点转换到父节点的新位置
            link.exit().transition()
              .duration(DURATION)
              .attr('d', () => {
                let o = {
                  x: source.x,
                  y: source.y
                return _this.diagonal(o, o, showtype)
              }).remove();
            // 隐藏旧位置方面过渡.
            nodes.forEach(d => { d.x0 = d.x; d.y0 = d.y });
          // 拷贝到_children 隐藏1排以后的树
          collapse (source) {
            if (source.children) {
              source._children = source.children;
              source._children.forEach(this.collapse);
              source.children = null;
              this.hasChildNodeArr.push(source);
          click  (source, showType, sourceTree) {
            // 不是起点才能点
            if (source.depth) {
              if (source.children) {
                source._children = source.children;
                source.children = null;
              } else {
                source.children = source._children;
                source._children = null;
              this.update(source, showType, sourceTree)
          diagonal (s, d, showtype) {
            let path
            if (showtype === 'up') {
              path = `M ${s.x} ${-s.y + 24}
            C${s.x} -${(s.y + d.y) * 0.45},
             ${s.x} -${(s.y + d.y) * 0.45},
              ${d.x} -${d.y}`;
            } else {
              path = `M ${s.x} ${s.y}
            C${s.x} ${(s.y + d.y) * 0.45},
             ${s.x} ${(s.y + d.y) * 0.45},
              ${d.x} ${d.y}`;
            return path;
          resetSvg () {
            this.d3.select('#treesvg').remove()
            this.init()
    </script>
    <style lang="scss">
        .penetrate-chart {
            .bt-group{
                position: fixed;
                z-index: 999;
                right: 15px;
                bottom: 15px;
                button{
                    width:88px;
                    height:32px;
                    display: block;
                    border-radius:18px;
                    font-size:14px;
                    font-family:PingFangSC-Medium;
                    font-weight:500;
                    line-height:20px;
                .save{
                    background:rgba(255,168,9,1);
                    color:rgba(255,255,255,1);
                .reset{
                    margin-top: 8px;
                    color: rgba(255, 168, 9, 1);
                    background: white;
                    border:1px solid rgba(255,168,9,1);
        #treesvg{
            display: block;
            margin: auto;
                .linkup, .linkdown {
                    fill: none;
                    stroke-width: 1px;
                .text-style-name{
                    font-size:12px; /*no*/
     font-family:PingFangSC-Medium;
                    font-weight:500;
                .text-style-money{
                    font-size:10px; /*no*/
     font-family:PingFangSC-Regular;
                    font-weight:400;
                    color:rgba(70,81,102,1)
                .proportion{
                    font-size:10px;
                    font-family:PingFangSC-Regular;
                    font-weight:400;
            .proportion-hide, .hide-node{
                display: none;
    </style>
    

    参考文档: d3.js官网 d3 股权穿透图干货 d3.js中svg的下载5分钟看懂svg path路径的所有命令