react中从零开始学d3可视化

25 阅读2分钟

背景

加入了公司的新项目组, 业务需要让我们用d3搭建出各个app之间的workflow流向, 由于之前并没有接触可视化相关的内容 , 因此我在这里做一个记录。 我们目前希望实现的demo效果:

效果演示

image.png 参考了d3的mobile-patent的例子(observablehq.com/@d3/mobile-…) 从mobile-patent到我们的最终效果, 需要实现以下几个功能:

  1. circle调整变大
  2. 增加tooltip,包括circle和中间的link
  3. 添加文字并实现文字居中,实现text-overflow:ellipsis的效果
  4. 调整link line为dash 并且调整距离

实现思路

1.构造nodes 和links

可以参考上述的mobile-patent上面的数据节点, 这里省略

2.创建一个力学仿真模型

    const simulation = d3
      .forceSimulation(nodes)
      .force(
        'link',
        d3
          .forceLink(links)
          .id(d => d.id)
          .distance(150),
      )
      .force('charge', d3.forceManyBody().strength(-400))
      .force('center', d3.forceCenter(width / 2, height / 2))
      .force('x', d3.forceX())
      .force('y', d3.forceY());
  • d3.forceSimulation创建一个没有任何力的仿真模型
  • simulation.force(name[, force])表示为仿真添加指定 name 的 力模型 并返回仿真。
  • .forceCenter- 创建一个中心作用力.
  • .forceManyBody().strength() - 创建一个电荷作用力模型并指定电荷强度
  • .forceLink 创建一个 link(弹簧) 作用力.通过distance和strength可以设置它的强度

3.创建画布

 const svg = d3
      .selectAll('.graph') // .graph 是之前创建的svg classname
      // .attr('viewBox', [-width / 2.4, -height / 2, width, height])
      // .attr('viewBox', [width, height, width, height])
      .style('font', `${FONTSIZE}px sans-serif`)
      .style('color', 'red');

如何创建tooltip

 .on('mouseover', function() {
        return tooltip.style('visibility', 'visible');
      })
      .on('mousemove', function(event, d) {
        const index = d.index;
        const text = index === 0 || index ? dataNodes[index]?.id : '';
        tooltip
          .style('top', event.pageY + 'px')
          .style('left', event.pageX + 'px')
          .html(text);
      })
      .on('mouseout', function() {
        tooltip.style('visibility', 'hidden');
      });

如何中间的文字居中并加上text-overflow特性

export const svgTextEllipsis = (d3, textNode, padding, fontsize) => {
  const d3Nodes = d3.selectAll(textNode).nodes();
  d3Nodes.forEach(function(d) {
    const d3Node = d3.select(d);
    const targetWidth = Number(d3Node.attr('width')) - padding;
    const initialText = d3Node.text();
    let textWidth = d3Node.node()?.getComputedTextLength() ?? 0;
    const restWidth = textWidth - targetWidth;
    let text = initialText;
    if (textWidth < targetWidth) {
      const equalOffest = -restWidth / 2;
      d3Node.attr('dx', equalOffest);
      return;
    }
    const restCharacter = Math.ceil(restWidth / fontsize);
    text = text.slice(0, -restCharacter - 2) + '....';
    d3Node.text(text);
  });
};

对于dom样式的文本节点,必须要select一下才可以使用 其次这里的实现思路是: 比较当前text的文字的宽度跟容器的宽度进行比较 , 如果文字宽度大大于了容器宽度 , 看一下超出了后面几个文字 , 就是用文字宽度- 容器宽度然后除文字的大小 ,然后把后面的文字截取 , 然后加上... 居中的实现思路: 我是直接通过设置dx来实现的。 刚刚看到另一个人是通过textpath,我需要再研究一下

对于marker偏移严重的问题

markerUnits属性

首先是画marker的时候必须定义一个'markerUnits'的属性 ,默认的属性是根据 后面被使用的时候stokewidth 来变换marker的大小的, 如果要求不变必须更改属性值为'userSpaceOnUse'

marker的refx 和refy是怎么看的

主要参考了这个大兄弟的 image.png

image.png

跟我的认知有偏差,感觉反过来了一样