我正在尝试对数据库中的某些数据进行网络图绘制,我正在前端使用AngularJS(后端使用C#)。
我找到了d3.js,它看起来很完美,但我根本不是JavaScript专家。 我以他为例https://observablehq.com/@d3/mobile-patent-suits?collection=@d3/d3-force (实际上,它必须是以前的版本,或者是有人从Github分叉的东西,我再也找不到了,但这仍然是核心)。
我的问题是,我必须从调用API的函数var links
加载JSON self.GetLinks
数组,并返回相同类型对象的JSON。
该函数在onInit
中被调用,但是到被调用时,所有内容都已经呈现在屏幕上了,self.GetLinks
的唯一结果就是改变了var links
内部的内容迟了。
为清楚起见,这里显示的数组var links
是我从示例中获取的数组,对我来说没有用,但结构相同。
我需要了解在发生任何事情之前如何调用self.GetLinks
,以便var links
的内容是我实际上从后端获得的内容,或者甚至更好地如何使它{ var links
的内容发生变化,将再次呈现图形(通过这种方式,我可以向其中动态添加元素)。
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;
}
}
答案 0 :(得分:0)
您需要做两件事。首先将逻辑分为“第一渲染”和“更新”部分。在第一个渲染部分中,您仅假设要在空白的SVG上绘制。在更新部分,您将使用数据更新已更改的内容。
在此示例中,第一个渲染部分可用于绘制svg
和defs
。然后更新部分可以处理在正确位置绘制节点和链接的情况。
您需要做的第二件事是查看触发功能。可以在您现在使用的回调中或通过Promise完成(有关此信息,请参见AngularJS的$q
)。
最后,在函数或构造函数中进行所有这些初始化工作是不好的做法。我已将您的代码分为firstRender
和update
部分,从firstRender
调用$onInit
,并模拟了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>