首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >D3.js在决策树中绘制钻石和连接链接

D3.js在决策树中绘制钻石和连接链接
EN

Stack Overflow用户
提问于 2020-09-06 06:44:36
回答 1查看 636关注 0票数 0

我正在使用D3进行以下布局(决策树),其中需要为流程图中的“决策”节点绘制菱形形状,其余的节点是操作(矩形)。

逻辑上,所有有子节点都是菱形。下面是UX的可视化。

我想出了一个从上到下的D3图表:https://jsfiddle.net/p6vrmnu0/3/

但是,所有svg元素目前都是矩形,所有的直线现在都通过“弯曲”链接连接起来,我希望svgs是“决策”节点的钻石,链接类似于来自钻石的两个角的图像,如果下一个是决定,则结束在菱形的顶部,如果下一个是操作。

代码语言:javascript
运行
复制
var margin = {
    top: 20,
    right: 120,
    bottom: 20,
    left: 120
  },
  width = 960 - margin.right - margin.left,
  height = 800 - margin.top - margin.bottom;

var emptyDecisionBox = {
  "name": "newDecision",
  "id": "newId",
  "value": "notSure",
  "condition": "true",
};

var selectedNode;


var root = {
  "name": "Root",
  "children": [{
      "name": "analytics",
      "type": "decision",
      "value": "a+b",
      "children": [{
        "name": "distinction",
        "type": "action",
        "condition": "true",
        "value": "5",
      }, {
        "name": "nonDistinction",
        "type": "action",
        "condition": "false",
        "value": "4"
      }],
    },
    {
      "name": "division",
      "type": "action",
      "value": "a-b",
      "children": [],
    }
  ]
};

var i = 0,
  duration = 750,
  rectW = 60,
  rectH = 30;

var tree = d3.layout.tree().nodeSize([70, 40]);
var diagonal = d3.svg.diagonal()
  .projection(function(d) {
    return [d.x + rectW / 2, d.y + rectH / 2];
  });

var svg = d3.select("body").append("svg").attr("width", 1000).attr("height", 1000)
  .call(zm = d3.behavior.zoom().scaleExtent([1, 3]).on("zoom", redraw)).append("g")
  .attr("transform", "translate(" + 350 + "," + 20 + ")");

//necessary so that zoom knows where to zoom and unzoom from
zm.translate([350, 20]);

// new part
var plusButton = svg
  .append('g')
  .classed('button', true)
  .classed('hide', true)
  .on('click', function() {
    console.log("CLICKED");
  });

plusButton
  .append('rect')
  .attr('transform', 'translate(-8, -8)') // center the button inside the `g`
  .attr('width', 16)
  .attr('height', 16)
  .attr('rx', 2);

plusButton
  .append('path')
  .attr('d', 'M-6 0 H6 M0 -6 V6');

var rectangleShape = svg.append('g')
  .classed('button', true)
  .classed('hide', true)
  .on('click', function() {
    removeAllButtonElements();
  })

rectangleShape
  .append('rect')
  .attr('width', 40)
  .attr('height', 20)
  .style('fill', 'orange');


var diamondImage = svg.append('g')
  .classed('button', true)
  .classed('hide', true)
  .classed('scale', true)
  .on('click', function() {
    addElement(selectedNode);
    console.log("Clicked on Diamond");
    console.log("set hide to true");
    removeAllButtonElements();
  });

diamondImage
  .append('path')
  .attr('d', 'M 20 0 40 20 20 40 0 20 Z')
  .style("fill", 'orange');


var rectangleShapeFalse = svg.append('g')
  .classed('button', true)
  .classed('hide', true)
  .on('click', function() {
    console.log("rectangle clicked");
    removeAllButtonElements();
  })

rectangleShapeFalse
  .append('rect')
  .attr('width', 40)
  .attr('height', 20)
  .style('fill', 'orange');

var diamondImageFalse = svg.append('g')
  .classed('button', true)
  .classed('hide', true)
  .classed('scale', true)
  .on('click', function() {
    console.log("Clicked on Diamond");
    console.log("set hide to true");
    removeAllButtonElements();
  });

diamondImageFalse
  .append('path')
  .attr('d', 'M 20 0 40 20 20 40 0 20 Z')
  .style("fill", 'orange');


function removeAllButtonElements() {
  plusButton.classed('hide', true);
  diamondImage.classed('hide', true);
  rectangleShape.classed('hide', true);
  diamondImageFalse.classed('hide', true);
  rectangleShapeFalse.classed('hide', true);
}

// new part ends. 

root.x0 = 0;
root.y0 = height / 2;

function collapse(d) {
  if (d.children) {
    d._children = d.children;
    d._children.forEach(collapse);
    d.children = null;
  }
}

/* root.children.forEach(collapse); */
update(root);

d3.select("#body").style("height", "800px");

function update(source) {

  // Compute the new tree layout.
  var nodes = tree.nodes(root).reverse(),
    links = tree.links(nodes);

  // Normalize for fixed-depth.
  nodes.forEach(function(d) {
    d.y = d.depth * 180;
  });

  // Update the nodes…
  var node = svg.selectAll("g.node")
    .data(nodes, function(d) {
      return d.id || (d.id = ++i);
    });

  // Enter any new nodes at the parent's previous position.
  var nodeEnter = node.enter().append("g")
    .attr("class", "node")
    .attr("transform", function(d) {
      return "translate(" + source.x0 + "," + source.y0 + ")";
    })
    .on("click", click);

  nodeEnter.append("rect")
    .attr("width", rectW)
    .attr("height", rectH)
    .attr("stroke", "black")
    .attr("stroke-width", 1)
    .style("fill", function(d) {
      return d._children ? "lightsteelblue" : "#fff";
    });

  /*   nodeEnter.append('path').attr("d", d3.svg.symbol().type( function(d) { return "circle" }) );
   */

  nodeEnter.append("text")
    .attr("x", rectW / 2)
    .attr("y", rectH / 2)
    .attr("dy", ".35em")
    .attr("text-anchor", "middle")
    .text(function(d) {
      return d.name;
    });

  // Transition nodes to their new position.
  var nodeUpdate = node.transition()
    .duration(duration)
    .attr("transform", function(d) {
      return "translate(" + d.x + "," + d.y + ")";
    });

  nodeUpdate.select("rect")
    .attr("width", rectW)
    .attr("height", rectH)
    .attr("stroke", "black")
    .attr("stroke-width", 1)
    .style("fill", function(d) {
      return d._children ? "lightsteelblue" : "#fff";
    });

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

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

  nodeExit.select("rect")
    .attr("width", rectW)
    .attr("height", rectH)
    //.attr("width", bbox.getBBox().width)""
    //.attr("height", bbox.getBBox().height)
    .attr("stroke", "black")
    .attr("stroke-width", 1);

  nodeExit.select("text");

  // Update the links…
  var link = svg.selectAll("path.link")
    .data(links, function(d) {
      return d.target.id;
    });

  // Enter any new links at the parent's previous position.
  link.enter().insert("path", "g")
    .attr("class", "link")
    .attr("x", rectW / 2)
    .attr("y", rectH / 2)
    .attr("d", function(d) {
      var o = {
        x: source.x0,
        y: source.y0
      };
      return diagonal({
        source: o,
        target: o
      });
    })
    .on('mouseenter', function(d, i) {
      // Use the native SVG interface to get the bounding box to
      // calculate the center of the path
      var bbox = this.getBBox();
      var x = bbox.x + bbox.width / 2,
        y = bbox.y + bbox.height / 2;
      plusButton
        .attr('transform', 'translate(' + x + ', ' + y + ')')
        .classed('hide', false);
    })
    .on('mouseleave', function(d, i) {
      plusButton
        .classed('hide', true);
    });

  // Transition links to their new position.
  link.transition()
    .duration(duration)
    .attr("d", diagonal);

  // Transition exiting nodes to the parent's new position.
  link.exit().transition()
    .duration(duration)
    .attr("d", function(d) {
      var o = {
        x: source.x,
        y: source.y
      };
      return diagonal({
        source: o,
        target: o
      });
    })
    .remove();

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

// Toggle children on click.
/* function click(d) {
    if (d.children) {
        d._children = d.children;
        d.children = null;
    } else {
        d.children = d._children;
        d._children = null;
    }
    update(d);
} */


function click(d) {
  console.log(d);
  selectedNode = d;
  /*     if(d.children && d.children.length < 1){
              return;
        } */
  var x = d.x;
  var y = d.y + 40;
  /*       plusButton
                 .attr('transform', 'translate(' + x + ', ' + y + ')')
          .classed('hide', false);
           
         plusButton
                 .attr('transform', 'translate(' + m + ', ' + h + ')')
          .classed('hide', false); */

  var m = d.x + 50;
  var h = d.y + 20;

  diamondImage
    .attr('transform', 'translate(' + m + ', ' + h + ')')
    .classed('hide', false);

  var m = d.x + 60;
  var h = d.y - 10;

  rectangleShape
    .attr('transform', 'translate(' + m + ', ' + h + ')')
    .classed('hide', false);

  var m = d.x - 40;
  var h = d.y + 20;

  diamondImageFalse
    .attr('transform', 'translate(' + m + ', ' + h + ')')
    .classed('hide', false);

  var m = d.x - 40;
  var h = d.y - 10;

  rectangleShapeFalse
    .attr('transform', 'translate(' + m + ', ' + h + ')')
    .classed('hide', false);
}

//Redraw for zoom
function redraw() {
  //console.log("here", d3.event.translate, d3.event.scale);
  svg.attr("transform",
    "translate(" + d3.event.translate + ")" +
    " scale(" + d3.event.scale + ")");
}

function addElement(d) {
  console.log(d);

  d.children = [];
  d.children.push(emptyDecisionBox);
  update(root);
}
代码语言:javascript
运行
复制
.node {
  cursor: pointer;
}

.node circle {
  fill: #fff;
  stroke: steelblue;
  stroke-width: 1.5px;
}

.node text {
  font: 10px sans-serif;
}

.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 1.5px;
}

body {
  overflow: hidden;
}

.button>path {
  stroke: blue;
  stroke-width: 1.5;
}

.button>rect {
  fill: #ddd;
  stroke: grey;
  stroke-width: 1px;
}

.hide {
  display: none;
  opacity: 0 !important;
  pointer-events: none;
}

.link:hover {
  cursor: pointer;
  stroke-width: 8px;
}

.scale {
  /* transform: scale(0.4); */
}
代码语言:javascript
运行
复制
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div class="tree-diagram"></div>

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2020-09-06 17:16:19

对于线条形状,我建议您自己编写一个形状函数。我写了一个水平的,然后做一个小的曲线,然后向下。它不考虑形状的宽度,但这并不重要,因为它毕竟在盒子后面。

对于盒子的形状,我会做一些类似的事情。而不是使用rects,而是使用path,并使用if语句编写路径的形状。始终是一个非常有价值的资源。

只是为了让你开始:

代码语言:javascript
运行
复制
function drawDiamond(centroid) {
  // Start at the top
  var result = 'M' + centroid.x + ',' + (centroid.y - rectH / 2);

  // Go right
  result += 'L' + (centroid.x + rectW / 2) + ',' + centroid.y;

  // Bottom
  result += 'L' + centroid.x + ',' + (centroid.y + rectH / 2);

  // Left
  result += 'L' + (centroid.x - rectW / 2) + ',' + centroid.y;

  // Close the shape
  result += 'Z';

  return result;
}

function drawRect(centroid) {
  // Start at the top left
  var result = 'M' + (centroid.x - rectW / 2) + ',' + (centroid.y - rectH / 2);

  // Go right
  result += 'h' + rectW;

  // Go down
  result += 'v' + rectH;

  // Left
  result += 'h-' + rectW;

  // Close the shape
  result += 'Z';

  return result;
}

代码语言:javascript
运行
复制
var margin = {
    top: 20,
    right: 120,
    bottom: 20,
    left: 120
  },
  width = 960 - margin.right - margin.left,
  height = 800 - margin.top - margin.bottom;

var emptyDecisionBox = {
  "name": "newDecision",
  "id": "newId",
  "value": "notSure",
  "condition": "true",
};

var selectedNode;


var root = {
  "name": "Root",
  "children": [{
      "name": "analytics",
      "type": "decision",
      "value": "a+b",
      "children": [{
        "name": "distinction",
        "type": "action",
        "condition": "true",
        "value": "5",
      }, {
        "name": "nonDistinction",
        "type": "action",
        "condition": "false",
        "value": "4"
      }],
    },
    {
      "name": "division",
      "type": "action",
      "value": "a-b",
      "children": [],
    }
  ]
};

var i = 0,
  duration = 750,
  rectW = 60,
  rectH = 30;

var tree = d3.layout.tree().nodeSize([70, 40]);
var linkFunc = function(d) {
  var source = {
    x: d.source.x,
    y: d.source.y + (rectH / 2)
  };
  var target = {
    x: d.target.x + (rectW / 2),
    y: d.target.y
  };

  // This is where the line bends
  var inflection = {
    x: target.x,
    y: source.y
  };
  var radius = 5;

  var result = "M" + source.x + ',' + source.y;
  
  if (source.x < target.x) {
    // Child is to the right of the parent
    result += ' H' + (inflection.x - radius);
  } else {
    result += ' H' + (inflection.x + radius);
  }

  // Curve the line at the bend slightly
  result += ' Q' + inflection.x + ',' + inflection.y + ' ' + inflection.x + ',' + (inflection.y + radius);

  result += 'V' + target.y;
  return result;
}

var svg = d3.select("body").append("svg").attr("width", 1000).attr("height", 1000)
  .call(zm = d3.behavior.zoom().scaleExtent([1, 3]).on("zoom", redraw)).append("g")
  .attr("transform", "translate(" + 350 + "," + 20 + ")");

//necessary so that zoom knows where to zoom and unzoom from
zm.translate([350, 20]);

// new part
var plusButton = svg
  .append('g')
  .classed('button', true)
  .classed('hide', true)
  .on('click', function() {
    console.log("CLICKED");
  });

plusButton
  .append('rect')
  .attr('transform', 'translate(-8, -8)') // center the button inside the `g`
  .attr('width', 16)
  .attr('height', 16)
  .attr('rx', 2);

plusButton
  .append('path')
  .attr('d', 'M-6 0 H6 M0 -6 V6');

var rectangleShape = svg.append('g')
  .classed('button', true)
  .classed('hide', true)
  .on('click', function() {
    removeAllButtonElements();
  })

rectangleShape
  .append('rect')
  .attr('width', 40)
  .attr('height', 20)
  .style('fill', 'orange');


var diamondImage = svg.append('g')
  .classed('button', true)
  .classed('hide', true)
  .classed('scale', true)
  .on('click', function() {
    addElement(selectedNode);
    console.log("Clicked on Diamond");
    console.log("set hide to true");
    removeAllButtonElements();
  });

diamondImage
  .append('path')
  .attr('d', 'M 20 0 40 20 20 40 0 20 Z')
  .style("fill", 'orange');


var rectangleShapeFalse = svg.append('g')
  .classed('button', true)
  .classed('hide', true)
  .on('click', function() {
    console.log("rectangle clicked");
    removeAllButtonElements();
  })

rectangleShapeFalse
  .append('rect')
  .attr('width', 40)
  .attr('height', 20)
  .style('fill', 'orange');

var diamondImageFalse = svg.append('g')
  .classed('button', true)
  .classed('hide', true)
  .classed('scale', true)
  .on('click', function() {
    console.log("Clicked on Diamond");
    console.log("set hide to true");
    removeAllButtonElements();
  });

diamondImageFalse
  .append('path')
  .attr('d', 'M 20 0 40 20 20 40 0 20 Z')
  .style("fill", 'orange');


function removeAllButtonElements() {
  plusButton.classed('hide', true);
  diamondImage.classed('hide', true);
  rectangleShape.classed('hide', true);
  diamondImageFalse.classed('hide', true);
  rectangleShapeFalse.classed('hide', true);
}

// new part ends. 

root.x0 = 0;
root.y0 = height / 2;

function collapse(d) {
  if (d.children) {
    d._children = d.children;
    d._children.forEach(collapse);
    d.children = null;
  }
}

/* root.children.forEach(collapse); */
update(root);

d3.select("#body").style("height", "800px");

function update(source) {

  // Compute the new tree layout.
  var nodes = tree.nodes(root).reverse(),
    links = tree.links(nodes);

  // Normalize for fixed-depth.
  nodes.forEach(function(d) {
    d.y = d.depth * 180;
  });

  // Update the nodes…
  var node = svg.selectAll("g.node")
    .data(nodes, function(d) {
      return d.id || (d.id = ++i);
    });

  // Enter any new nodes at the parent's previous position.
  var nodeEnter = node.enter().append("g")
    .attr("class", "node")
    .attr("transform", function(d) {
      return "translate(" + source.x0 + "," + source.y0 + ")";
    })
    .on("click", click);

  nodeEnter.append("rect")
    .attr("width", rectW)
    .attr("height", rectH)
    .attr("stroke", "black")
    .attr("stroke-width", 1)
    .style("fill", function(d) {
      return d._children ? "lightsteelblue" : "#fff";
    });

  /*   nodeEnter.append('path').attr("d", d3.svg.symbol().type( function(d) { return "circle" }) );
   */

  nodeEnter.append("text")
    .attr("x", rectW / 2)
    .attr("y", rectH / 2)
    .attr("dy", ".35em")
    .attr("text-anchor", "middle")
    .text(function(d) {
      return d.name;
    });

  // Transition nodes to their new position.
  var nodeUpdate = node.transition()
    .duration(duration)
    .attr("transform", function(d) {
      return "translate(" + d.x + "," + d.y + ")";
    });

  nodeUpdate.select("rect")
    .attr("width", rectW)
    .attr("height", rectH)
    .attr("stroke", "black")
    .attr("stroke-width", 1)
    .style("fill", function(d) {
      return d._children ? "lightsteelblue" : "#fff";
    });

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

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

  nodeExit.select("rect")
    .attr("width", rectW)
    .attr("height", rectH)
    //.attr("width", bbox.getBBox().width)""
    //.attr("height", bbox.getBBox().height)
    .attr("stroke", "black")
    .attr("stroke-width", 1);

  nodeExit.select("text");

  // Update the links…
  var link = svg.selectAll("path.link")
    .data(links, function(d) {
      return d.target.id;
    });

  // Enter any new links at the parent's previous position.
  link.enter().insert("path", "g")
    .attr("class", "link")
    .attr("x", rectW / 2)
    .attr("y", rectH / 2)
    .attr("d", linkFunc)
    .on('mouseenter', function(d, i) {
      // Use the native SVG interface to get the bounding box to
      // calculate the center of the path
      var bbox = this.getBBox();
      var x = bbox.x + bbox.width / 2,
        y = bbox.y + bbox.height / 2;
      plusButton
        .attr('transform', 'translate(' + x + ', ' + y + ')')
        .classed('hide', false);
    })
    .on('mouseleave', function(d, i) {
      plusButton
        .classed('hide', true);
    });

  // Transition links to their new position.
  link.transition()
    .duration(duration)
    .attr("d", linkFunc);

  // Transition exiting nodes to the parent's new position.
  link.exit().transition()
    .duration(duration)
    .attr("d", linkFunc)
    .remove();

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


function click(d) {
  console.log(d);
  selectedNode = d;
  var x = d.x;
  var y = d.y + 40;

  var m = d.x + 50;
  var h = d.y + 20;

  diamondImage
    .attr('transform', 'translate(' + m + ', ' + h + ')')
    .classed('hide', false);

  var m = d.x + 60;
  var h = d.y - 10;

  rectangleShape
    .attr('transform', 'translate(' + m + ', ' + h + ')')
    .classed('hide', false);

  var m = d.x - 40;
  var h = d.y + 20;

  diamondImageFalse
    .attr('transform', 'translate(' + m + ', ' + h + ')')
    .classed('hide', false);

  var m = d.x - 40;
  var h = d.y - 10;

  rectangleShapeFalse
    .attr('transform', 'translate(' + m + ', ' + h + ')')
    .classed('hide', false);
}

//Redraw for zoom
function redraw() {
  //console.log("here", d3.event.translate, d3.event.scale);
  svg.attr("transform",
    "translate(" + d3.event.translate + ")" +
    " scale(" + d3.event.scale + ")");
}

function addElement(d) {
  console.log(d);

  d.children = [];
  d.children.push(emptyDecisionBox);
  update(root);
}
代码语言:javascript
运行
复制
.node {
  cursor: pointer;
}

.node circle {
  fill: #fff;
  stroke: steelblue;
  stroke-width: 1.5px;
}

.node text {
  font: 10px sans-serif;
}

.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 1.5px;
}

body {
  overflow: hidden;
}

.button>path {
  stroke: blue;
  stroke-width: 1.5;
}

.button>rect {
  fill: #ddd;
  stroke: grey;
  stroke-width: 1px;
}

.hide {
  display: none;
  opacity: 0 !important;
  pointer-events: none;
}

.link:hover {
  cursor: pointer;
  stroke-width: 8px;
}

.scale {
  /* transform: scale(0.4); */
}
代码语言:javascript
运行
复制
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div class="tree-diagram"></div>

现在,您的代码通常会出现一些问题。您将设置要删除的项或已经设置的项的属性。请记住,页面上的每个节点都已经输入了一次,所以当您设置它们的宽度/高度/笔画时,没有理由再次设置它,除非它发生了更改。类似地,没有理由使用nodeUpdate.select('rect'),然后重新设置所有的值,只需删除代码,它就什么也不做,导致混乱。

您也不按类进行选择,而是按标记进行选择。这会给你带来问题,只要你从路径开始。用类代替!

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/63761477

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档