如何为路径过渡线性渐变

时间:2018-07-09 18:08:04

标签: javascript css d3.js svg javascript-events

我正在研究一个sankey图,只要我使用d3.js和sankey插件更改过滤器(单选按钮),该图就会更新。现在,我尝试添加一项功能,每当我将鼠标悬停在一条路径上时,都会在从源节点的颜色到目标节点的颜色的路径上添加线性渐变。如果我不使用滤镜,一切正常,但是如果我应用滤镜(颜色设置错误),则渐变颜色将不起作用,因为链接已过渡。我认为我必须以某种方式转换线性渐变,但是我不明白我该怎么做。

我写了一个小脚本来显示问题,在单击按钮颜色正确之前,以及弄乱之后,都是这样。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script src="https://unpkg.com/d3-sankey@0.6"></script>
</head>
<body>
    <svg id="diagram" height="150" width="600"></svg>
    <button onclick="updateSankey()">Click Me!</button>

    <style>
        #diagram{
            border: 1px solid black;
        }
    </style>

    <script>
        var target = 0;
        var sankeyLinks;
        var sankeyData = {nodes:[], links:[]};

        calculateLinks();
        initSankey();
        updateSankey();

        function initSankey() {
            /*simple initialisation of the sankey, should explain itself*/

            svg = d3.select("svg"),
                width = +svg.attr("width"),
                height = +svg.attr("height");

            formatNumber = d3.format(",.0f"),
                format = function (d) { return formatNumber(d) + " %"; },
                color = d3.scaleOrdinal(d3.schemeCategory10);

            sankey = d3.sankey()
                .nodeWidth(15)
                .nodePadding(10)
                .extent([[1, 1], [width - 1, height - 6]])
                .iterations(0);

            t = d3.transition()
                .duration(1500)
                .ease(d3.easeLinear);

            //set attributes for all links
            titleGroup = svg.append("g")
                .attr("class", "titles")
                .attr("font-family", "sans-serif")
                .attr("font-size", "150%");

            diagram= svg.append("g")
                .attr("class", "sankey")
               // .attr("transform", "translate(" + marginleft + "," + margintop + ")");

            linkGroup = diagram.append("g")
                .attr("class", "links")
                .attr("fill", "none");
            //.attr("stroke", "#000")
            //.attr("stroke-opacity", 0.2);

            //set attributes for all nodes
            nodeGroup = diagram.append("g")
                .attr("class", "nodes")
                .attr("font-family", "sans-serif")
                .attr("font-size", 10);
        }

        function calculateLinks() {
            if(target == 0)
            {
                target = 1;
                sankeyLinks = [{source:0, target:1, value:5},{source:0, target:2, value:10},{source:0, target:3, value:15}];
            }
            else
            {
                target = 0;
                sankeyLinks = [{source:0, target:2, value:15},{source:0, target:1, value:20},{source:0, target:3, value:10}];
            }
        }

        function updateSankey() {
            calculateLinks();
            sankeyData.links = sankeyLinks;
            sankeyData.nodes =  [{name: "first"}, {name:"second"}, {name:"third"}, {name: "fourth"}];
            sankey(sankeyData);

            var links = linkGroup.selectAll('path')
                .data(sankeyData.links);

            //Set attributes for each link separately
            links.enter().append("g")
                .attr("id",function (d,i) {return "path"+i;})
                .append("path")
                .attr("stroke", "#000")
                .attr("stroke-opacity", 0.15)
                .attr("d", d3.sankeyLinkHorizontal())
                .attr("stroke-width", function (d) {return Math.max(1, d.width); })
                .on("mouseover",function (d,id) {
                    var pathGroup = svg.select('#path' + id);
                    var path = pathGroup.select("path");

                    path.attr("stroke","url(#grad"+id+")")
                        .attr("stroke-opacity","0.95");
                })
                .on("mouseout",function (d, id) {
                    pathGroup = svg.select('#path' + id);
                    var path = pathGroup.select("path");

                    path.attr("stroke","#000")
                        .attr("stroke-opacity","0.15");
                })
                .append("title")
                .text(function (d) {
                    //tooltip info for the links
                    return d.source.name + " → " + d.target.name + "\n" + format(d.value); });

            var pathGradient = svg.select(".links")
                .selectAll("g")
                .append("defs")
                .append("linearGradient")
                .attr("id",function (d, id) {
                    return "grad" + id;
                })
                //.attr("from", function () {return this.parentElement.parentElement.childNodes[0].getAttribute("from");})
                //.attr("to", function () {return this.parentElement.parentElement.childNodes[0].getAttribute("to");})
                .attr("gradientUnit","userSpaceOnUse")
                .attr("style","mix-blend-mode: multiply;")
                .attr("x1","0%")
                .attr("x2","100%")
                .attr("y1","0%")
                .attr("y2","0%");

            pathGradient.append("stop")
                .attr("class","from")
                .attr("offset","0%")
                .attr("style", function (d) {
                    var color = setColor(d.source);
                    return "stop-color:" + color + ";stop-opacity:1";
                });

            pathGradient.append("stop")
                .attr("class","to")
                .attr("offset","100%")
                .attr("style",function (d) {
                    var color = setColor(d.target);
                    return "stop-color:" + color + ";stop-opacity:1";
                });

            links.transition(t)
                .attr("d", d3.sankeyLinkHorizontal())
                .attr("stroke-width", function (d) { return Math.max(1, d.width); })
                .select('title')
                .text(function (d) {
                    //same argumentation as above, we need the method again for the transition
                    return d.source.name + " → " + d.target.name + "\n" + format(d.value); });

            links.exit().remove();

            var nodes = nodeGroup.selectAll('.node')
                .data(sankeyData.nodes);

            var nodesEnter = nodes.enter()
                .append("g")
                .attr('class', 'node');

            //set attributes for each node separately
            nodesEnter.append("rect")
                .attr("x", function (d) { return d.x0; })
                .attr("y", function (d) { return d.y0; })
                .attr("height", function (d) { return d.y1 - d.y0; })
                .attr("width", function (d) {
                    var width = d.x1 - d.x0;
                    return width;
                })
                .attr("fill", setColor)
                .attr("stroke", "#000")
                .attr("fill-opacity", 0.5)

            //specify Pop-Up when hovering over node
            nodesEnter.append("title")
                .text(function (d) { return d.name + "\n" + format(d.value); });

            //Update selection
            var nodesUpdate = nodes.transition(t);

            //same as the links we have to state the methods again in the update
            nodesUpdate.select("rect")
                .attr("y", function (d) { return d.y0; })
                .attr("x", function (d) { return d.x0; })
                .attr("height", function (d) { return d.y1 - d.y0; });

            nodesUpdate.select("title")
                .text(function (d) { return d.name + "\n" + format(d.value); });

            //Exit selection
            nodes.exit().remove();
        }

        function setColor(d) {
            switch (d.name) {
                case "first":
                    return "#f00";
                case "second":
                    return "#ff0";
                case "third":
                    return "#f0f";
                case "fourth":
                    return "#0ff";
                default:
                    return "#0f0";
            }
        }

    </script>
</body>
</html>

单击一次按钮后,从红色到紫色节点的路径具有从红色到黄色的线性渐变,即使我希望它从红色变为紫色。

我已经意识到,我可以在.iterations(15)中写例如.iterations(0)而不是initSankey()来解决这个问题。在实际的项目中,我无法执行此操作,因为必须强制节点的顺序。

我希望我的解释足够清楚,如果没有,请随时提问。

如果有人可以告诉我如何解决此问题,我将非常高兴。

问候

PS。在此摘要中,顶部的链接在悬停时消失了,我已经在实际项目中修复了此问题,这没什么大不了的

1 个答案:

答案 0 :(得分:2)

您的问题是,渐变网址基于i,对于每次更新的特定链接,它可能会有所不同(即,链接的顺序可能不同,因此i值会有所不同);并且数据更新不是基于链接的恒定唯一ID。

在代码段中,我为calculateLinks函数中的链接添加了唯一的名称值,然后将其用于数据连接并创建def渐变,这意味着它们在每次更新时保持不变。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script src="https://unpkg.com/d3-sankey"></script>
</head>
<body>
    <svg id="diagram" height="150" width="600"></svg>
    <button onclick="updateSankey()">Click Me!</button>

  
    <style>
        #diagram{
            border: 1px solid black;
        }
    </style>

    <script>
        var target = 0;
        var sankeyLinks;
        var sankeyData = {nodes:[], links:[]};

        calculateLinks();
        initSankey();
        updateSankey();

        function initSankey() {

            svg = d3.select("svg"),
                width = +svg.attr("width"),
                height = +svg.attr("height");

            formatNumber = d3.format(",.0f"),
                format = function (d) { return formatNumber(d) + " %"; },

            sankey = d3.sankey()
                .nodeWidth(15)
                .nodePadding(10)
                .size([width - 1, height - 6])

            t = d3.transition()
                .duration(1500)
                .ease(d3.easeLinear);

            //set attributes for all links
            titleGroup = svg.append("g")
                .attr("class", "titles")
                .attr("font-family", "sans-serif")
                .attr("font-size", "150%");

            diagram= svg.append("g")
                .attr("class", "sankey")
            
            svg.append("defs")
               
            linkGroup = diagram.append("g")
                .attr("class", "links")
                .attr("fill", "none");

            //set attributes for all nodes
            nodeGroup = diagram.append("g")
                .attr("class", "nodes")
                .attr("font-family", "sans-serif")
                .attr("font-size", 10);
        }

        function calculateLinks() {
            if(target == 0)
            {
                target = 1;
                sankeyLinks = [
                  {name: "firstsecond",source:0, target:1, value:5},
                  {name: "firstthird",source:0, target:2, value:10},
                  {name: "firstfourth",source:0, target:3, value:15}];
            }
            else
            {
                target = 0;
                sankeyLinks = [
                  {name: "firstthird", source:0, target:2, value:15},
                  {name: "firstsecond", source:0, target:1, value:20},
                  {name: "firstfourth", source:0, target:3, value:10}
                ];
            }
        }

        function updateSankey() {
            calculateLinks();
            sankeyData.links = sankeyLinks;
            sankeyData.nodes =  [{name: "first"}, {name:"second"}, {name:"third"}, {name: "fourth"}];
            sankey(sankeyData);
         
          
          var pathGradient = svg.select("defs").selectAll("linearGradient")
          			.data(sankeyData.links, function(d){ return d.name })
          			.enter()
                .append("linearGradient")
                .attr("id",function (d) {
                    return "grad" + d.name;
                })
                .attr("gradientUnit","userSpaceOnUse")
                .attr("x1","0%")
                .attr("x2","100%")
                .attr("y1","0%")
                .attr("y2","0%");

            pathGradient.append("stop")
                .attr("class","from")
                .attr("offset","0%")
                .attr("style", function (d) {
                    var color = setColor(d.source);
                    return "stop-color:" + color;
                });

            pathGradient.append("stop")
                .attr("class","to")
                .attr("offset","100%")
                .attr("style",function (d) {
                    var color = setColor(d.target);
                    return "stop-color:" + color;
                });

            var links = linkGroup.selectAll('path')
                .data(sankeyData.links, function(d){ return d.name });

            //Set attributes for each link separately
            var linksenter = links.enter()
            		.append("g")
                .attr("id",function (d) {return "path" + d.name;})
                .append("path")
                .style("stroke", "#000")
                .style("stroke-opacity", 0.15)
                .attr("stroke-width", function (d) {return Math.max(1, d.width); })
                .on("mouseover",function (d) {
                    var pathGroup = svg.select('#path' + d.name);
                    var path = pathGroup.select("path");

                    path.style("stroke","url(#grad" + d.name + ")")
                        .style("stroke-opacity","0.95");
                })
                .on("mouseout",function (d, id) {
                    pathGroup = svg.select('#path' + d.source.name + d.target.name);
                    var path = pathGroup.select("path");

                    path.style("stroke","#000")
                        .style("stroke-opacity","0.15");
                })
      
						linksenter.merge(links).attr("d", d3.sankeyLinkHorizontal())
            
            links.transition(t)
                .attr("d", d3.sankeyLinkHorizontal())
                .attr("stroke-width", function (d) { return Math.max(1, d.width); })
                .select('title')
                .text(function (d) {
                    return d.source.name + " → " + d.target.name + "\n" + format(d.value); });

            var nodes = nodeGroup.selectAll('.node')
                .data(sankeyData.nodes, function(d){ return d.name });

            var nodesEnter = nodes.enter()
                .append("g")
                .attr('class', 'node');
						
          	nodesEnter.append("rect")
                .attr("x", function (d) { return d.x0; })
                .attr("y", function (d) { return d.y0; })
                .attr("height", function (d) { return d.y1 - d.y0; })
                .attr("width", function (d) {
                    var width = d.x1 - d.x0;
                    return width;
                })
                .attr("fill", setColor)
                .attr("stroke", "#000")
                .attr("fill-opacity", 0.5)

            //specify Pop-Up when hovering over node
            nodesEnter.append("title")
                .text(function (d) { return d.name + "\n" + format(d.value); });

            //Update selection
            var nodesUpdate = nodes.transition(t);

            //same as the links we have to state the methods again in the update
            nodesUpdate.select("rect")
                .attr("y", function (d) { return d.y0; })
                .attr("x", function (d) { return d.x0; })
                .attr("height", function (d) { return d.y1 - d.y0; });

            nodesUpdate.select("title")
                .text(function (d) { return d.name + "\n" + format(d.value); });

        }

        function setColor(d) {
            switch (d.name) {
                case "first":
                    return "#f00";
                case "second":
                    return "#ff0";
                case "third":
                    return "#f0f";
                case "fourth":
                    return "#0ff";
                default:
                    return "#0f0";
            }
        }

    </script>
</body>
</html>