Javascript 使用angular.js更改数据时触发d3.js更新

Javascript 使用angular.js更改数据时触发d3.js更新,javascript,angularjs,d3.js,Javascript,Angularjs,D3.js,我正在使用Angular.js创建一些可视化效果。我得到了一个条形图的指令,如下所示: angular.module('MissionControlApp').directive('d3Bars', ['d3', function(d3) { return { restrict: 'EA', scope: { data: "=", label: "=", onClick: "&

我正在使用Angular.js创建一些可视化效果。我得到了一个条形图的指令,如下所示:

angular.module('MissionControlApp').directive('d3Bars', ['d3', function(d3) {
    return {
        restrict: 'EA',
        scope: {
            data: "=",
            label: "=",
            onClick: "&"
        },
        link: function(scope, iElement) {
            var svg = d3.select(iElement[0])
                .append("svg")
                .attr("width", "100%");

            // on window resize, re-render d3 canvas
            window.onresize = function() {
                return scope.$apply();
            };
            scope.$watch(function(){
                    return angular.element(window)[0].innerWidth;
                }, function(){
                    return scope.render(scope.data);
                }
            );

            // watch for data changes and re-render
            scope.$watch('data', function(newVals) {
                return scope.render(newVals);
            }, true);

            // define render function
            scope.render = function(data){
                if(data === undefined){
                    return;
                }
                // remove all previous items before render
                svg.selectAll("*").remove();

                // setup variables
                var width, height, max;
                var margin = {top: 5, right: 30, bottom: 10, left: 150};

                width = d3.select(iElement[0])[0][0].offsetWidth - margin.left - margin.right;
                height = (scope.data.length * 35) + margin.top + margin.bottom;
                max = 100;

                // set the height based on the calculations above
                svg.attr('height', height);

                var x = d3.scale.linear()
                    .domain([0, 100])
                    .range([0, width]);

                var y = d3.scale.ordinal()
                    .domain(data.map(function (d) { return d.user; }))
                    .rangeBands([0, height], 0.1, 0.35);

                // var color = d3.scale.category20c();
                var color = d3.scale.linear()
                    .domain([0, 25, 50, 75, 100])
                    .range(["#51b75d", "#90eb9d","#ffff8c","#f5c93f","#c45c44"])
                    .interpolate(d3.interpolateHcl);

                var xAxis = d3.svg.axis()
                    .scale(x)
                    .orient("bottom")
                    .innerTickSize(-(height-5));

                //create the rectangles for the bar chart
                svg.selectAll("rect")
                    .data(data)
                    .enter()
                    .append("rect")
                    .attr("fill", function(d){return color(d.value); })
                    .on("click", function(d){return scope.onClick({item: d});})
                    .attr("height", y.rangeBand()) // height of each bar
                    .attr("width", 0) // initial width of 0 for transition
                    .attr("x", margin.left) // half of the 20 side margin specified above
                    .attr("y", function (d) { return y(d.user); }) // height + margin between bars
                    .transition()
                    .duration(1000)
                    .attr("width", function(d){ return d.value/(max/width); });

                svg.selectAll("text")
                    .data(data)
                    .enter()
                    .append("text")
                    .attr("fill", "#000")
                    .attr("y", function(d){return y(d.user) + (y.rangeBand() / 2);})
                    .attr("x", 150)
                    .attr("text-anchor", "end")
                    .attr("dy", ".35em")
                    .attr("dx", -5)
                    .text(function(d){return d[scope.label];});

                svg.append("g")
                    .selectAll("valueLabels")
                    .data(data)
                    .enter()
                    .append("text")
                    .attr("fill", "#000")
                    .attr("y", function(d){return y(d.user) + (y.rangeBand() / 2);})
                    .attr("x", function(d){return x(d.value) + margin.left;})
                    .attr("dx", 5)
                    .attr("dy", ".35em")
                    .text(function(d){return parseFloat(d.value).toFixed(0) + "%";})
                    .attr("fill-opacity", 0)
                    .transition()
                    .duration(1500)
                    .attr("fill-opacity", 1);

                svg.append("g")
                    .attr("class", "x axisHorizontal")
                    .attr("transform", "translate(" +  margin.left + "," + (height-margin.bottom) + ")")
                    .call(xAxis);
            };
        }
    };
}]);
angular.module('MissionControlApp').directive('d3WorksetUser', ['d3', function(d3) {
    return {
        restrict: 'E',
        scope: {
            data: "=",
            label: "=",
            onClick: "&"
        },
        link: function(scope, iElement) {
            var svg = d3.select(iElement[0])
                .append("svg")
                .attr("width", "100%");

            // on window resize, re-render d3 canvas
            window.onresize = function() {
                return scope.$apply();
            };
            scope.$watch(function(){
                    return angular.element(window)[0].innerWidth;
                }, function(){
                    return scope.render(scope.data);
                }
            );

            // watch for data changes and re-render
            scope.$watch('data', function(newVals) {
                return scope.render(newVals);
            }, true);

            // define render function
            scope.render = function(data){
                if(data === undefined){
                    return;
                }
                // remove all previous items before render
                svg.selectAll("*").remove();

                // setup variables
                var width, height;
                var margin = {top: 5, right: 10, bottom: 20, left: 25};

                width = d3.select(iElement[0])[0][0].offsetWidth - margin.left - margin.right;
                height = 300 - margin.top - margin.bottom;

                // set the height based on the calculations above
                svg.attr('height', height + margin.top + margin.bottom);

                var parseDate = d3.time.format('%Y-%m-%dT%H:%M:%S.%LZ').parse;

                var x = d3.time.scale().range([0, width]);
                var y = d3.scale.linear().range([height, 0]);

                var xAxis = d3.svg.axis()
                    .scale(x)
                    .orient("bottom")
                    .ticks(5);

                var yAxis = d3.svg.axis()
                    .scale(y)
                    .orient("left")
                    .ticks(9);

                var line = d3.svg.line()
                    .x(function(d) { return x(d.date) + margin.left; })
                    .y(function(d) { return y(d.value); });

                data.forEach(function(d) {
                    d.date = parseDate(d.createdOn);
                    d.value = +d.value;
                });

                x.domain(d3.extent(data, function(d) { return d.date; }));
                y.domain([0, 100]);

                // Add the valueline path.
                svg.append("path")
                    .datum(data)
                    .attr("d", line)
                    .attr("stroke", "steelblue")
                    .attr("fill", "none")
                    .attr("stroke-width", 1.5);

                // Add the X Axis
                svg.append("g")
                    .attr("class", "axisMain")
                    .attr("transform", "translate(" + margin.left + "," + (height + margin.top) + ")")
                    .call(xAxis);

                // Add the Y Axis
                svg.append("g")
                    .attr("class", "axisMain")
                    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
                    .call(yAxis);
            };
        }
    };
}]);
这定义了一个“onClick”事件,该事件依次在我的控制器中设置一段数据,如下所示:

vm.d3OnClick = function(item){
        // item {user: konrad.sobon, value: 26}
        SetUserData(item);
    };

    function SetUserData (item) {
        var allData = vm.selectedWorkset.onOpened;
        var userData = [];
        allData.forEach(function (d) {
            if(d.user === item.user){
                userData.push({
                    name: d.user,
                    value: (d.opened * 100) / (d.closed + d.opened),
                    createdOn: d.createdOn
                })
            }
        });
        console.log("SettingData to: ", userData);
        vm.userData = userData;
    }
vm.userData用于构建一个不同的图表,如下所示:

angular.module('MissionControlApp').directive('d3Bars', ['d3', function(d3) {
    return {
        restrict: 'EA',
        scope: {
            data: "=",
            label: "=",
            onClick: "&"
        },
        link: function(scope, iElement) {
            var svg = d3.select(iElement[0])
                .append("svg")
                .attr("width", "100%");

            // on window resize, re-render d3 canvas
            window.onresize = function() {
                return scope.$apply();
            };
            scope.$watch(function(){
                    return angular.element(window)[0].innerWidth;
                }, function(){
                    return scope.render(scope.data);
                }
            );

            // watch for data changes and re-render
            scope.$watch('data', function(newVals) {
                return scope.render(newVals);
            }, true);

            // define render function
            scope.render = function(data){
                if(data === undefined){
                    return;
                }
                // remove all previous items before render
                svg.selectAll("*").remove();

                // setup variables
                var width, height, max;
                var margin = {top: 5, right: 30, bottom: 10, left: 150};

                width = d3.select(iElement[0])[0][0].offsetWidth - margin.left - margin.right;
                height = (scope.data.length * 35) + margin.top + margin.bottom;
                max = 100;

                // set the height based on the calculations above
                svg.attr('height', height);

                var x = d3.scale.linear()
                    .domain([0, 100])
                    .range([0, width]);

                var y = d3.scale.ordinal()
                    .domain(data.map(function (d) { return d.user; }))
                    .rangeBands([0, height], 0.1, 0.35);

                // var color = d3.scale.category20c();
                var color = d3.scale.linear()
                    .domain([0, 25, 50, 75, 100])
                    .range(["#51b75d", "#90eb9d","#ffff8c","#f5c93f","#c45c44"])
                    .interpolate(d3.interpolateHcl);

                var xAxis = d3.svg.axis()
                    .scale(x)
                    .orient("bottom")
                    .innerTickSize(-(height-5));

                //create the rectangles for the bar chart
                svg.selectAll("rect")
                    .data(data)
                    .enter()
                    .append("rect")
                    .attr("fill", function(d){return color(d.value); })
                    .on("click", function(d){return scope.onClick({item: d});})
                    .attr("height", y.rangeBand()) // height of each bar
                    .attr("width", 0) // initial width of 0 for transition
                    .attr("x", margin.left) // half of the 20 side margin specified above
                    .attr("y", function (d) { return y(d.user); }) // height + margin between bars
                    .transition()
                    .duration(1000)
                    .attr("width", function(d){ return d.value/(max/width); });

                svg.selectAll("text")
                    .data(data)
                    .enter()
                    .append("text")
                    .attr("fill", "#000")
                    .attr("y", function(d){return y(d.user) + (y.rangeBand() / 2);})
                    .attr("x", 150)
                    .attr("text-anchor", "end")
                    .attr("dy", ".35em")
                    .attr("dx", -5)
                    .text(function(d){return d[scope.label];});

                svg.append("g")
                    .selectAll("valueLabels")
                    .data(data)
                    .enter()
                    .append("text")
                    .attr("fill", "#000")
                    .attr("y", function(d){return y(d.user) + (y.rangeBand() / 2);})
                    .attr("x", function(d){return x(d.value) + margin.left;})
                    .attr("dx", 5)
                    .attr("dy", ".35em")
                    .text(function(d){return parseFloat(d.value).toFixed(0) + "%";})
                    .attr("fill-opacity", 0)
                    .transition()
                    .duration(1500)
                    .attr("fill-opacity", 1);

                svg.append("g")
                    .attr("class", "x axisHorizontal")
                    .attr("transform", "translate(" +  margin.left + "," + (height-margin.bottom) + ")")
                    .call(xAxis);
            };
        }
    };
}]);
angular.module('MissionControlApp').directive('d3WorksetUser', ['d3', function(d3) {
    return {
        restrict: 'E',
        scope: {
            data: "=",
            label: "=",
            onClick: "&"
        },
        link: function(scope, iElement) {
            var svg = d3.select(iElement[0])
                .append("svg")
                .attr("width", "100%");

            // on window resize, re-render d3 canvas
            window.onresize = function() {
                return scope.$apply();
            };
            scope.$watch(function(){
                    return angular.element(window)[0].innerWidth;
                }, function(){
                    return scope.render(scope.data);
                }
            );

            // watch for data changes and re-render
            scope.$watch('data', function(newVals) {
                return scope.render(newVals);
            }, true);

            // define render function
            scope.render = function(data){
                if(data === undefined){
                    return;
                }
                // remove all previous items before render
                svg.selectAll("*").remove();

                // setup variables
                var width, height;
                var margin = {top: 5, right: 10, bottom: 20, left: 25};

                width = d3.select(iElement[0])[0][0].offsetWidth - margin.left - margin.right;
                height = 300 - margin.top - margin.bottom;

                // set the height based on the calculations above
                svg.attr('height', height + margin.top + margin.bottom);

                var parseDate = d3.time.format('%Y-%m-%dT%H:%M:%S.%LZ').parse;

                var x = d3.time.scale().range([0, width]);
                var y = d3.scale.linear().range([height, 0]);

                var xAxis = d3.svg.axis()
                    .scale(x)
                    .orient("bottom")
                    .ticks(5);

                var yAxis = d3.svg.axis()
                    .scale(y)
                    .orient("left")
                    .ticks(9);

                var line = d3.svg.line()
                    .x(function(d) { return x(d.date) + margin.left; })
                    .y(function(d) { return y(d.value); });

                data.forEach(function(d) {
                    d.date = parseDate(d.createdOn);
                    d.value = +d.value;
                });

                x.domain(d3.extent(data, function(d) { return d.date; }));
                y.domain([0, 100]);

                // Add the valueline path.
                svg.append("path")
                    .datum(data)
                    .attr("d", line)
                    .attr("stroke", "steelblue")
                    .attr("fill", "none")
                    .attr("stroke-width", 1.5);

                // Add the X Axis
                svg.append("g")
                    .attr("class", "axisMain")
                    .attr("transform", "translate(" + margin.left + "," + (height + margin.top) + ")")
                    .call(xAxis);

                // Add the Y Axis
                svg.append("g")
                    .attr("class", "axisMain")
                    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
                    .call(yAxis);
            };
        }
    };
}]);
当我在Chrome中调试时,我可以看到我的点击事件触发了控制器上的相应方法,vm.userData被设置为新数据,但我的折线图没有更新……基本上什么都没有发生。是否有明显遗漏的内容会使一个图表中的单击事件刷新/重画另一个图表

HTML:

<div class="row">
    <div class="panel-group" id="accordion">
        <div class="panel panel-default">
            <div class="panel-heading" data-toggle="collapse" data-parent="#accordion" data-target="#collapseOne">
                <h4 class="panel-title">Workset on Opened</h4>
            </div>
            <div id="collapseOne" class="panel-collapse collapse in">
                <div class="panel-body">
                    <d3-bars data="vm.DataOpened" label="vm.d3Label" on-click="vm.d3OnClick(item)"></d3-bars>
                </div>
            </div>
        </div>
    </div>
</div>

<div class="row">
    <div class="panel-group" id="userDetail">
        <div class="panel panel-default">
            <div class="panel-heading" data-toggle="collapse" data-parent="#userDetail" data-target="#collapseUserDetail">
                <h4 class="panel-title">User detail info.</h4>
            </div>
            <div id="collapseUserDetail" class="panel-collapse collapse in">
                <div class="panel-body">
                    <d3-workset-user data="vm.userData"></d3-workset-user>
                </div>
            </div>
        </div>
    </div>
</div>

已打开上的工作集
用户详细信息。
两个图表在第一次加载时都画得很好,但第二次加载后,图表永远不会更新

这里有一个控制台日志来支持:

其中:将数据设置为:是在我第一次从服务器获取数据并进行设置时由控制器触发的。 然后我的
范围。$watch
会对此触发两次。我认为第一种情况是数据仍然
未定义
,然后在解析后再次出现。
最后,还有一些与鼠标单击和数据更新相关的事件,但正如您所看到的那样,
watch
从不重新触发。

将第二个
$watch
功能更改为以下内容:

// watch for data changes and re-render
scope.$watch('data', function(newVals) {
    $timeout(function() { scope.render(newVals) });
}, true);
我仍然会保留
$watch
,并使用
$timeout
而不是
$interval
,这样应用程序就不会浪费摘要周期


之所以会发生这种情况,是因为scope.$watch在摘要周期内被触发,而且angular是基于事件的,因此在周期内更改值不会重新触发手表。带有
0 ms
超时的
$timeout
在当前摘要周期之后安排另一个摘要周期。基本上(iirc),
$timeout
是一个
setTimeout
,里面有一个
作用域。

你检查过你的
作用域了吗。$watch('data',…
确实有效吗?如果你已经检查过,很抱歉。甚至可能会添加一个
作用域。$apply
我认为它有效。我放了一个控制台。log()它在加载页面时触发一次。然后它再也不会触发。我认为它应该在数据更新时触发,对吗?这就是想法。所以它在原始页面加载时触发,这很好,因为我认为这是数据第一次被设置的时候。但是,我再次从onClick事件中设置了相同的变量vm.UserData,它会oesn不会触发。请查看控制台日志,然后尝试
$timeout(function(){})
,使Angular在未进行时自动执行此操作。(注意,将函数保留为空)