我目前正在建立D3.js研究论文之间关系的图表。目前我的代码允许我生成一个力导向图。我可以缩放和拖动图表,暂时不会出现这样的问题"丑陋的"工具提示在"鼠标悬停"上显示节点信息(但这与这个问题无关。)
我正在寻找基于发布年度可视化文章网络的最佳方式。我认为最好的方法是按同心圆模式按年显示节点,如下所示:
Simple representation of the expected result of a concentric circle force-directed graph
在我的代码中的图像中,节点根据年份进行着色。
以下是我的插件链接:http://plnkr.co/edit/RCzGe0OFaQNnI32kBuSn?p=preview
这是我的代码: HTML:
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v4.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="script.js"></script>
</body>
</html>
的style.css:
/* Styles go here */
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
div.tooltip {
position: absolute;
text-align: center;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
测试data.JSON:
{
"papers":[
{
"id":"1",
"title":"Title 1",
"year":"2016",
"authors":["A1","A2"],
"problematic":"",
"solution":"",
"references":["2","3"]
},
{
"id":"2",
"title":"Title 2",
"year":"2015",
"authors":["A2","A3"],
"problematic":"",
"solution":"",
"references":["4","5"]
},
{
"id":"3",
"title":"Title 3",
"year":"2015",
"authors":["A4","A5"],
"problematic":"",
"solution":"",
"references":["4"]
},
{
"id":"4",
"title":"Title 4",
"year":"2014",
"authors":["A1","A3"],
"problematic":"",
"solution":"",
"references":[]
},
{
"id":"5",
"title":"Title 5",
"year":"2013",
"authors":["A6","A7"],
"problematic":"",
"solution":"",
"references":[]
}
]
}
的script.js:
/* ------ DESCRIPTION ------
Properties of the graph:
BASIC:
✓ Graph represents all papers and relationships in RTB research
✓ Graph is force dynamic
✓ Nodes are coloured by publishing year
✓ Graph is draggable
✓ Graph is zoomable
X Graph is "tree like" where the nodes are "ordered" by publishing year, the oldest being at the bottom
~ Hovering over a Node will display it's info
- Clicking a node will allow to visualize it's direct or most important connections
ADVANCED:
- Display papers graph
- Display authors graph
- Search for paper based on info: id, title, author, year, ...
- Add new paper to graph and modify and save JSON file
- Open PDF File in new Tab
*/
// ----- GLOBAL VARIABLES ------
var w = window.innerWidth;
var h = window.innerHeight;
var svg = d3.select("body").append("svg")
.attr("width",w)
.attr("height",h)
.style("cursor","move");
var g = svg.append("g");
// NODE COLORS
var color = d3.scaleOrdinal(d3.schemeCategory20);
// FORCE SIMULATION
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody().strength(-100))
.force("center", d3.forceCenter(w / 2, h / 2))
.force("collide", d3.forceCollide(10));
// ZOOM PARAMETERS
var min_zoom = 0.1;
var max_zoom = 7;
var zoom = d3.zoom()
.scaleExtent([min_zoom,max_zoom])
.on("zoom", zoomed);
svg.call(zoom);
var transform = d3.zoomIdentity
.translate(w / 6, h / 6)
.scale(0.5);
svg.call(zoom.transform, transform);
// BASIC NODE SIZE
var nominal_stroke = 1.5;
var nominal_node_size = 8;
// ----- GLOBAL FUNCTIONS -----
function dragStart(d){
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragging(d){
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragEnd(d){
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function zoomed() {
g.attr("transform", d3.event.transform);
// Manually offsets the zoom to compensate for the initial position. Should get fixed asap or the position variables made global.
//svg.attr("transform", "translate(" + (d3.event.transform.x + 400) + "," + (d3.event.transform.y + 325) + ")scale(" + d3.event.transform.k + ")");
}
function isInList(el, list){
for (var i = 0; i < list.length; i++){
if (el == list[i]) return true;
}
return false;
}
// builds a graph dictionary based on paper references
function referencesGraph(file_data){
var nodes = [];
var links = [];
// we use these to add nodes to references that are missing as nodes
var node_ids = [];
var ref_ids = [];
// for each paper in graph create a node and append result to node list
for (var i = 0; i < file_data.length; i++ ){
var node = {
"id":file_data[i].id,
"title":file_data[i].title,
"year":file_data[i].year,
"authors":file_data[i].authors
};
node_ids.push(file_data[i].id);
nodes.push(node);
// for each referenced paper in graph create a link and append result to link list
for (var j = 0; j < file_data[i].references.length; j++){
var link = {
"source":file_data[i].id,
"target":file_data[i].references[j]
};
ref_ids.push(file_data[i].references[j]);
links.push(link);
}
}
//check if all referenced elements have a node associated
for (var i = 0; i < ref_ids.length; i++){
if (!isInList(ref_ids[i],node_ids)){
var node = {
"id":ref_ids[i],
"title":ref_ids[i],
"year":""
}
nodes.push(node);
}
}
var graph = {
"nodes":nodes,
"links":links
};
return graph;
}
// builds a graph dictionary based on author collaboration
function authorsGraph(data){
}
// DEAL WITH MISSING DATA TO BE WORKED
// ----- MANAGE JSON DATA -----
d3.json("test-data.json",function(error,graph){
if (error) throw error;
// Read the JSON data and create a dictionary of nodes and links based on references
var paper_graph_data = referencesGraph(graph.papers);
//var authors_graph_data; //function not implemented yet
// INITIALIZE THE LINKS
var link = g.append("g")
.attr("class","links")
.selectAll("line")
.data(paper_graph_data.links)
.enter()
.append("line")
.attr("stroke-width",function(d){return nominal_stroke})
/* FUNCTION THAT CREATES DIV ELEMENT TO HOLD NODE INFORMATION
[ PAPER TITLE ]
[ PUBLISHING YEAR ][ PERSONAL RATING ]
[ AUTHORS & LINKS ]
[ PROBLEMATIC ]
[ SOLUTION ]
[OPEN PDF FILE]
*/
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
function createTooltip(d){
//get node data, manage missing values
div.transition()
.duration(200)
.style("opacity", .9);
div.html("<table><tr><td>" + d.title + "</td></tr><tr><td>" + d.year + "</td></tr><tr><td>" + d.authors + "</td></tr><tr><td>" + d.problematic + "</td></tr><tr><td>" + d. solution + "</td></tr></table>")
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
}
// INITIALIZE THE NODES
var node = g.append("g")
.attr("class","nodes")
.selectAll("circles")
.data(paper_graph_data.nodes)
.enter()
.append("circle")
.attr("r",nominal_node_size)
.attr("fill",function(d){return color(d.year);})
.style("cursor","pointer")
.on("mouseover",createTooltip)
.on("mouseout",function(d){
div.transition()
.duration(500)
.style("opacity", 0);
})
.call(d3.drag()
.on("start", dragStart)
.on("drag", dragging)
.on("end", dragEnd));
simulation.nodes(paper_graph_data.nodes)
.on("tick",ticked);
simulation.force("link")
.links(paper_graph_data.links);
// function to return link and node position when simulation is generated
function ticked(){
// Each year is placed on a different level to get chronological order of paper network
/*
switch(d.source.year){
case "2016":
return 40;
case "2015":
return 80;
case "2014":
return 120;
case "2013":
return 160;
case "2012":
return 200;
case "2011":
return 240;
case "2010":
return 280;
case "2009":
return 320;
case "2008":
return 360;
case "2007":
return 400;
default:
return 600;
}
*/
link
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function ticked_advanced(){
link
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) {
switch(d.source.year){
case "2016":
return 40;
case "2015":
return 80;
case "2014":
return 120;
case "2013":
return 160;
case "2012":
return 200;
case "2011":
return 240;
case "2010":
return 280;
case "2009":
return 320;
case "2008":
return 360;
case "2007":
return 400;
default:
return 600;
}
})
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) {
switch(d.target.year){
case "2016":
return 40;
case "2015":
return 80;
case "2014":
return 120;
case "2013":
return 160;
case "2012":
return 200;
case "2011":
return 240;
case "2010":
return 280;
case "2009":
return 320;
case "2008":
return 360;
case "2007":
return 400;
default:
return 600;
}
});
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) {
switch(d.year){
case "2016":
return 40;
case "2015":
return 80;
case "2014":
return 120;
case "2013":
return 160;
case "2012":
return 200;
case "2011":
return 240;
case "2010":
return 280;
case "2009":
return 320;
case "2008":
return 360;
case "2007":
return 400;
default:
return 600;
}
});
}
});
我想我必须修改tick函数,以便在每个&#34; year zone&#34;中返回x和y随机坐标。但不知道如何计算。
关于如何做到这一点的任何想法?非常感谢。
注意:
我发现这个答案是在一个环中生成一个随机数,也就是指在一个圆中统一生成一个随机数:
答案 0 :(得分:1)
我认为有几种方法可以做到这一点,
如下所示的一种方法是约束节点可以移动到的可能位置。我创建了一个logger.log(<LOG_LEVEL>, <LOG_MESSAGE>, ex);
函数,它接受一个节点并更新其x / y以适应由数据集中的年数定义的圆形区域。每次更新节点位置时,只需调用约束函数,它们将保持在其定义的区域内。这样做的一个缺点是边缘力会将它们拉向边界。
constrain(d)
&#13;
var graph = {
"papers": [{
"id": "1",
"title": "Title 1",
"year": "2016",
"authors": ["A1", "A2"],
"problematic": "",
"solution": "",
"references": ["2", "3"]
}, {
"id": "2",
"title": "Title 2",
"year": "2015",
"authors": ["A2", "A3"],
"problematic": "",
"solution": "",
"references": ["4", "5"]
}, {
"id": "3",
"title": "Title 3",
"year": "2015",
"authors": ["A4", "A5"],
"problematic": "",
"solution": "",
"references": ["4"]
}, {
"id": "4",
"title": "Title 4",
"year": "2014",
"authors": ["A1", "A3"],
"problematic": "",
"solution": "",
"references": []
}, {
"id": "5",
"title": "Title 5",
"year": "2013",
"authors": ["A6", "A7"],
"problematic": "",
"solution": "",
"references": []
}]
};
var w = window.innerWidth;
var h = window.innerHeight;
var maxRadStep = 100;
var cX = w / 2,
cY = h / 2;
var years = d3.set(graph.papers.map(function(obj) {
return +obj.year;
})).values();
years.sort();
function constrain(d) {
var yearIndex = years.indexOf(d.year);
var max = (maxRadStep * (yearIndex + 1)) - 10;
var min = (max - maxRadStep) + 20;
var vX = d.x - cX;
var vY = d.y - cY;
var magV = Math.sqrt(vX * vX + vY * vY);
if (magV > max) {
d.vx = 0;
d.vy = 0;
d.x = cX + vX / magV * max;
d.y = cY + vY / magV * max;
} else if (magV < min) {
d.vx = 0;
d.vy = 0;
d.x = cX + vX / magV * min;
d.y = cY + vY / magV * min;
}
}
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.style("cursor", "move");
var g = svg.append("g");
// NODE COLORS
var color = d3.scaleOrdinal(d3.schemeCategory20);
// FORCE SIMULATION
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody().strength(-100))
//.force("center", d3.forceCenter(w / 2, h / 2))
.force("collide", d3.forceCollide(10));
// ZOOM PARAMETERS
var min_zoom = 0.1;
var max_zoom = 7;
var zoom = d3.zoom()
.scaleExtent([min_zoom, max_zoom])
.on("zoom", zoomed);
svg.call(zoom);
var transform = d3.zoomIdentity
.translate(w / 6, h / 6)
.scale(0.5);
svg.call(zoom.transform, transform);
// BASIC NODE SIZE
var nominal_stroke = 1.5;
var nominal_node_size = 8;
// ----- GLOBAL FUNCTIONS -----
function dragStart(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragging(d) {
console.log(d3.event.x + ' ' + d3.event.y);
d.fx = d3.event.x;
d.fy = d3.event.y;
constrain(d);
}
function dragEnd(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function zoomed() {
g.attr("transform", d3.event.transform);
// Manually offsets the zoom to compensate for the initial position. Should get fixed asap or the position variables made global.
//svg.attr("transform", "translate(" + (d3.event.transform.x + 400) + "," + (d3.event.transform.y + 325) + ")scale(" + d3.event.transform.k + ")");
}
function isInList(el, list) {
for (var i = 0; i < list.length; i++) {
if (el == list[i]) return true;
}
return false;
}
// builds a graph dictionary based on paper references
function referencesGraph(file_data) {
var nodes = [];
var links = [];
// we use these to add nodes to references that are missing as nodes
var node_ids = [];
var ref_ids = [];
// for each paper in graph create a node and append result to node list
for (var i = 0; i < file_data.length; i++) {
var node = {
"id": file_data[i].id,
"title": file_data[i].title,
"year": file_data[i].year,
"authors": file_data[i].authors
};
node_ids.push(file_data[i].id);
nodes.push(node);
// for each referenced paper in graph create a link and append result to link list
for (var j = 0; j < file_data[i].references.length; j++) {
var link = {
"source": file_data[i].id,
"target": file_data[i].references[j]
};
ref_ids.push(file_data[i].references[j]);
links.push(link);
}
}
//check if all referenced elements have a node associated
for (var i = 0; i < ref_ids.length; i++) {
if (!isInList(ref_ids[i], node_ids)) {
var node = {
"id": ref_ids[i],
"title": ref_ids[i],
"year": ""
}
nodes.push(node);
}
}
var graph = {
"nodes": nodes,
"links": links
};
return graph;
}
// builds a graph dictionary based on author collaboration
function authorsGraph(data) {
}
// DEAL WITH MISSING DATA TO BE WORKED
// ----- MANAGE JSON DATA -----
// Read the JSON data and create a dictionary of nodes and links based on references
var paper_graph_data = referencesGraph(graph.papers);
//var authors_graph_data; //function not implemented yet
// INITIALIZE THE LINKS
var link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(paper_graph_data.links)
.enter()
.append("line")
.attr("stroke-width", function(d) {
return nominal_stroke
})
// INITIALIZE THE NODES
var node = g.append("g")
.attr("class", "nodes")
.selectAll("circles")
.data(paper_graph_data.nodes)
.enter()
.append("circle")
.attr("r", nominal_node_size)
.attr("fill", function(d) {
return color(d.year);
})
.style("cursor", "pointer")
.call(d3.drag()
.on("start", dragStart)
.on("drag", dragging)
.on("end", dragEnd));
g.append('g')
.attr('class', 'boundry')
.selectAll('.boundry')
.data(years)
.enter()
.append('circle')
.attr('r', function(d, index) {
return (index + 1) * maxRadStep;
}).attr('cx', cX).attr('cy', cY);
simulation.nodes(paper_graph_data.nodes)
.on("tick", ticked);
simulation.force("link")
.links(paper_graph_data.links);
function ticked() {
node.each(constrain);
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
link
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
}
&#13;
/* Styles go here */
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
.boundry circle {
stroke: #000;
fill: none;
}
&#13;