簡體   English   中英

D3.js分層邊緣捆綁按組着色

[英]D3.js hierarchical edge bundling coloring by group

我正在嘗試根據它們連接到的組為我的分層邊緣捆綁可視化中的連接着色。 這方面的一個例子可以在這里看到。

在此處輸入圖像描述

這是我當前的鼠標懸停功能:

    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 有點陌生,但我假設我需要做的是根據密鑰檢查組,然后將其匹配到與該組相同的顏色。

我的完整代碼在這里:

 <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示例的示例解決方案以及我對其他問題的回答 基本點:

  • 您將在輸入數據中添加“組” - 對於您在評論中提到的數據,我將group定義為名稱的第二個元素(每點分隔)。 Observable 中的hierarchy函數似乎去除了這一點。
  • 幸運的是,所有name值都是例如root.parent.child - 這使得leafGroups 非常適合您的數據(但可能不適用於非對稱層次結構)。
  • 定義一個顏色范圍,例如const colors = d3.scaleOrdinal().domain(leafGroups.map(d => d[0])).range(d3.schemeTableau10); 可用於弧、標簽文本( node )、路徑( link
  • 我已經避免在示例中使用mix-blend-mode樣式,因為它對我來說看起來不太好。
  • 我正在應用overedouted的樣式 - 請參閱下面的邏輯。

有關mouseover的樣式邏輯,請參閱overed中的注釋:

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");    
}

有關mouseout的樣式邏輯,請參閱outed中的注釋:

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 => dy) .angle(d => dx); 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 => dx), end: d3.max(g[1], d => dx) })); 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(${dx * 180 / Math.PI - 90}) translate(${dy}, 0)`) .append("text") .attr("dy", "0.31em") .attr("x", d => dx < Math.PI ? (arcWidth + 5) : (arcWidth + 5) * -1) .attr("text-anchor", d => dx < Math.PI ? "start" : "end") .attr("transform", d => dx >= 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>

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM