【问题标题】:Get the data from a GET request before rendering D3.js graph在渲染 D3.js 图之前从 GET 请求中获取数据
【发布时间】:2021-01-31 21:34:33
【问题描述】:

我正在尝试为数据库中的一些数据制作网络图,我正在为前端开发 AngularJS(后端为 C#)。

我找到了 d3.js,它看起来很完美,但我根本不是 JavaScript 专家。 我从他那里拿了例子https://observablehq.com/@d3/mobile-patent-suits?collection=@d3/d3-force (其实肯定是以前的版本,或者是有人从 Github 上 fork 的东西,现在找不到了,反正就是核心)。

我的问题是,我必须从调用 API 的函数 self.GetLinks 加载 JSON var links 数组并返回同类型对象的 JSON。 该函数在onInit 中被调用,但到它被调用时,所有内容都已呈现在屏幕上,self.GetLinks 的唯一结果是更改var links 内部的内容,而此时已经太晚了。

为了清楚起见,这里显示的数组var links 是我从示例中获取的,对我来说没用但结构是一样的。

我需要了解如何在发生任何事情之前调用self.GetLinks,以便var links 的内容是我实际上从后端获得的,或者更好的是如何制作它,以便当@987654330 的内容@更改图表再次呈现(这样我可以动态添加元素)。

function MyController($scope, MyService, $routeParams, viewModelHelper) {

    var self = this;
    self.model = {
        id: parseInt($routeParams.Id) || -1,
        links: {},
    };

    self.$onInit = function () {
        self.GetLinks(self.model.id);
    };

     var links = [
        { source: "Microsoft", target: "Amazon", type: "licensing" },
        { source: "Microsoft", target: "HTC", type: "licensing" },
        { source: "Samsung", target: "Apple", type: "suit" },
        { source: "Motorola", target: "Apple", type: "suit" },
        { source: "Nokia", target: "Apple", type: "resolved" },
        { source: "HTC", target: "Apple", type: "suit" },
        { source: "Kodak", target: "Apple", type: "suit" },
        { source: "Microsoft", target: "Barnes & Noble", type: "suit" },
        { source: "Microsoft", target: "Foxconn", type: "suit" },
        { source: "Oracle", target: "Google", type: "suit" },
        { source: "Apple", target: "HTC", type: "suit" },
        { source: "Microsoft", target: "Inventec", type: "suit" },
        { source: "Samsung", target: "Kodak", type: "resolved" },
        { source: "LG", target: "Kodak", type: "resolved" },
        { source: "RIM", target: "Kodak", type: "suit" },
        { source: "Sony", target: "LG", type: "suit" },
        { source: "Kodak", target: "LG", type: "resolved" },
        { source: "Apple", target: "Nokia", type: "resolved" },
        { source: "Qualcomm", target: "Nokia", type: "resolved" },
        { source: "Apple", target: "Motorola", type: "suit" },
        { source: "Microsoft", target: "Motorola", type: "suit" },
        { source: "Motorola", target: "Microsoft", type: "suit" },
        { source: "Huawei", target: "ZTE", type: "suit" },
        { source: "Ericsson", target: "ZTE", type: "suit" },
        { source: "Kodak", target: "Samsung", type: "resolved" },
        { source: "Apple", target: "Samsung", type: "suit" },
        { source: "Kodak", target: "RIM", type: "suit" },
        { source: "Nokia", target: "Qualcomm", type: "suit" },
        { source: "Pippo", target: "Pippo" },
        { source: "Paperino", target: "Pippo", type: "suit" }
    ];

    self.GetLinks = function (id) {
        viewModelHelper.apiGet("Api/GetLinks/" + id,
            null,
            function (result) {
                links = result.data.Elements;

                var message = "Success";
                setResultMessage(message, "Info");
            },
            function (result) {

                var message = "Error " + result.data.Message;
                setResultMessage(message, "danger");
            },
            function (result) {

            });        

    };

    var nodes = {};


    // Compute the distinct nodes from the links.
    links.forEach(function (link) {
        link.source = nodes[link.source] || (nodes[link.source] = { name: link.source });
        link.target = nodes[link.target] || (nodes[link.target] = { name: link.target });
    });


    var w = 1400,
        h = 900;

    var force = d3.layout.force()
        .nodes(d3.values(nodes))
        .links(links)
        .size([w, h])
        .linkDistance(200)
        .charge(-1200)
        .on("tick", tick)
        .start();

    var svg = d3.select("body").append("svg:svg")
        .attr("width", w)
        .attr("height", h);

    // Per-type markers, as they don't inherit styles.
    svg.append("svg:defs").selectAll("marker")
        .data(["suit", "licensing", "resolved"])
        .enter().append("svg:marker")
        .attr("id", String)
        .attr("viewBox", "0 -5 10 10")
        .attr("refX", 15)
        .attr("refY", -1.5)
        .attr("markerWidth", 6)
        .attr("markerHeight", 6)
        .attr("orient", "auto")
        .append("svg:path")
        .attr("d", "M0,-5L10,0L0,5");

    var path = svg.append("svg:g").selectAll("path")
        .data(force.links())
        .enter().append("svg:path")
        .attr("id", function (d) { return d.source.index + "_" + d.target.index; })
        .attr("class", function (d) { return "link " + d.type; })
        .attr("marker-end", function (d) { return "url(#" + d.type + ")"; });

    var circle = svg.append("svg:g").selectAll("circle")
        .data(force.nodes())
        .enter().append("svg:image")
        .attr("class", "circle")
        .attr("xlink:href", "/Image/icon.png")
        .attr("x", "-8px")
        .attr("y", "-8px")
        .attr("width", "50px")
        .attr("height", "50px")
        .call(force.drag);



    var text = svg.append("svg:g").selectAll("g")
        .data(force.nodes())
        .enter().append("svg:g");

    // A copy of the text with a thick white stroke for legibility.
    text.append("svg:text")
        .attr("x", 2)
        .attr("y", 50)//".31em"
        .attr("class", "shadow")
        .text(function (d) { return d.name; });

    text.append("svg:text")
        .attr("x", 2)
        .attr("y", 50)
        .text(function (d) { return d.name; });

    var path_label = svg.append("svg:g").selectAll(".path_label")
        .data(force.links())
        .enter().append("svg:text")
        .attr("class", "path_label")
        .append("svg:textPath")
        .attr("startOffset", "50%")
        .attr("text-anchor", "middle")
        .attr("xlink:href", function (d) { return "#" + d.source.index + "_" + d.target.index; })
        .style("fill", "#000")
        .style("font-family", "Arial")
        .text(function (d) { return d.type; });

    // Use elliptical arc path segments to doubly-encode directionality.
    function tick() {
        path.attr("d", function (d) {
            var dx = d.target.x - d.source.x,
                dy = d.target.y - d.source.y,
                dr = Math.sqrt(dx * dx + dy * dy);
            return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
        });

        circle.attr("transform", function (d) {
            return "translate(" + d.x + "," + d.y + ")";
        });

        text.attr("transform", function (d) {
            return "translate(" + d.x + "," + d.y + ")";
        });
    }

    function setResultMessage(message, type) {
        self.ResponseModel = {};
        self.ResponseModel.ResponseAlert = true;
        self.ResponseModel.ResponseType = type;
        self.ResponseModel.ResponseMessage = message;
    }
}

【问题讨论】:

    标签: javascript angularjs svg d3.js


    【解决方案1】:

    你需要做两件事。第一个是将逻辑分为“首次渲染”和“更新”部分。在第一个渲染部分,你只是假设你在一个空白的 SVG 上绘图。在更新部分,您使用您的数据更新已更改的内容。

    在本例中,第一个渲染部分可用于绘制svgdefs。然后更新部分可以处理在正确位置绘制节点和链接。


    您需要做的第二件事是查看触发函数。这可以在您现在使用的回调中或通过 Promise 完成(参见 AngularJS 的$q)。


    最后,不在函数或构造函数中执行所有这些初始化操作是不好的做法。我已将您的代码分成firstRenderupdate 部分,从$onInit 调用firstRender,并模拟API 仅在2 秒后返回。请注意图表在 2 秒后是如何更新的。

    function MyController($scope) {
    
      var self = this;
      self.model = {
        id: -1,
        links: {},
      };
    
      self.$onInit = function() {
        self.GetLinks(self.model.id);
        self.firstRender();
      };
    
      var links = [];
      var nodes = {};
      var w = 1400,
        h = 900;
      var force;
      var svg;
      var paths;
      var circles;
      var texts;
      var pathLabels;
    
      self.GetLinks = function(id) {
        viewModelHelper.apiGet("Api/GetLinks/" + id,
          null,
          function(result) {
            links = result;
            self.update();
          }
        )
      };
    
      self.firstRender = function() {
        force = d3.layout.force()
          .size([w, h])
          .linkDistance(200)
          .charge(-1200)
          .on("tick", tick);
    
        svg = d3.select("body").append("svg:svg")
          .attr("width", w)
          .attr("height", h);
    
        // Per-type markers, as they don't inherit styles.
        svg.append("svg:defs").selectAll("marker")
          .data(["suit", "licensing", "resolved"])
          .enter().append("svg:marker")
          .attr("id", String)
          .attr("viewBox", "0 -5 10 10")
          .attr("refX", 15)
          .attr("refY", -1.5)
          .attr("markerWidth", 6)
          .attr("markerHeight", 6)
          .attr("orient", "auto")
          .append("svg:path")
          .attr("d", "M0,-5L10,0L0,5");
    
        paths = svg.append("svg:g");
        circles = svg.append("svg:g");
        texts = svg.append("svg:g");
        pathLabels = svg.append("svg:g");
      }
    
      self.update = function() {
        // Compute the distinct nodes from the links.
        links.forEach(function(link) {
          link.source = nodes[link.source] || (nodes[link.source] = {
            name: link.source
          });
          link.target = nodes[link.target] || (nodes[link.target] = {
            name: link.target
          });
        });
    
        force
          .nodes(d3.values(nodes))
          .links(links)
          .start();
    
        var path = paths.selectAll("path")
          .data(force.links());
    
        path.exit()
          .remove();
        path.enter()
          .append("svg:path");
    
        path.attr("id", function(d) {
            return d.source.index + "_" + d.target.index;
          })
          .attr("class", function(d) {
            return "link " + d.type;
          })
          .attr("marker-end", function(d) {
            return "url(#" + d.type + ")";
          });
    
        // var circle = circles.selectAll(".circle")
        //   .data(force.nodes());
        //
        // circle.exit()
        //   .remove();
        // circle.enter()
        //   .append("svg:image")
        //   .attr("class", "circle")
        //   .attr("xlink:href", "/Image/icon.png")
        //   .attr("x", "-8px")
        //   .attr("y", "-8px")
        //   .attr("width", "50px")
        //   .attr("height", "50px")
        //   .call(force.drag);
        var circle = circles.selectAll(".circle")
          .data(force.nodes());
    
        circle.exit()
          .remove();
        circle.enter()
          .append("svg:circle")
          .attr("class", "circle")
          .attr("r", 16)
          .call(force.drag);
    
        var text = texts.selectAll("g")
          .data(force.nodes());
    
        text.exit()
          .remove();
        text.enter()
          .append("svg:text")
          .attr("x", 2)
          .attr("y", 50) //".31em"
    
        text.text(function(d) {
          return d.name;
        });
    
        var pathLabel = pathLabels
          .selectAll(".path_label")
          .data(force.links());
    
        pathLabel.exit().remove();
        pathLabel.enter()
          .append("svg:text")
          .attr("class", "path_label")
          .append("svg:textPath")
          .attr("startOffset", "50%")
          .attr("text-anchor", "middle")
          .style("fill", "#000")
          .style("font-family", "Arial");
    
        pathLabel
          .attr("xlink:href", function(d) {
            return "#" + d.source.index + "_" + d.target.index;
          })
          .text(function(d) {
            return d.type;
          });
      }
    
      // Use elliptical arc path segments to doubly-encode directionality.
      function tick() {
        paths.selectAll("path").attr("d", function(d) {
          var dx = d.target.x - d.source.x,
            dy = d.target.y - d.source.y,
            dr = Math.sqrt(dx * dx + dy * dy);
          return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
        });
    
        circles.selectAll("circle").attr("transform", function(d) {
          return "translate(" + d.x + "," + d.y + ")";
        });
    
        texts.selectAll("text").attr("transform", function(d) {
          return "translate(" + d.x + "," + d.y + ")";
        });
      }
    }
    
    angular.module("app", [])
      .controller("ctrl", MyController);
    
    var viewModelHelper = {
      apiGet: function(a, b, then) {
        setTimeout(
          function() {
            console.log("Response from server");
            then(viewModelHelper.links);
          },
          2000
        );
      },
      links: [{
          source: "Microsoft",
          target: "Amazon",
          type: "licensing"
        },
        {
          source: "Microsoft",
          target: "HTC",
          type: "licensing"
        },
        {
          source: "Samsung",
          target: "Apple",
          type: "suit"
        },
        {
          source: "Motorola",
          target: "Apple",
          type: "suit"
        },
        {
          source: "Nokia",
          target: "Apple",
          type: "resolved"
        },
        {
          source: "HTC",
          target: "Apple",
          type: "suit"
        },
        {
          source: "Kodak",
          target: "Apple",
          type: "suit"
        },
        {
          source: "Microsoft",
          target: "Barnes & Noble",
          type: "suit"
        },
        {
          source: "Microsoft",
          target: "Foxconn",
          type: "suit"
        },
        {
          source: "Oracle",
          target: "Google",
          type: "suit"
        },
        {
          source: "Apple",
          target: "HTC",
          type: "suit"
        },
        {
          source: "Microsoft",
          target: "Inventec",
          type: "suit"
        },
        {
          source: "Samsung",
          target: "Kodak",
          type: "resolved"
        },
        {
          source: "LG",
          target: "Kodak",
          type: "resolved"
        },
        {
          source: "RIM",
          target: "Kodak",
          type: "suit"
        },
        {
          source: "Sony",
          target: "LG",
          type: "suit"
        },
        {
          source: "Kodak",
          target: "LG",
          type: "resolved"
        },
        {
          source: "Apple",
          target: "Nokia",
          type: "resolved"
        },
        {
          source: "Qualcomm",
          target: "Nokia",
          type: "resolved"
        },
        {
          source: "Apple",
          target: "Motorola",
          type: "suit"
        },
        {
          source: "Microsoft",
          target: "Motorola",
          type: "suit"
        },
        {
          source: "Motorola",
          target: "Microsoft",
          type: "suit"
        },
        {
          source: "Huawei",
          target: "ZTE",
          type: "suit"
        },
        {
          source: "Ericsson",
          target: "ZTE",
          type: "suit"
        },
        {
          source: "Kodak",
          target: "Samsung",
          type: "resolved"
        },
        {
          source: "Apple",
          target: "Samsung",
          type: "suit"
        },
        {
          source: "Kodak",
          target: "RIM",
          type: "suit"
        },
        {
          source: "Nokia",
          target: "Qualcomm",
          type: "suit"
        },
        {
          source: "Pippo",
          target: "Pippo"
        },
        {
          source: "Paperino",
          target: "Pippo",
          type: "suit"
        }
      ],
    };
    path {
      fill: none;
      stroke: black;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>
    <div ng-app="app" ng-controller="ctrl">
    </div>

    【讨论】:

    • 首先,感谢您的宝贵时间,我试过了,它工作正常。你介意我问一些澄清,以便我更好地了解这里发生的事情吗? 1 - 关于 var viewModelHelper :为什么它在控制器之外定义?为什么要在这里声明数组?如果我必须使用 viewModelHelper 编写更多函数,它们会延迟 2 秒吗? 2 - 我不知道您是否是 d3/svg 方面的专家,但您知道有关如何实现图表上的右键菜单等功能的任何资源吗?
    • 这只是一个用于模拟 API 调用的虚拟函数。当然,我没有真正的服务器,所以我所做的只是让数据对象在 2000 毫秒后才可用。这样,图表就已经绘制好了,所以您应该能够看到它在获取新数据后更新
    • d3 具有出色的事件处理程序。我从来没有专门用右键单击来使用它们,但它支持单击、悬停、触摸、按键等等。有关文档,请参阅here
    • 你的意思是,如果我进入 ApiController 并在其中放入一个 ThreadSleep(2000),我可以将那个虚拟函数移除到控制器之外?
    • 不,如果您将其替换为您拥有的常规 API 调用,则可以删除虚拟函数。无需在任何地方放置threadsleep,只是为了演示目的
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-10-10
    • 1970-01-01
    • 1970-01-01
    • 2019-04-28
    • 2018-02-19
    • 2021-05-11
    • 2019-12-31
    相关资源
    最近更新 更多