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

Tro*_*ats 5 html javascript css svg d3.js

我正在尝试根据连接到的组来为分层边缘捆绑可视化中的连接着色。可以在这里看到这样的一个例子。

在此输入图像描述

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

    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));
    }
Run Code Online (Sandbox Code Playgroud)

这是我发布的示例中的鼠标悬停函数:

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); 
            });
}
Run Code Online (Sandbox Code Playgroud)

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

我的完整代码在这里:

 <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>
Run Code Online (Sandbox Code Playgroud)

Rob*_*zie 2

这是 D3 v6 中的一个示例解决方案,改编了 Observable示例以及我对另一个问题的回答。基本要点:

  • 您将把“组”添加到输入数据中 - 对于您在注释中提到的数据,我将其定义group为名称的第二个元素(每个点分隔)。hierarchyObservable 中的函数似乎去除了这一点。
  • 幸运的是,所有name值都是例如root.parent.child- 这使得 leafGroups 非常适合您的数据(但可能不适用于不对称层次结构)。
  • 定义颜色范围,例如const colors = d3.scaleOrdinal().domain(leafGroups.map(d => d[0])).range(d3.schemeTableau10);可用于弧、标签文本nodelink路径
  • 我避免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");    
}
Run Code Online (Sandbox Code Playgroud)

请参阅以下评论中的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);    
}
Run Code Online (Sandbox Code Playgroud)

使用您在评论中提到的数据的工作示例:

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");    
}
Run Code Online (Sandbox Code Playgroud)
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);    
}
Run Code Online (Sandbox Code Playgroud)
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;
  }
  
}
Run Code Online (Sandbox Code Playgroud)