svg text文本换行

1.svg text文本换行的方法.

常用的有两种方法:

方法一. 使用 text 添加多个 tspan 绘制文字

用 tspan 把 text 拆成多行,重新计算每个tspan 的 y 坐标。

<svg xmlns="http://www.w3.org/2000/svg">
  <text font-size="14">
    <tspan x="0" y="10">医药制造业国家</tspan>
    <tspan x="0" y="28">统计局分类</tspan>
  </text>
</svg>

d3 代码中添加:

// 给text标签加唯一的class,以便后面为每个拆分的文字添加tspan
let texts = g.selectAll('g.' + type + 'Node')
              .append('text')
              .attr('class', function (d) {
                return 'text-id' + d.data.id.toString().replace(/\./g, '')
              })
              .attr('width', rectWidth)
              .attr('height', rectHeight)
              .attr('x', 0)
              .attr('y', 5)
              .style('font-size',fontSize+'px')

// 设置text文字自动换行
texts.each(item => {
  var text = g.selectAll('text.text-id' + item.data.id.toString().replace(/\./g, ''));
  item.data.names = insertEnter(item.data.name, rectWidth);
  let x = +text.attr('x'),
    y = +text.attr('y');
  let lineHight = fontSize + 4;
  for (let i = 0; i < item.data.names.length; i++) {
    text
      .append("tspan")
      .attr('x', x)
      .attr('y', function (d) {
        return y + lineHight * i
      })
      .text(function (d) {
        return d.data.names[i];
      })
      .append('title')
      .text(d => d.data.title)
  }
})

方法二. 使用 foreignObject 添加 DOM 节点绘制文字(IE浏览器不支持)

用 foreignObject 包裹 DOM 元素,利用 DOM 的文本布局能力自动处理换行。

<svg xmlns="http://www.w3.org/2000/svg">
  <foreignObject width="120" height="50">
      <body xmlns="http://www.w3.org/1999/xhtml">
        <p style="font-size:14px;margin:0;">医药制造业国家统计局分类</p>
      </body>
    </foreignObject>
</svg>

d3 代码中添加:

g.selectAll('g.' + type + 'Node')
  .append("foreignObject")
  .attr('width',rectWidth)
  .attr('height',rectHeight)
  .attr('x',0)
  .attr('y',-15)
  // 必须加 xhtml: 前缀,否则文字不显示
  .append('xhtml:p')
  .attr('title',function(d){
    return d.data.title
  })
  .style('width',rectWidth)
  .style('height',rectHeight)
  .style('box-sizing','border-box')
  .style('color','#000')
  .style('font-size',fontSize+'px')
  .style('text-align',function(d){
    return 'center'
  })
  .text(function (d) {
      return  d.data.name ;
  })  

2. 举个完整的栗子

<html>
<head>
  <meta charset="utf-8">
  <title>d3-text文字换行</title>
</head>
<body>
  <div id="svg" style="width: 100%;height: 100%;"></div>
  <script src="https://cdn.bootcdn.net/ajax/libs/d3/5.16.0/d3.min.js"></script>
  <script>
    // 节点宽度
    var rectWidth = 110;
    // 节点高度
    var rectHeight = 46;
    // 箭头长度
    let markerWidth = 10;
    // 树状图数据
    var data = getData();

    treeInit();

    // 树状图数据
    function getData(){
      let data = [
        {"id":'1',"name":"医药制造业国家统计局分类",clickData:{url:"https://baike.baidu.com/item/%E5%8C%BB%E8%8D%AF%E5%88%B6%E9%80%A0%E4%B8%9A/9217195?fr=aladdin"},"title": "",
          "children":[
            {"id":'1.1',"name":"化学药品原料药制造",clickData:{url:""},"title": "指供进一步加工化学药品制剂、生物药品制剂所需的原料药生产",},
            {"id":'1.2',"name":"化学药品制剂制造",clickData:{url:"https://baike.baidu.com/item/%E5%8C%96%E5%AD%A6%E8%8D%AF%E5%93%81%E5%88%B6%E5%89%82%E5%88%B6%E9%80%A0/7411169?fr=aladdin"},"title": "指直接用于人体疾病防治、诊断的化学药品制剂的制造",},
            {"id":'1.3',"name":"中药饮片加工",clickData:{url:""},"title": "指对采集的天然或人工种植、养殖的动物、植物和矿物的药材部位进行加工、炮制,使其符合中药处方调剂或中成药生产使用的活动",},
            {"id":'1.4',"name":"中成药生产",clickData:{url:""},"title": "指直接用于人体疾病防治的传统药的加工生产活动",},
            {"id":'1.5',"name":"兽用药品制造",clickData:{url:""},"title": "指用于动物疾病防治医药的制造",},
            {"id":'1.6',"name":"生物药品制品制造",clickData:{url:""},"title": "指利用生物技术生产生物化学药品、基因工程药物和疫苗的制剂生产活动",
              "children":[
                {"id":'1.6.1',"name":"生物药品制造",clickData:{url:""},"children":null,"title": "指利用生物技术生产生物化学药品的生产活动",},
                {"id":'1.6.2',"name":"基因工程药物和疫苗制造",clickData:{url:""},"children":null,"title": "",},
              ]
            },
            {"id":'1.7',"name":"卫生材料及医药用品制造",clickData:{url:""},"title": "指卫生材料、外科敷料以及其他内、外科用医药制品的制造",},
            {"id":'1.8',"name":"药用辅料及包装材料",clickData:{url:""},"title": "指药品辅料和包装材料等制造",},
          ]
        }
      ];
      return data;
    }
    // 树状图初始化
    function treeInit(){
      let svg_container = document.getElementById('svg');
      let width = window.getComputedStyle(svg_container).width.replace('px', '');
      let height = window.getComputedStyle(svg_container).height.replace('px', '');
      // 缩放比例
      let zoom = height / 650;
      let ratio = (data.height ? data.height : 100) / 100 * 0.8;
      let scaleRat = (width >= 1300 ? 1 : 0.88) * zoom;
      let svg = d3.select(`#svg`)
        .append('svg')
        .attr('width', width)
        .attr('height', height)

      // 树状图默认从上到下绘制,转成从左至右绘制后,x 代表纵轴,y 代表横轴.
      let g = svg.append('g')
        .attr('transform', `translate(${width / 3 - 160 * zoom}, ${height / 2 + 40 * zoom * ratio}) scale(${scaleRat})`)
      
      drawMark(g);
      for (let key in data) {
        drawTree(data[key], g, key)
      }
    }
    //画可复用的三角形
    function drawMark(g){
      g.append('defs')
        .append('marker')
        .attr('id', 'markerArrow')
        .attr('markerWidth', markerWidth)
        .attr('markerHeight', markerWidth)
        .attr('refX', '2')
        .attr('refY', '4')
        .attr('orient', 'auto')
        .append('path')
        .attr('d', 'M10,4 L0,0 L0,8 z')
        .attr('fill', '#6F6F6F')
    }
    // 绘制树状图
    function drawTree(data, g, key) {
      let tree = d3.tree()
        .nodeSize([rectHeight + 10, rectWidth + 50])
      // 初始化json数据
      let hierarchyData = d3.hierarchy(data)
      // 初始化树状图
      let treeData = tree(hierarchyData)
      // 获取节点
      let nodes = treeData.descendants();
      let type = 'key' + key;
      let idx = 0;
      nodes.forEach((element, index) => {
        // 可以对节点进行处理
      });
      // 获取边,也就是连线
      let links = treeData.links()
      if (data.showType != 'rect-box') {
        drawLink(g, links, type)
      }

      // 绘制文本和节点
      drawNode(g, nodes, type)
      drawText(g, type)
    }
    // 绘制节点
    function drawNode(g, nodes, type, title) {
      nodes = data.noRoot ? nodes.slice(1, nodes.length) : nodes
      g.selectAll('g.' + type + 'Node')
        .data(nodes)
        .enter().append('g')
        .attr('class', function (d) {
          var result = type + 'Node Node';
          return result
        })
        .attr('transform', function (d) { return 'translate(' + d.y + ',' + d.x + ')' })
        .on('click', function (evt) {
          
          let eventData = evt.data.clickData;
          if (eventData && eventData.url) {
            // 有url链接可点击跳转
            window.open(eventData.url, '_block');
          } 
        });
      g.selectAll('g.' + type + 'Node').append("rect")
        .attr('y', function (d) {
          return -15
        })
        .attr('width', function (d) {
          if (d.data.rectWidth) {
            return d.data.rectWidth
          }
          return rectWidth
        })
        .attr('height', function (d) {
          return rectHeight
        })
        .attr('class', function (d) {
          if (d.data.clickData && d.data.clickData.type) {
            return 'Select'
          }
        })
        .attr("rx", 2)
        // 矩形背景色以及边框颜色宽度
        .attr('fill', d => {
          if (d.data.color) {
            return d.data.color
          }
          if(!d.data.name){
            return 'rgba(255,255,255,0)'
          }
          return 'rgba(208,239,255,1)'
        })
        .append("title")
        .text(d => d.data.title)
    }
    // 绘制连线
    function drawLink(g, links, type) {
      let arrow = data.arrowReverse ? 'marker-end' : 'marker-start';
      let link = g.selectAll('g.' + type + 'Link').data(links)
      link.enter().append('path')
        .attr('class', type + 'Link')
        .attr('fill', 'none')
        .attr('stroke-width', 1)
        .attr('stroke', '#858585')
        .attr(arrow, function (d) {
          return d.source.data.noArrow ? '' : 'url(#markerArrow)'
        })
        .attr('d', function (d) {
          if (d.target.data.noLink) {
            return ''
          }
          let sourceX = d.source.x + rectHeight/5,
            sourceY = d.source.y + rectWidth,
            targetX = d.target.x + rectHeight/5,
            targetY = d.target.y,
            trans = 24;
          if (d.target.data.customLink) {
            return d.target.data.customLink
          }
          return `M${sourceY + markerWidth - 2},${sourceX}H${sourceY}H${targetY - 15}V${targetX}H${targetY}`

        })
        .attr('id', (d, i) => 'my_path' + i)
      link.enter().append('text')
        .attr('text-anchor', 'mid')
        .attr('class', 'link-text')
        .style('font-size', '12px')
        .append('textPath')
        .attr('xlink:href', (d, i) => '#my_path' + i)
        .attr('startOffset', '10%')
        .text(function (d) {
          if (d.target.data.preLinkLabel) {
            return d.target.data.preLinkLabel
          }
        })
    }
    // 绘制文本
    function drawText(g, type) {
      let fontSize = 14;
      // 方法一 使用 text,手动设置换行
      appendText(g,type,fontSize);

      // 方法二 使用 foreignObject 插入 DOM 节点
      // IE 浏览器不支持
      // appendXhtml(g,type,fontSize);
      

    }
    // 使用 text 添加多个 tspan 绘制文字(方法一)
    function appendText(g,type,fontSize){
      // 给text标签加唯一的class,以便后面为每个拆分的文字添加tspan
      let texts = g.selectAll('g.' + type + 'Node')
        .append('text')
        .attr('class', function (d) {
          return 'text-id' + d.data.id.toString().replace(/\./g, '')
        })
        .attr('width', function (d) {
          let rectWidth1 = d.data.rectWidth ? d.data.rectWidth : rectWidth;
          return rectWidth1
        })
        .attr('height', rectHeight)
        .attr('x', function (d) {
          let rectWidth1 = d.data.rectWidth ? d.data.rectWidth : rectWidth;
          let nameLen = d.data.name.length * fontSize;
          let x = 0;
          if (nameLen > rectWidth1) {
            x = 5
          } else {
            x = (rectWidth1 - nameLen) / 2
          }
          return x
        })
        .attr('y', 5)
        .style('font-size',fontSize+'px')
        // 可跳转的链接加样式
        .style('text-decoration', function (d) {
          return d.data.clickData && d.data.clickData.url ? 'underline' : 'none'
        })
        .style('cursor', function (d) {
          return d.data.clickData && d.data.clickData.url ? 'pointer' : 'default'
        })

      // 设置text文字自动换行
      texts.each(item => {
        var text = g.selectAll('text.text-id' + item.data.id.toString().replace(/\./g, ''));
        let rectWidth1 = item.data.rectWidth ? item.data.rectWidth : rectWidth;
        item.data.names = insertEnter(item.data.name, rectWidth1);
        let x = +text.attr('x'),
          y = +text.attr('y');
        let lineHight = fontSize + 4;
        for (let i = 0; i < item.data.names.length; i++) {
          text
            .append("tspan")
            .attr('x', x)
            .attr('y', function (d) {
              return y + lineHight * i
            })
            .text(function (d) {
              return d.data.names[i];
            })
            .append('title')
            .text(d => d.data.title)
        }
      })
      // 拆分文字字符串 (文字字符串,外框宽度)
      function insertEnter(name, width) {
        // 文字宽度
        let nameLen = name.length * fontSize;
        // 每行字数,超过换行
        let num = 4;
        // 文字宽度大于rect宽度时,计算每行最大字数
        console.log('nameLen',nameLen,width,name)
        if (nameLen > width) {
          num = Math.floor(width / fontSize)
        } else {
          num = Math.floor(nameLen / fontSize)
        }
        if(!num) num = 1;
        var s = name,
          reg = new RegExp(`.{1,${num}}`, 'g'),
          rs = s.match(reg);

        if (name.length <= num) {
          return [name];
        } else {
          rs.push(s.substring(rs.join('').length));
        }
        console.log('rs',rs)
        return rs;
      }
    }
    // 使用 foreignObject 添加 DOM 节点绘制文字(方法二,IE浏览器不支持)
    function appendXhtml(g,type,fontSize){
      g.selectAll('g.' + type + 'Node')
        .append("foreignObject")
        .attr('width',function(d){
          let  rectWidth1 = d.data.rectWidth ? d.data.rectWidth : rectWidth;
          return rectWidth1
        })
        .attr('height',rectHeight)
        .attr('x',function(d){
          return 0
        })
        .attr('y',-15)
        // 必须加 xhtml: 前缀,否则文字不显示
        .append('xhtml:p')
        .attr('title',function(d){
          return d.data.title
        })
        .style('width',function(d){
          let  rectWidth1 = d.data.rectWidth ? d.data.rectWidth : rectWidth;
          return rectWidth1
        })
        .style('height',function(d){
          return rectHeight
        })
        .style('box-sizing','border-box')
        .style('color','#000')
        .style('font-size',fontSize+'px')
        .style('text-align',function(d){
          return 'center'
        })
        // 可跳转的链接加样式
        .style('text-decoration',function(d){
          return d.data.clickData && d.data.clickData.url ? 'underline' : 'none'
        })
        .style('cursor',function(d){
          return d.data.clickData && d.data.clickData.url ? 'pointer' : 'default'
        })
        .style('margin','0px')
        .text(function (d) {
            return  d.data.name ;
        })  
    }
  </script>

</body>
</html>

效果图:

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值