I have a working code example (only the <script type="text/javascript"> part) of a static graph using d3.js as below:
/* Create graph data */
var nodes = [];
for (var i = 0; i < 13; i++)
{
var datum = {
"value": i
};
nodes.push(datum);
}
var links = [{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0},
{"source": 1, "target": 3},
{"source": 3, "target": 2},
{"source": 3, "target": 4},
{"source": 4, "target": 5},
{"source": 5, "target": 6},
{"source": 5, "target": 7},
{"source": 6, "target": 7},
{"source": 6, "target": 8},
{"source": 7, "target": 8},
{"source": 9, "target": 4},
{"source": 9, "target": 11},
{"source": 9, "target": 10},
{"source": 10, "target": 11},
{"source": 11, "target": 12},
{"source": 12, "target": 10}];
/* Create force graph */
var w = 800;
var h = 500;
var size = nodes.length;
nodes.forEach(function(d, i) { d.x = d.y = w / size * i});
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("weight", h);
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.linkDistance(200)
.size([w, h]);
setTimeout(function() {
var n = 400
force.start();
for (var i = n * n; i > 0; --i) force.tick();
force.stop();
svg.selectAll("line")
.data(links)
.enter().append("line")
.attr("class", "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; });
svg.append("svg:g")
.selectAll("circle")
.data(nodes)
.enter().append("svg:circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 15);
svg.append("svg:g")
.selectAll("text")
.data(nodes)
.enter().append("svg:text")
.attr("class", "label")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.attr("text-anchor", "middle")
.attr("y", ".3em")
.text(function(d) { return d.value; });
}, 10);
and it produces this rather scrambled layout:
While it is technically the correct graph, the ideal layout should be something like this (ignoring the different visual graphics):
Note that the layout should be fixed so that reloading the page does not change the positioning of each node; the layout should also be static, in that there is no animation effect and the nodes are not draggable. Both requirements are already achieved in the script above.
So how should I further configure this d3 script to produce a layout shown in the second image?
First, increase the charge strength and reduce the link distance. Doing so places a greater emphasis on global structure rather than local connections. Also, if you increase the charge strength enough, the repulsive charge will push even directly-connected nodes farther apart, thus effectively increasing the link distance while giving better overall structure. (The downside of a stronger charge force is that graph initialization is more chaotic, but this shouldn’t be a problem for static layouts.)
Second, you may need to increase the number of iterations or add custom forces to get better results. Force layouts often work well on arbitrary graphs, but there’s no guarantee that they will produce an optimal (or even good) result. For any graph where you can make simplifying assumptions (for example, trees), there may be additional forces or constraints that you can apply to encourage the simulation to converge onto a better solution.
Related
Suppose I have an array of values with corresponding dates, [{date: d1, value: v1}, ..., {date: dn, value: vn}], that I'd like to visualize using d3.js. As long as subsequent measurements are within a certain time range, for example not more than a week apart, I am happy with d3 interpolating between the measurements.
However, when subsequent records are farther apart, I don't want d3 to connect them. What would be the easiest way to achieve this?
Your question is not exactly clear: by "interpolate", I believe you mean "connecting the dots".
If you use a time scale for your x axis, D3 will automatically connect the dots for you and create a line (an SVG path element), regardless the time separation in the data points. But there is a way for making "gaps" in that line: using line.defined(). According to the API:
If defined is specified, sets the defined accessor to the specified function or boolean and returns this line generator.
The problem is, for this approach to work, you'll have to set a given value (let's say, null) in your dataset, between the dates in which you don't want to draw the line (that is, the dates that are not close enough, as you say in your question). You can do this manually or using a function.
This is a working demo: in my dataset, my data jumps from 7-Oct to 17-Oct (more than 1 week). So, I just created a null value in any date between these two (in my demo, 16-Oct). Then, in the line generator, the line jumps this null value, using defined:
d3.line().defined(function(d){return d.value != null;})
The result is that the line jumps from 7-Oct to 17-Oct:
var data = [{date: "1-Oct-16",value: 14},
{date: "2-Oct-16",value: 33},
{date: "3-Oct-16",value: 12},
{date: "4-Oct-16",value: 43},
{date: "5-Oct-16",value: 54},
{date: "6-Oct-16",value: 71},
{date: "7-Oct-16",value: 32},
{date: "16-Oct-16",value: null},
{date: "17-Oct-16",value: 54},
{date: "18-Oct-16",value: 14},
{date: "19-Oct-16",value: 34},
{date: "20-Oct-16",value: 32},
{date: "21-Oct-16",value: 56},
{date: "22-Oct-16",value: 24},
{date: "23-Oct-16",value: 42},
{date: "24-Oct-16",value: 52},
{date: "25-Oct-16",value: 66},
{date: "26-Oct-16",value: 34},
{date: "27-Oct-16",value: 62},
{date: "28-Oct-16",value: 48},
{date: "29-Oct-16",value: 51},
{date: "30-Oct-16",value: 42}];
var parseDate = d3.timeParse("%d-%b-%y");
data.forEach(function (d) {
d.date = parseDate(d.date);
});
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var xScale = d3.scaleTime().range([20, 480]);
var yScale = d3.scaleLinear().range([180, 10]);
xScale.domain(d3.extent(data, function (d) {
return d.date;
}));
yScale.domain([0, d3.max(data, function (d) {
return d.value;
})]);
var xAxis = d3.axisBottom(xScale).tickFormat(d3.timeFormat("%d"));
var yAxis = d3.axisLeft(yScale);
var baseline = d3.line()
.defined(function(d){return d.value != null;})
.x(function (d) {
return xScale(d.date);
})
.y(function (d) {
return yScale(d.value);
});
svg.append("path") // Add the valueline path.
.attr("d", baseline(data))
.attr("fill", "none")
.attr("stroke", "teal");
svg.append("g")
.attr("transform", "translate(0,180)")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(20,0)")
.attr("class", "y axis")
.call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
If you're trying to draw a broken line chart, then I don't think interpolation is what you're looking for. Building a custom SVG path is probably the way to go. See an example here:
https://bl.ocks.org/joeldouglass/1a0b2e855d2bedc24c63e396b04c8e36
I'm trying to implement the Sankey plugin with d3. I'm not getting the output I expected though. Hopefully someone can help.
I've reduced down to simplest possible dataset.
There are 3 nodes. I would expect one link from Entry to Exit, and one going from Entry to Zone1, then Exit. Here is the set:
const graph = {
nodes: [
{'node': 0, 'name': 'Entry'},
{'node': 1, 'name': 'Zone 1'},
{'node': 2, 'name': 'Exit'}
],
links: [
{"source": 0, "target": 1, "value": 2},
{"source": 1, "target": 2, "value": 2},
{"source": 0, "target": 2, "value": 4}
]
}
However I'm just getting one link pathway, and also it looks kind of mashed up
Here is the link code:
var link = svg.append("g").selectAll(".link")
.data(graph.links)
.enter().append("path")
.attr("class", "link")
.attr("d", path)
.style("stroke-width", function (d) {
return Math.max(1, d.dy);
})
.sort(function (a, b) {
return b.dy - a.dy;
});
link.append("title")
.text(function (d) {
return d.source.name + " → " +
d.target.name + "\n" + format(d.value);
});
There was no explicit stroke color in the link code. I just needed to add:
.attr("stroke", "#CDCDCD")
Say I have an Array of these objects:
[
{name: "P1", Subj1: 9.7, Subj2: 10, Subj3: 9.5, Subj4: 8.2, Subj5:9.3, Subj6: 8.9},
{name: "P2", Subj1: 9.7, Subj2: 10, Subj3: 9.5, Subj4: 8.2, Subj5:9.3, Subj6: 8.9},
{name: "P3", Subj1: 9.7, Subj2: 10, Subj3: 9.5, Subj4: 8.2, Subj5:9.3, Subj6: 8.9} ]
I would like to plot a bar chart for one person. I currently have this code:
d3.select("#chart").selectAll("div.h-bar")
.data(getObj(name))
.enter()
.append("div")
.attr("class", "h-bar")
.append("span");
//delete elements not associated with data element
d3.select("#chart").selectAll("div.h-bar")
.data(getObj(name))
.exit().remove();
d3.select("#chart").selectAll("div.h-bar")
.data(getObj(name))
.attr("class", "h-bar")
.style("width", function (d) {
return ( d.Subj1* 10) + "px";
}
)
.select("span")
.text(function (d) {
return d.name;
});
with the getObj(name) function:
function getObj(Name){
for(var i = 0; i <studentList.length; i++){
if(studentList[i].Naam == Name){
console.log(studentList[i]);
return [studentList[i]];
}
}
}
I cant figure out how to get a bar chart for person 1 where every bar has a width of achieved note, and the span text the name of the subject.
Any hints?
I'm new to d3 and haven't much web frontend development experience. For a web application I have I'm trying to draw a force directed graph. I've been trying the last few hours to get it to work. I've been looking at lots of different code example and what I'm doing looks very similar. I eventually got nodes to draw but the links between the nodes don't show up and I was trying different things and nothing seems to work. I don't know why my code wouldn't draw the edges.
From printing the nodes and links to the console I saw that the nodes got extra attributes like the d3 docs had mentioned but the links never seem to get these attributes. Below is my javascript file and the JSON file. I reduced the JSON file to only 3 entries to try and make it easier to solve the problem.
var height = 1080;
var width = 1920;
var color = d3.scale.category20();
var force = d3.layout.force()
.linkDistance(-120)
.linkStrength(30)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("/static/javascript/language_data.json", function(data){
force
.nodes(data.languages)
.links(data.language_pairs)
.start();
var link = svg.selectAll(".link")
.data(data.language_pairs)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(data.languages)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);
node.append("title")
.text(function(d) { return d.language; });
force.on("tick", function() {
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; });
});
});
Here is the JSON file:
From looking at few examples my understanding is that the source and target are index positions from the list of nodes.
{
"languages":[
{"language": "TypeScript", "group": 1},
{"language": "Java", "group": 2},
{"language": "VHDL", "group": 3}
],
"language_pairs":[
{"source": "0", "target": "1", "value": 5},
{"source": "1", "target": "2", "value": 5},
{"source": "2", "target": "0", "value": 5}
]
}
Sorry if I left out anything! Thanks for any help!
Two issues:
1.) Your "language_pairs" source/target indexes are strings and not numbers. Get rid of the quotes:
"language_pairs":[
{"source": 0, "target": 1, "value": 5},
{"source": 1, "target": 2, "value": 5},
{"source": 2, "target": 0, "value": 5}
]
2.) Your linkDistance and linkStrength parameters don't make sense:
var force = d3.layout.force()
.linkDistance(-120) // negative distance?
.linkStrength(30) // according to the docs, this must be between 0 and 1?
.size([width, height]);
Here's an example that fixes these problems.
I have the following array
var data = [
[{"time": 1, "value": 2.1}, {"time": 2, "value": 1.1}],{"time": 3, "value": 3.1}],
[{"time": 1, "value": 5.3}, {"time": 2, "value": 0.1}, {"time": 3, "value": 6.1}]
];
and I need to find the maximum time and value out of the entire array. the code that doesn't quite work is
var x = d3.scale.linear()
.domain([1, d3.max(data, function(d,i){ return d[i].time;})])
.range([0, width]);
for some reason I get a maximum time of 2, and not 3. even if I use a larger dataset with more point I still don't get the actual maximum value.
Any help is appreciated.
Your data is an array or arrays. If you want the "max of the maxes", you'll need to account for the nesting. One way to do it:
.domain([1, d3.max(data, function(arrayOfDs, i) {
return d3.max(arrayOfDs, function(d, i) { return d.time; });
})])