javascript - D3.js分层边缘捆绑按组着色

标签 javascript html css d3.js svg

我正在尝试根据它们连接到的组为我的分层边缘捆绑可视化中的连接着色。这方面的一个例子可以看到here .

enter image description here

这是我当前的鼠标悬停功能:

    function mouseover(d) {
        svg.selectAll("path.link.target-" + d.key)
            .classed("target", true)
            .each(updateNodes("source", true));

        svg.selectAll("path.link.source-" + d.key)
            .classed("source", true)
            .each(updateNodes("target", true));
    }

这是我发布的示例中的鼠标悬停功能:
function mouseovered(d) 
{
        // Handle tooltip
        // Tooltips should avoid crossing into the center circle

        d3.selectAll("#tooltip").remove();
        d3.selectAll("#vis")
            .append("xhtml:div")
            .attr("id", "tooltip")
            .style("opacity", 0)
            .html(d.title);
        var mouseloc = d3.mouse(d3.select("#vis")[0][0]),
            my = ((rotateit(d.x) > 90) && (rotateit(d.x) < 270)) ? mouseloc[1] + 10 : mouseloc[1] - 35,
            mx = (rotateit(d.x) < 180) ? (mouseloc[0] + 10) :  Math.max(130, (mouseloc[0] - 10 - document.getElementById("tooltip").offsetWidth));
        d3.selectAll("#tooltip").style({"top" : my + "px", "left": mx + "px"});
        d3.selectAll("#tooltip")
            .transition()
            .duration(500)
            .style("opacity", 1);
        node.each(function(n) { n.target = n.source = false; });

        currnode = d3.select(this)[0][0].__data__;

        link.classed("link--target", function(l) { 
                if (l.target === d) 
                { 
                    return l.source.source = true; 
                }
                if (l.source === d) 
                { 
                    return l.target.target = true; 
                }
            })
            .filter(function(l) { return l.target === d || l.source === d; })
            .attr("stroke", function(d){
                if (d[0].name == currnode.name)
                {
                    return color(d[2].cat);
                }
                return color(d[0].cat);
            })
            .each(function() { this.parentNode.appendChild(this); });

        d3.selectAll(".link--clicked").each(function() { this.parentNode.appendChild(this); });

        node.classed("node--target", function(n) { 
                return (n.target || n.source); 
            });
}

我对 D3 有点陌生,但我假设我需要做的是根据 key 检查组,然后将其匹配到与该组相同的颜色。

我的完整代码在这里:
 <script type="text/javascript">
    color = d3.scale.category10(); 

    var w = 840,
        h = 800,
        rx = w / 2,
        ry = h / 2,
        m0,
        rotate = 0
    pi = Math.PI;

    var splines = [];

    var cluster = d3.layout.cluster()
        .size([360, ry - 180])
        .sort(function(a, b) {
            return d3.ascending(a.key, b.key);
        });

    var bundle = d3.layout.bundle();

    var line = d3.svg.line.radial()
        .interpolate("bundle")
        .tension(.5)
        .radius(function(d) {
            return d.y;
        })
        .angle(function(d) {
            return d.x / 180 * Math.PI;
        });

    // Chrome 15 bug: <http://code.google.com/p/chromium/issues/detail?id=98951>
    var div = d3.select("#bundle")
        .style("width", w + "px")
        .style("height", w + "px")
        .style("position", "absolute");

    var svg = div.append("svg:svg")
        .attr("width", w)
        .attr("height", w)
        .append("svg:g")
        .attr("transform", "translate(" + rx + "," + ry + ")");

    svg.append("svg:path")
        .attr("class", "arc")
        .attr("d", d3.svg.arc().outerRadius(ry - 180).innerRadius(0).startAngle(0).endAngle(2 * Math.PI))
        .on("mousedown", mousedown);

    d3.json("TASKS AND PHASES.json", function(classes) {

        var nodes = cluster.nodes(packages.root(classes)),
            links = packages.imports(nodes),
            splines = bundle(links);

        var path = svg.selectAll("path.link")
            .data(links)
            .enter().append("svg:path")
            .attr("class", function(d) {
                return "link source-" + d.source.key + " target-" + d.target.key;
            })
            .attr("d", function(d, i) {
                return line(splines[i]);
            });

        var groupData = svg.selectAll("g.group")
            .data(nodes.filter(function(d) {
                return (d.key == 'Department' || d.key == 'Software' || d.key == 'Tasks' || d.key == 'Phases') && d.children;
            }))
            .enter().append("group")
            .attr("class", "group");

        var groupArc = d3.svg.arc()
            .innerRadius(ry - 177)
            .outerRadius(ry - 157)
            .startAngle(function(d) {
                return (findStartAngle(d.__data__.children) - 2) * pi / 180;
            })
            .endAngle(function(d) {
                return (findEndAngle(d.__data__.children) + 2) * pi / 180
            });        

        svg.selectAll("g.arc")
            .data(groupData[0])
            .enter().append("svg:path")
            .attr("d", groupArc)
            .attr("class", "groupArc")
            .attr("id", function(d, i) {console.log(d.__data__.key); return d.__data__.key;})
            .style("fill", function(d, i) {return color(i);})
            .style("fill-opacity", 0.5)
            .each(function(d,i) {

                var firstArcSection = /(^.+?)L/;

                var newArc = firstArcSection.exec( d3.select(this).attr("d") )[1];

                newArc = newArc.replace(/,/g , " ");

                svg.append("path")
                    .attr("class", "hiddenArcs")
                    .attr("id", "hidden"+d.__data__.key)
                    .attr("d", newArc)
                    .style("fill", "none");
            });



        svg.selectAll(".arcText")
            .data(groupData[0])
            .enter().append("text")
            .attr("class", "arcText")
            .attr("dy", 15)
            .append("textPath")
            .attr("startOffset","50%")
            .style("text-anchor","middle")
            .attr("xlink:href",function(d,i){return "#hidden" + d.__data__.key;})
            .text(function(d){return d.__data__.key;});    

        svg.selectAll("g.node")
            .data(nodes.filter(function(n) {
                return !n.children;
            }))
            .enter().append("svg:g")
            .attr("class", "node")
            .attr("id", function(d) {
                return "node-" + d.key;
            })
            .attr("transform", function(d) {
                return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
            })
            .append("svg:text")
            .attr("dx", function(d) {
                return d.x < 180 ? 25 : -25;
            })
            .attr("dy", ".31em")
            .attr("text-anchor", function(d) {
                return d.x < 180 ? "start" : "end";
            })
            .attr("transform", function(d) {
                return d.x < 180 ? null : "rotate(180)";
            })
            .text(function(d) {
                return d.key.replace(/_/g, ' ');
            })
            .on("mouseover", mouseover)
            .on("mouseout", mouseout);

        d3.select("input[type=range]").on("change", function() {
            line.tension(this.value / 100);
            path.attr("d", function(d, i) {
                return line(splines[i]);
            });
        });
    });

    d3.select(window)
        .on("mousemove", mousemove)
        .on("mouseup", mouseup);

    function mouse(e) {
        return [e.pageX - rx, e.pageY - ry];
    }

    function mousedown() {
        m0 = mouse(d3.event);
        d3.event.preventDefault();
    }

    function mousemove() {
        if (m0) {
            var m1 = mouse(d3.event),
                dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
            div.style("-webkit-transform", "translate3d(0," + (ry - rx) + "px,0)rotate3d(0,0,0," + dm + "deg)translate3d(0," + (rx - ry) + "px,0)");
        }
    }

    function mouseup() {
        if (m0) {
            var m1 = mouse(d3.event),
                dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;

            rotate += dm;
            if (rotate > 360) rotate -= 360;
            else if (rotate < 0) rotate += 360;
            m0 = null;

            div.style("-webkit-transform", "rotate3d(0,0,0,0deg)");

            svg.attr("transform", "translate(" + rx + "," + ry + ")rotate(" + rotate + ")")
                .selectAll("g.node text")
                .attr("dx", function(d) {
                    return (d.x + rotate) % 360 < 180 ? 25 : -25;
                })
                .attr("text-anchor", function(d) {
                    return (d.x + rotate) % 360 < 180 ? "start" : "end";
                })
                .attr("transform", function(d) {
                    return (d.x + rotate) % 360 < 180 ? null : "rotate(180)";
                });
        }
    }

    function mouseover(d) {
        svg.selectAll("path.link.target-" + d.key)
            .classed("target", true)
            .each(updateNodes("source", true));

        svg.selectAll("path.link.source-" + d.key)
            .classed("source", true)
            .each(updateNodes("target", true));
    }

    function mouseout(d) {
        svg.selectAll("path.link.source-" + d.key)
            .classed("source", false)
            .each(updateNodes("target", false));

        svg.selectAll("path.link.target-" + d.key)
            .classed("target", false)
            .each(updateNodes("source", false));
    }

    function updateNodes(name, value) {
        return function(d) {
            if (value) this.parentNode.appendChild(this);
            svg.select("#node-" + d[name].key).classed(name, value);
        };
    }

    function cross(a, b) {
        return a[0] * b[1] - a[1] * b[0];
    }

    function dot(a, b) {
        return a[0] * b[0] + a[1] * b[1];
    }

    function findStartAngle(children) {
        var min = children[0].x;
        children.forEach(function(d) {
            if (d.x < min)
                min = d.x;
        });
        return min;
    }

    function findEndAngle(children) {
        var max = children[0].x;
        children.forEach(function(d) {
            if (d.x > max)
                max = d.x;
        });
        return max;
    }
</script>

最佳答案

这是 D3 v6 中采用 Observable example 的示例解决方案加上我的answer对于另一个问题。基本点:

  • 您将添加“组”到输入数据中 - 对于您 mention in the comments 的数据我已经定义了 group作为名称的第二个元素(每点分隔)。 hierarchy Observable 中的函数似乎去掉了这一点。
  • 很幸运,所有 name值是例如root.parent.child - 这使得leafGroups 对您的数据非常有效(但可能不适用于非对称层次结构)。
  • 定义颜色范围,例如const colors = d3.scaleOrdinal().domain(leafGroups.map(d => d[0])).range(d3.schemeTableau10);可用于弧线、标签文本(node s)、路径(link s)
  • 我已经避免使用 mix-blend-mode使用示例进行样式设置,因为它对我来说看起来不太好。
  • 我正在应用 overed 中的样式和 outed - 请参阅下面的逻辑。

  • overed 中的评论用于 mouseover 上的样式逻辑:
    function overed(event, d) {
    
      //link.style("mix-blend-mode", null);
    
      d3.select(this)
        // set dark/ bold on hovered node 
        .style("fill", colordark) 
        .attr("font-weight", "bold"); 
    
      d3.selectAll(d.incoming.map(d => d.path))
        // each link has data with source and target so you can get group 
        // and therefore group color; 0 for incoming and 1 for outgoing
        .attr("stroke", d => colors(d[0].data.group)) 
        // increase stroke width for emphasis
        .attr("stroke-width", 4)
        .raise();
    
      d3.selectAll(d.outgoing.map(d => d.path))
        // each link has data with source and target so you can get group 
        // and therefore group color; 0 for incoming and 1 for outgoing
        .attr("stroke", d => colors(d[1].data.group))
        // increase stroke width for emphasis
        .attr("stroke-width", 4)
        .raise()
    
      d3.selectAll(d.incoming.map(([d]) => d.text))
        // source and target nodes to go dark and bold
        .style("fill", colordark) 
        .attr("font-weight", "bold");    
    
      d3.selectAll(d.outgoing.map(([, d]) => d.text))
        // source and target nodes to go dark and bold
        .style("fill", colordark) 
        .attr("font-weight", "bold");    
    }
    
    outed 中的评论用于 mouseout 上的样式逻辑:
    function outed(event, d) {
    
      //link.style("mix-blend-mode", "multiply");
    
      d3.select(this)
        // hovered node to revert to group colour on mouseout
        .style("fill", d => colors(d.data.group))
        .attr("font-weight", null); 
    
      d3.selectAll(d.incoming.map(d => d.path))
        // incoming links to revert to 'colornone' and width 1 on mouseout
        .attr("stroke", colornone)
        .attr("stroke-width", 1);
    
      d3.selectAll(d.outgoing.map(d => d.path))
        // incoming links to revert to 'colornone' and width 1 on mouseout
        .attr("stroke", colornone)
        .attr("stroke-width", 1);
    
      d3.selectAll(d.incoming.map(([d]) => d.text))
        // incoming nodes to revert to group colour on mouseout
        .style("fill", d => colors(d.data.group))
        .attr("font-weight", null);    
    
      d3.selectAll(d.outgoing.map(([, d]) => d.text))
        // incoming nodes to revert to group colour on mouseout
        .style("fill", d => colors(d.data.group))
        .attr("font-weight", null);    
    }
    
    使用您在评论中提到的数据的工作示例:

    const url = "https://gist.githubusercontent.com/robinmackenzie/5c5d2af4e3db47d9150a2c4ba55b7bcd/raw/9f9c6b92d24bd9f9077b7fc6c4bfc5aebd2787d5/harvard_vis.json";
    const colornone = "#ccc";
    const colordark = "#222";
    const width = 600;
    const radius = width / 2;
    
    d3.json(url).then(json => {
      // hack in the group name to each object
      json.forEach(o => o.group = o.name.split(".")[1]);
      // then render
      render(json);
    });
    
    function render(data) {
    
      const line = d3.lineRadial()
        .curve(d3.curveBundle.beta(0.85))
        .radius(d => d.y)
        .angle(d => d.x);
    
      const tree = d3.cluster()
        .size([2 * Math.PI, radius - 100]);
    
      const root = tree(bilink(d3.hierarchy(hierarchy(data))
        .sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.name, b.data.name))));
    
      const svg = d3.select("body")
        .append("svg")
        .attr("width", width)
        .attr("height", width)
        .append("g")
        .attr("transform", `translate(${radius},${radius})`);
    
      const arcInnerRadius = radius - 100;
      const arcWidth = 20;
      const arcOuterRadius = arcInnerRadius + arcWidth;
      const arc = d3
        .arc()
        .innerRadius(arcInnerRadius)
        .outerRadius(arcOuterRadius)
        .startAngle((d) => d.start)
        .endAngle((d) => d.end);
    
      const leafGroups = d3.groups(root.leaves(), d => d.parent.data.name);
      const arcAngles = leafGroups.map(g => ({
        name: g[0],
        start: d3.min(g[1], d => d.x),
        end: d3.max(g[1], d => d.x)
      }));
      const colors = d3.scaleOrdinal().domain(leafGroups.map(d => d[0])).range(d3.schemeTableau10);
    
      svg
        .selectAll(".arc")
        .data(arcAngles)
        .enter()
        .append("path")
        .attr("id", (d, i) => `arc_${i}`)
        .attr("d", (d) => arc({start: d.start, end: d.end}))
        .attr("fill", d => colors(d.name))
    
      svg
        .selectAll(".arcLabel")
        .data(arcAngles) 
        .enter()
        .append("text")
        .attr("x", 5) 
        .attr("dy", (d) => ((arcOuterRadius - arcInnerRadius) * 0.8)) 
        .append("textPath")
        .attr("class", "arcLabel")
        .attr("xlink:href", (d, i) => `#arc_${i}`)
        .text((d, i) => ((d.end - d.start) < (6 * Math.PI / 180)) ? "" : d.name); 
    
      // add nodes
      const node = svg.append("g")
          .attr("font-family", "sans-serif")
          .attr("font-size", 10)
        .selectAll("g")
        .data(root.leaves())
        .join("g")
          .attr("transform", d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y}, 0)`)
        .append("text")
          .attr("dy", "0.31em")
          .attr("x", d => d.x < Math.PI ? (arcWidth + 5) : (arcWidth + 5) * -1) 
          .attr("text-anchor", d => d.x < Math.PI ? "start" : "end")
          .attr("transform", d => d.x >= Math.PI ? "rotate(180)" : null)
          .text(d => d.data.name)
          .style("fill", d => colors(d.data.group))
          .each(function(d) { d.text = this; })
          .on("mouseover", overed)
          .on("mouseout", outed)
          .call(text => text.append("title").text(d => `${id(d)} ${d.outgoing.length} outgoing ${d.incoming.length} incoming`));
    
      // add edges
      const link = svg.append("g")
          .attr("stroke", colornone)
          .attr("fill", "none")
        .selectAll("path")
        .data(root.leaves().flatMap(leaf => leaf.outgoing))
        .join("path")
          //.style("mix-blend-mode", "multiply")
          .attr("d", ([i, o]) => line(i.path(o)))
          .each(function(d) { d.path = this; });
    
      function overed(event, d) {
    
        //link.style("mix-blend-mode", null);
    
        d3.select(this)
          .style("fill", colordark)
          .attr("font-weight", "bold"); 
    
        d3.selectAll(d.incoming.map(d => d.path))
          .attr("stroke", d => colors(d[0].data.group))
          .attr("stroke-width", 4)
          .raise();
    
        d3.selectAll(d.outgoing.map(d => d.path))
          .attr("stroke", d => colors(d[1].data.group))
          .attr("stroke-width", 4)
          .raise()
    
        d3.selectAll(d.incoming.map(([d]) => d.text))
          .style("fill", colordark)
          .attr("font-weight", "bold");    
    
        d3.selectAll(d.outgoing.map(([, d]) => d.text))
          .style("fill", colordark)
          .attr("font-weight", "bold");    
      }
    
      function outed(event, d) {
    
        //link.style("mix-blend-mode", "multiply");
    
        d3.select(this)
          .style("fill", d => colors(d.data.group))
          .attr("font-weight", null); 
    
        d3.selectAll(d.incoming.map(d => d.path))
          .attr("stroke", colornone)
          .attr("stroke-width", 1);
    
        d3.selectAll(d.outgoing.map(d => d.path))
          .attr("stroke", colornone)
          .attr("stroke-width", 1);
    
        d3.selectAll(d.incoming.map(([d]) => d.text))
          .style("fill", d => colors(d.data.group))
          .attr("font-weight", null);    
    
        d3.selectAll(d.outgoing.map(([, d]) => d.text))
          .style("fill", d => colors(d.data.group))
          .attr("font-weight", null);    
      }
    
      function id(node) {
        return `${node.parent ? id(node.parent) + "." : ""}${node.data.name}`;
      }
    
      function bilink(root) {
        const map = new Map(root.leaves().map(d => [id(d), d]));
        for (const d of root.leaves()) d.incoming = [], d.outgoing = d.data.imports.map(i => [d, map.get(i)]);
        for (const d of root.leaves()) for (const o of d.outgoing) o[1].incoming.push(o);
        return root;
      }
    
      function hierarchy(data, delimiter = ".") {
        let root;
        const map = new Map;
        data.forEach(function find(data) {
          const {name} = data;
          if (map.has(name)) return map.get(name);
          const i = name.lastIndexOf(delimiter);
          map.set(name, data);
          if (i >= 0) {
            find({name: name.substring(0, i), children: []}).children.push(data);
            data.name = name.substring(i + 1);
          } else {
            root = data;
          }
          return data;
        });
        return root;
      }
      
    }
    .node {
      font: 300 11px "Helvetica Neue", Helvetica, Arial, sans-serif;
      fill: #fff;
    }
    
    .arcLabel {
      font: 300 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
      fill: #fff;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>

    关于javascript - D3.js分层边缘捆绑按组着色,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50709722/

    相关文章:

    javascript - 在可变高度图像的特定区域上定位 div

    javascript - 从回调函数内部使用 "pure"Node.js 重定向

    ruby-on-rails - HTML5 应用程序 list 未在 list 更改时清除缓存

    javascript - 如何防止双击时在 HTML5 Canvas 外选择文本?

    html - IE7 (CSS) 的定位问题

    javascript - 如何在iframe中控制img的宽度

    javascript - 使用 setTimeout 来解决或拒绝 promise

    javascript - 放入 div 时变量属性消失

    javascript - 弹出背景不占据屏幕高度的完整高度

    javascript - 如何隐藏页面上某种类型的第一个标签