当前位置:   article > 正文

使用d3.js开发力导向图_d3.js力导向图

d3.js力导向图
1. 基本配置
  1. // 生成力
  2. const force = d3
  3. .forceSimulation()
  4. .force('link',d3.forceLink().id((d) => d.id),)
  5. .force('collide', d3.forceCollide(72).strength(0.1))
  6. .force('charge',d3.forceManyBody().strength(-400),)
  7. .force('center', d3.forceCenter());
2. 分配点与线

处理一下线的数据, 两个点可能出现多条线的情况

  1. export const setLinkNumber = (group, type) => {
  2. if (group.length === 0) return;
  3. const linksA = [];
  4. const linksB = [];
  5. for (let i = 0; i < group.length; i++) {
  6. const link = group[i];
  7. // 对该分组内的关系按照方向进行分类,此处根据连接的实体ASCII值大小分成两部分
  8. if (link.source.id < link.target.id) {
  9. linksA.push(link);
  10. } else {
  11. linksB.push(link);
  12. }
  13. }
  14. // 确定关系最大编号。为了使得连接两个实体的关系曲线呈现对称,根据关系数量奇偶性进行平分。
  15. // 特殊情况:当关系都是连接到同一个实体时,不平分
  16. let maxLinkNumber = 0;
  17. if (type === 'self') {
  18. maxLinkNumber = group.length;
  19. } else {
  20. maxLinkNumber = group.length % 2 === 0 ? group.length / 2 : (group.length + 1) / 2;
  21. }
  22. // 如果两个方向的关系数量一样多,直接分别设置编号即可
  23. if (linksA.length === linksB.length) {
  24. let startLinkNumber = 1;
  25. for (let i = 0; i < linksA.length; i++) {
  26. linksA[i].linknum = startLinkNumber++;
  27. }
  28. startLinkNumber = 1;
  29. for (let i = 0; i < linksB.length; i++) {
  30. linksB[i].linknum = startLinkNumber++;
  31. }
  32. } else {
  33. // 当两个方向的关系数量不对等时,先对数量少的那组关系从最大编号值进行逆序编号,然后在对另一组数量多的关系从编号1一直编号到最大编号,再对剩余关系进行负编号
  34. // 如果抛开负号,可以发现,最终所有关系的编号序列一定是对称的(对称是为了保证后续绘图时曲线的弯曲程度也是对称的)
  35. let biggerLinks;
  36. let smallerLinks;
  37. if (linksA.length > linksB.length) {
  38. biggerLinks = linksA;
  39. smallerLinks = linksB;
  40. } else {
  41. biggerLinks = linksB;
  42. smallerLinks = linksA;
  43. }
  44. let startLinkNumber = maxLinkNumber;
  45. for (let i = 0; i < smallerLinks.length; i++) {
  46. smallerLinks[i].linknum = startLinkNumber--;
  47. }
  48. const tmpNumber = startLinkNumber;
  49. startLinkNumber = 1;
  50. let p = 0;
  51. while (startLinkNumber <= maxLinkNumber) {
  52. biggerLinks[p++].linknum = startLinkNumber++;
  53. }
  54. // 开始负编号
  55. startLinkNumber = 0 - tmpNumber;
  56. for (let i = p; i < biggerLinks.length; i++) {
  57. biggerLinks[i].linknum = startLinkNumber++;
  58. }
  59. }
  60. };
  61. function getKey(target, source) {
  62. const result = target > source ? `${target}:${source}` : `${source}:${target}`;
  63. return result;
  64. }
  65. export const operationData = (chartData, clickType) => {
  66. const linkmap = {};
  67. const linkGroup = {};
  68. const { links, dots } = chartData;
  69. for (let i = 0; i < links.length; i++) {
  70. const link = links[i];
  71. const { target, source } = link;
  72. const key = getKey(target, source);
  73. if (linkGroup[key]) {
  74. linkGroup[key].push(link);
  75. linkmap[key] += 1;
  76. } else {
  77. linkGroup[key] = [links[i]];
  78. }
  79. }
  80. Object.keys(linkGroup).forEach((groupKey) => {
  81. linkmap[groupKey] = linkGroup[groupKey].length;
  82. });
  83. // 关联线与点
  84. JSON.parse(JSON.stringify(links)).forEach((e) => {
  85. const sourceNode = dots.filter((n) => n.id === e.source)[0];
  86. const targetNode = dots.filter((n) => n.id === e.target)[0];
  87. const nowIndex = links.findIndex(
  88. (n) => n.source === e.source && n.target === e.target,
  89. );
  90. if (!sourceNode || !targetNode) {
  91. links.splice(nowIndex, 1);
  92. } else {
  93. links[nowIndex].source = sourceNode;
  94. links[nowIndex].target = targetNode;
  95. }
  96. });
  97. for (let i = 0; i < links.length; i++) {
  98. let { target, source } = links[i];
  99. target = target.id;
  100. source = source.id;
  101. const link = links[i];
  102. const key = getKey(target, source);
  103. link.size = linkmap[key];
  104. const group = linkGroup[key];
  105. const type = 'noself'; // 标示该组关系是指向两个不同实体还是同一个实体
  106. setLinkNumber(group, type);
  107. }
  108. return { links, dots };
  109. };

处理好的数据丢到d3里面去

  1. // tick 渲染时执行的方法
  2. force.nodes(dots).alpha(0.01).on('tick', this.tick).restart();
  3. force.force('link').links(links).distance(150);

经d3处理后,会出来有 x,y 的数据。如:

数据处理完后,接下来创建dom。

3. 创建dom
3.1 线

线: 线是一个g标签包含着N条线(a)

线内包含着2条线以及线相关的箭头

两条线的目的是因为1条线很细的情况下会很不好hover 到。所以1条粗线 一条细线 ,直接把透明度(opacity) 属性 设置为0 即可。效果如下:

所有箭头状态的属性,因为箭头和线不是"一体"的,所以当hover 的时候,圆点(dot) 的大小会发生变化,箭头的refX,refY,也会发生变化,甚至 箭头的大小变化 refX 和refY也得改变。

  1. export const styleSize = {
  2. normal: {
  3. refX: 30,
  4. markerHeight: 8,
  5. markerWidth: 8,
  6. },
  7. hover: {
  8. refX: 28,
  9. markerHeight: 10,
  10. markerWidth: 10,
  11. },
  12. click: {
  13. refX: 19,
  14. markerHeight: 17.5,
  15. markerWidth: 17.5,
  16. },
  17. dotnormal: {
  18. refX: 35,
  19. markerHeight: 8,
  20. markerWidth: 8,
  21. },
  22. dothover: {
  23. refX: 43,
  24. markerHeight: 10,
  25. markerWidth: 10,
  26. },
  27. dotclick: {
  28. refX: 48,
  29. markerHeight: 10,
  30. markerWidth: 10,
  31. },
  32. dotlineclick: {
  33. refX: 28,
  34. markerHeight: 25,
  35. markerWidth: 20,
  36. },
  37. };
  38. // isThumb 是否是缩略图
  39. export const drawLine = (svg, type, links) => {
  40. const isThumb = type === 'thumb';
  41. const warp = isThumb ? svg.insert('g', '.dragThumb') : svg.append('g');
  42. const lineWarp = warp
  43. .attr('class', `${isThumb ? 'thumbG' : 'forceLines forceMainG'}`)
  44. .selectAll('g')
  45. .data(links)
  46. .enter()
  47. .append('a')
  48. const { refX, markerWidth, markerHeight } = styleSize.normal;
  49. const markerId = (d) => `marker-${(d.id)}`;
  50. lineWarp
  51. .append('marker')
  52. .attr('id', markerId)
  53. .attr('markerUnits', 'userSpaceOnUse')
  54. .attr('viewBox', '0 -5 10 10') // 坐标系的区域
  55. .attr('refX', refX) // 箭头坐标
  56. .attr('markerWidth', markerWidth) // 标识的大小
  57. .attr('markerHeight', markerHeight)
  58. .attr('orient', 'auto') // 绘制方向,可设定为:auto(自动确认方向)和 角度值
  59. .attr('stroke-width', 2) // 箭头宽度
  60. .append('path')
  61. .attr('d', 'M0,-5L10,0L0,5') // 箭头的路径
  62. .attr('fill', 'inherit'); //箭头的颜色, 设置箭头的颜色 不可以直接找到箭头然后更改fill 因为真正有颜色的是 箭头里面的dom
  63. // 展示的线
  64. const line = lineWarp.append('path')
  65. //实线虚线自己控制
  66. .attr('stroke-dasharray', (d) => ('虚线' ? '8,5' : ''))
  67. .attr('marker-end', (d) => {
  68. if (isThumb) return '';
  69. return `url(#${(markerId(d))})`;
  70. });
  71. const bkLine = lineWarp.append('path')
  72. // 实际hover 以及 点击的线
  73. .attr('stroke-width', 10)
  74. .attr('stroke', 'red')
  75. .attr('fill', 'none')
  76. .attr('opacity', '0')
  77. return {
  78. lineWarp,
  79. line,
  80. bkLine,
  81. };
  82. };
3.2 点
  1. export const drawCircle = (svg, nodes, type) => {
  2. const dotWarp = svg
  3. .append('g')
  4. .attr('class', 'forceNodes forceMainG')
  5. .selectAll('g')
  6. .data(nodes)
  7. .enter()
  8. .append('a')
  9. .attr('xlink:href', 'js:void(0)');
  10. const circle = dotWarp.append('circle')
  11. .attr('class', 'forceNode regionNode');
  12. return {
  13. circle,
  14. dotWarp
  15. };
  16. };
4. 绘制线

在之前的tick 中去改变 path 的d属性 以及 点的 位置

  1. this.paths.attr('d', function (data) {
  2. return pathD(data, this);
  3. });
  4. this.bkLine.attr('d', function (data) {
  5. return pathD(data, this);
  6. });
  7. this.dotWarp.attr('transform', setTransform); // 圆圈
  1. export const pathD = (d, dom) => {
  2. const { x: sx, y: sy } = d.source;
  3. const { x: tx, y: ty } = d.target;
  4. let dr;
  5. // 如果连接线连接的是同一个实体,则对path属性进行调整,绘制的圆弧属于长圆弧,同时对终点坐标进行微调,避免因坐标一致导致弧无法绘制
  6. if (d.target === d.source) {
  7. dr = 30 / d.linknum;
  8. return `M ${sx}, ${sy}A${dr},${dr} 0 1,1 ${tx},${ty + 1}`;
  9. }
  10. if (d.size % 2 !== 0 && d.linknum === 1) {
  11. // 如果两个节点之间的连接线数量为奇数条,则设置编号为1的连接线为直线,其他连接线会均分在两边
  12. return `M ${sx},${sy},L ${tx},${ty}`;
  13. }
  14. // 根据连接线编号值来动态确定该条椭圆弧线的长半轴和短半轴,当两者一致时绘制的是圆弧
  15. // 注意A属性后面的参数,前两个为长半轴和短半轴,第三个默认为0,
  16. // 第四个表示弧度大于180度则为1,小于则为0,这在绘制连接到相同节点的连接线时用到;
  17. // 第五个参数,0表示正角,1表示负角,即用来控制弧形凹凸的方向。本文正是结合编号的正负情况来控制该条连接线的凹凸方向,从而达到连接线对称的效果
  18. const curve = 1.5;
  19. const homogeneous = 2;
  20. const dx = d.target.x - d.source.x;
  21. const dy = d.target.y - d.source.y;
  22. dr = (Math.sqrt(dx * dx + dy * dy) * (d.linknum + homogeneous)) / (curve * homogeneous);
  23. // 当节点编号为负数时,对弧形进行反向凹凸,达到对称效果
  24. if (d.linknum < 0) {
  25. if (dom) {
  26. d3.select(dom.previousElementSibling).attr('refY', 4).attr('oldRefY', 4);
  27. }
  28. dr = (Math.sqrt(dx * dx + dy * dy) * (-1 * d.linknum + homogeneous)) / (curve * homogeneous);
  29. return `M${sx},${sy}A${dr},${dr} 0 0,0 ${tx},${ty}`;
  30. }
  31. if (dom) {
  32. d3.select(dom.previousElementSibling).attr('refY', -4).attr('oldRefY', -4);
  33. }
  34. return `M${sx},${sy}A${dr},${dr} 0 0,1 ${tx},${ty}`;
  35. };
  1. export const setTransform = (node) => {
  2. const { x, y, k } = node;
  3. let result = '';
  4. if (x && y) result += `translate(${x},${y})`;
  5. if (k)result += ` scale(${k})`;
  6. return result;
  7. };
5. 点的拖拽

固定节点的方法就是 设置fx fy=null

  1. // 创建完dotWarp的时候 可以直接绑定
  2. dotWarp.call(
  3. d3.drag()
  4. .on('start', this.started)
  5. .on('drag', this.dragged)
  6. .on('end', this.ended),
  7. );
  8. started(d) {
  9. const { force } = this;
  10. if (!d3.event.active) {
  11. force.alphaTarget(0.2).restart(); // 设置衰减系数,对节点位置移动过程的模拟,数值越高移动越快,数值范围[0,1]
  12. }
  13. d.fx = d.x;
  14. d.fy = d.y;
  15. },
  16. dragged(d) {
  17. const { x, y } = d3.event;
  18. if (this.inBoundaries(x, y).isIn) {
  19. d.fx = x;
  20. d.fy = y;
  21. }
  22. d.fx = d3.event.x;
  23. d.fy = d3.event.y;
  24. },
  25. ended(d) {
  26. const { force } = this;
  27. if (!d3.event.active) {
  28. force.alphaTarget(0);
  29. }
  30. d.fx = null;
  31. d.fy = null;
  32. },
6. zoom 画布的拖拽 以及放大缩小
  1. /**
  2. * @params
  3. * zoomMin 最小缩小倍数
  4. * zoomMax 最大放大倍数
  5. */
  6. const zoom = d3
  7. .zoom()
  8. .scaleExtent([zoomMin, zoomMax])
  9. .on('zoom', () => {
  10. const transInfo = d3.event.transform; //绘制框选的时候需要用到
  11. mainSvg.selectAll('g').attr('transform', transInfo);
  12. this.transInfo = transInfo;
  13. this.$emit('zoom', transInfo); // 告诉外层 发生了拖拽
  14. });
  15. mainSvg.call(zoom).on('dblclick.zoom', null); // 注销双击缩放

放大/缩小

  1. //svg.transition().duration(750).call(zoom.scaleBy,放大的倍数);
  2. // 缩小 0.9倍直到 缩小到最小倍数
  3. svg.transition().duration(750).call(zoom.scaleBy, 0.9);
  4. // 放大 1.1倍直到 放大到最大倍数
  5. svg.transition().duration(750).call(zoom.scaleBy, 1.1);
7. 缩略图

缩略图目前的逻辑是主图的最大倍数作为背景,主图的宽高作为缩略图视野(蓝框)的宽高。因为缩略图的dom 的宽高是css 定死的,所以给定主图(正常)的宽高 会自动缩放。

  1. /**
  2. * @params
  3. * width 缩略图宽度
  4. * height 缩略图高度
  5. * mainWidth 主图的宽度
  6. * mainHeight 主图的高度
  7. * zoomMax 最大缩放比例
  8. */
  9. thumbSvg = d3
  10. .select('#thumbWarp')
  11. .append('svg');
  12. dragThumb = thumbSvg.append('rect')
  13. .attr('class', 'dragThumb')
  14. .attr('fill', 'none');
  15. let w; let h; let x = 0; let y = 0;
  16. thumbSvg.attr('width', width)
  17. .attr('height', height)
  18. .attr('id', 'thumbSvg')
  19. .attr('viewBox', () => {
  20. // 缩略图的宽高为 主图的 最大缩略比例
  21. w = mainWidth * zoomMax;
  22. h = mainHeight * zoomMax;
  23. // 设置偏移 让背景图移至中心,缩略图与主图的差/ 2 就是需要移动的距离
  24. x = -(w - mainWidth) / 2;
  25. y = -(h - mainHeight) / 2;
  26. return `${x} ${y} ${w} ${h}`;
  27. });
  28. dragThumb.attr('width', mainWidth)
  29. .attr('height', mainHeight);
8. 主图的拖拽、缩放与缩略图

主图的拖拽与缩放 在调用上面的缩放的时候会调用zoom 的on zoom 方法,并将缩放以及拖拽的距离传给 缩略图,因为缩放会造成 主图的 translate 发生变化 与手动拖拽造成的translate 会有差 所以 要扣除缩放造成的偏移

  1. if (!mainTransform.x && !mainTransform.y && mainTransform.k === 1) {
  2. this.initSvg();
  3. return;
  4. }
  5. const {
  6. innerZoomInfo, mainWidth, mainHeight,
  7. } = this;
  8. // 如果传入的 缩放值与之前记录的缩放值不一致 则认为发生了缩放 记录发生缩放后偏移值
  9. if (!innerZoomInfo || innerZoomInfo.k !== mainTransform.k) {
  10. this.moveDiff = {
  11. x: (mainWidth - innerZoomInfo.k * mainWidth) / 2,
  12. y: (mainHeight - innerZoomInfo.k * mainHeight) / 2,
  13. };
  14. }
  15. const { x: diffX, y: diffY } = this.moveDiff;
  16. const { x, y, k } = mainTransform; // 主图偏移以及缩放数据
  17. this.dragThumb
  18. .attr('width', mainWidth / k)
  19. .attr('height', mainHeight / k)
  20. .attr('transform', () => setTransform({
  21. x: -((x - diffX) / k), // 这个地方应该不能直接 除 k 这里的x,y 应该是放大后的x,y应该减去缩放的差值 再 除K
  22. y: -((y - diffY) / k),
  23. }));
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/132713?site
推荐阅读
相关标签
  

闽ICP备14008679号