Related
I've recently begun learning D3.js and I am struggling to create a transition in a scatter plot with the following data:
var data = [
{"year" : "2004", "x":100, "y":300, "size": 2, "type": "A"},
{"year" : "2005", "x":200, "y":200, "size": 2, "type": "A"},
{"year" : "2006", "x":300, "y":100, "size": 2, "type": "A"},
{"year" : "2004", "x":150, "y":250, "size": 2.382450, "type": "B"},
{"year" : "2005", "x":150, "y":250, "size": 3.078548, "type": "B"},
{"year" : "2006", "x":150, "y":250, "size": 4.265410, "type": "B"}];
Where in the scatter plot there are 2 points (type A&B) and they change location (x&y) and size by year. I've created a fiddle where I try to nest the data and plot the points, but making the next step of using transition() function is confusing. More specifically, I am still declaring the whole data, but to make transitions work I only need part of the data.
The key to understand what you want lies here:
There are 2 points and they change location (x&y) and size by year
Therefore, this is clearly a XY problem. Your problem is not "how to transition with nested data". Your problem is "how to transition by year".
My proposed solution involves, first of all, dropping that nested array. You don't need that.
Instead, get all the years in the data...
var years = [...new Set(data.map(function(d) {
return d.year
}))];
..., filter the data by year...
var dataStart = data.filter(function(d) {
return d.year === years[0]
});
... and loop trough the years. Here, I'm using d3.interval():
var counter = 1;
var timer = d3.interval(transition, 1500);
function transition() {
var newData = data.filter(function(d) {
return d.year === years[counter]
});
svg.selectAll("circle").data(newData)
.transition()
.duration(1000)
.attr("cx", function(d) {
console.log(d)
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.size * 10;
});
counter += 1;
if (counter === 3) {
timer.stop()
}
}
Here is the demo:
var data = [{
"year": "2004",
"x": 100,
"y": 100,
"size": 2,
"type": "A"
}, {
"year": "2005",
"x": 200,
"y": 180,
"size": 2,
"type": "A"
}, {
"year": "2006",
"x": 300,
"y": 50,
"size": 2,
"type": "A"
}, {
"year": "2004",
"x": 150,
"y": 150,
"size": 2.382450,
"type": "B"
}, {
"year": "2005",
"x": 150,
"y": 50,
"size": 3.078548,
"type": "B"
}, {
"year": "2006",
"x": 150,
"y": 100,
"size": 4.265410,
"type": "B"
}];
var width = 400,
height = 200;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var years = [...new Set(data.map(function(d) {
return d.year
}))];
var dataStart = data.filter(function(d) {
return d.year === years[0]
});
var cell = svg.selectAll("circle")
.data(dataStart);
cell.enter()
.append("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.size * 10;
});
var counter = 1;
var timer = d3.interval(transition, 1500);
function transition() {
var newData = data.filter(function(d) {
return d.year === years[counter]
});
svg.selectAll("circle").data(newData)
.transition()
.duration(1000)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.size * 10;
});
counter += 1;
if (counter === 3) {
timer.stop()
}
}
<script src="https://d3js.org/d3.v4.min.js"></script>
Below code generates a force directed graph but there are couple of problems.
Like how do I control the opening animation speed
How do I change the drag speed
And major problem every time I try to drag some element it reloads automatically.
I not sure what am I doing wrong.
var width = $(window).width(),
height = 700;
var force = d3.layout.force()
.size([width, height])
.on("tick", tick2);
var svg = d3.select("body .banner").append("svg")
.attr("width", width)
.attr("height", height);
//.on("click", explicitlyPosition);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
function tick2() {
link
.attr("x1", function (d) {
return width * 0.5;
})
.attr("y1", function (d) {
return height * 0.5;
})
.attr("x2", function (d) {
return width * 0.5;
})
.attr("y2", function (d) {
return height * 0.5;
});
d3.selectAll("circle")
.attr("cx", function (d) {
return width * 0.5;
})
.attr("cy", function (d) {
return height * 0.5;
});
d3.selectAll("text")
.attr("x", function (d) {
return width * 0.5;
})
.attr("y", function (d) {
return height * 0.5;
});
tick();
}
function tick() {
link.transition()
.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;
});
d3.selectAll("circle").transition()
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
d3.selectAll("text").transition()
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
});
}
var graph = {
"nodes": [
{"name": "You", "val": "You", "x": width * 0.50, "y": height * 0.5, "fixed": false},
{"name": "SaaS", "val": 768, "x": width * 0.40, "y": height * 0.14, "fixed": true},
{"name": "Education", "val": 1021, "x": width * 0.65, "y": height * 0.10, "fixed": true},
{"name": "E-Commerce", "val": 1345, "x": width * 0.75, "y": height * 0.35, "fixed": true},
{"name": "Food Tech", "val": 512, "x": width * 0.70, "y": height * 0.72, "fixed": true},
{"name": "Healthcare", "val": 246, "x": width * 0.57, "y": height * 0.70, "fixed": true},
{"name": "Fashion Industry", "val": 657, "x": width * 0.30, "y": height * 0.80, "fixed": true},
{"name": "Hardware", "val": 145, "x": width * 0.30, "y": height * 0.65, "fixed": true},
{"name": "Fintech", "val": 1160, "x": width * 0.25, "y": height * 0.18, "fixed": true},
{"name": "Series A", "val": 392, "x": width * 0.85, "y": height * 0.13, "fixed": true},
{"name": "Series B", "val": 873, "x": width * 0.80, "y": height * 0.60, "fixed": true},
{"name": "2014", "val": 592, "x": width * 0.125, "y": height * 0.25, "fixed": true},
{"name": "2015", "val": 630, "x": width * 0.19, "y": height * 0.45, "fixed": true}
],
"links": [
{"source": 0, "target": 1},
{"source": 0, "target": 2},
{"source": 0, "target": 3},
{"source": 3, "target": 9},
{"source": 3, "target": 10},
{"source": 0, "target": 4},
{"source": 0, "target": 5},
{"source": 0, "target": 6},
{"source": 0, "target": 7},
{"source": 0, "target": 8},
{"source": 8, "target": 11},
{"source": 8, "target": 12}
]
};
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("g")
.call(force.drag);
node.append("circle")
.attr("class", "node")
.attr("r", function (d) {
you_val = (d.val === "You") ? 1500 : d.val;
return ((you_val) / 30) < 15 ? 15 : ((you_val) / 30);
});
node.append("text")
.attr("x", 0)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("fill", "#9a9a9a")
.attr("font-size", "12px")
.attr("font-weight", "600")
.text(function (d) {
return d.val;
});
node.append("text")
.attr("x", 0)
.attr("dy", function (d) {
you_val = (d.val === "You") ? 1500 : d.val;
var rad = ((you_val) / 30) < 15 ? 15 : ((you_val) / 30);
return (rad + 15) + "px";
})
.attr("text-anchor", "middle")
.attr("fill", "#9a9a9a")
.attr("font-size", "12px")
.text(function (d) {
return d.name;
});
force
.nodes(graph.nodes)
.links(graph.links)
.start();
I don't understand why you have two tick functions.
How do I change the drag speed
And major problem every time I try to drag some element it reloads automatically.
Just have a single tick function like this:
function tick2() {
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;
});
d3.selectAll("circle")
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
d3.selectAll("text")
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
});
}
In your case you have two tick functions, both have a very different logic.
3) Like how do I control the opening animation speed
You have specified the node as fixed with x and y
Like: {"name": "You", "val": "You", "x": width * 0.50, "y": height * 0.5, "fixed": true}
In this case the force layout does not calculate the x and y since you have said its a fixed node which means it cannot be moved via force layout.
In case you want the layout to have animation on load to calculate its own place read this awesome tutorial
working code here
I have a d3 stacked column chart that I'm very happy with. The full code is in a JS Fiddle.
What I'd like to do is lop the last data series off, and set it on its own axis, but ensure that it maintains the same scale. So if this is my data:
var dataset = [
// apples
[{"x": 1, "y": 5 }, { "x": 2, "y": 4 }, { "x": 3, "y": 2 }, { "x": 4, "y": 7 }, { "x": 5, "y": 23 }],
// oranges
[{ "x": 1, "y": 10 }, { "x": 2, "y": 12 }, { "x": 3, "y": 19 }, { "x": 4, "y": 23 }, { "x": 5, "y": 17 }],
// grapes
[{ "x": 1, "y": 22 }, { "x": 2, "y": 28 }, { "x": 3, "y": 32 }, { "x": 4, "y": 35 }, { "x": 5, "y": 43 }],
// carrots
[{"x": 1, "y": 5 }, { "x": 2, "y": 4 }, { "x": 3, "y": 23 }, { "x": 4, "y": 2 }, { "x": 5, "y": 7 }]
];
I'd like to keep apples, oranges and grapes stacked, but I want carrots separated out. Carrots is always the last series. I was hoping I could draw the carrots into the same SVG with this:
var lower_svg = d3.select("#chart")
.append("svg")
.attr("width", w)
.attr("height", b);
var lower_rects = lower_svg.selectAll("rect")
.data(dataset[3])
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", h)
.attr("height", function(d) {
return yScale(d.y);
})
.attr("width", xScale.rangeBand());
But a) that doesn't work (it doesn't draw anything) and b) that calls on the data series 3, which happens to be the last one in this example but isn't always.
And ... if it did work it would draw the carrots twice, once stacked with the other fruits and once below. I only want to draw it once, below.
What I want is to have this chart of various fruit: https://jsfiddle.net/oa7hho9q/17/
And this chart of carrots: https://jsfiddle.net/oa7hho9q/19/
Using the same x and y scales and pulling from the same dataset, where, carrots is just the last series in the set.
I have addressed your problem like this:
Step 1:
I pop out the carrot related data.
var carrots = dataset.pop(); store it in variable carrots
Step 2
I make 2 groups
//this g(group) will hold the stacked chart for carrot
var svgcarrot = svg.append("g").attr("transform", "translate(0,200)");
//this g(group) will hold the stacked chart for other fruits
var svg = svg.append("g").attr("transform", "translate(0,-150)");
//you may change the translate to move the chart as per your choice of positioning.
Step3
Make a function to make charts input svg group and its related dataset
//here svg is the group on which you wish to draw the chart.
//dataset is the data for which the chart need to be drawn.
function makeChart(dataset, svg) {
Step4
Inside your makeChart function your usual stack bar chart code.
function makeChart(dataset, svg) {
var stack = d3.layout.stack();
stack(dataset);//set data
xScale = d3.scale.ordinal()
.domain(d3.range(dataset[0].length))
.rangeRoundBands([0, w], 0.05);
yScale = d3.scale.linear()
.domain([0,
d3.max(dataset, function(d) {
return d3.max(d, function(d) {
return d.y0 + d.y;
});
})
])
.range([0, h / 2]);//2 chart so height/2
//make groups for fruits
var groups = svg.selectAll("g")
.data(dataset)
.enter()
.append("g")
.style("fill", function(d, i) {
return colors(i);
});
//make rectangles
var rects = groups.selectAll("rect")
.data(function(d) {
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d, i) {
return h - b - (yScale(d.y0 + d.y));
})
.attr("height", function(d) {
return yScale(d.y);
})
.attr("width", xScale.rangeBand());
}
Step 5
Now make your first chart
makeChart(dataset, svg);//note dataset has no carrot data as its popped in step1 also the svg container group made in step 2
makeChart([carrots], svgcarrot);//make carrot chart note the svgcarrot container group made in step 2
working example here
I have a d3 force directed layout with data in a similar structure below. Is it possible to apply collapsible force layout such as http://bl.ocks.org/mbostock/1062288 to it? I want a node to be collapsed /expanded on click.
{
"nodes": [
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314},
],
"links": [
{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0},
{"source": 1, "target": 3},
{"source": 3, "target": 2},
]
}
If I understand correctly, perhaps this is what you are looking for. I edited the demo you linked to. Now when a source node is collapsed, we iterate over all edges and look for other nodes it has edges to.
for each target node that the source node has an edge to, we increment it's collapsing count. If a node has a collapsing count of greater than zero, it is not displayed.
When we uncollapse a node, we do the same thing, except we decrement from the collapsing count.
We need this collapsing count, since, as we are not in a tree, nodes can have more than one node which should cause them to collapse.
I made this work for directed graphs, though I'm not sure that's what you wanted.
Let me know what you think!
The json I used:
{
"nodes": [
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314}
],
"links": [
{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0},
{"source": 1, "target": 3},
{"source": 3, "target": 2}
]
}
Modified tutorial code:
<!DOCTYPE html>
<meta charset="utf-8">
<title>Force-Directed Graph</title>
<style>
.node {
cursor: pointer;
stroke: #3182bd;
stroke-width: 1.5px;
}
.link {
fill: none;
stroke: #9ecae1;
stroke-width: 1.5px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500,
root;
var force = d3.layout.force()
.size([width, height])
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
//Added markers to indicate that this is a directed graph
svg.append("defs").selectAll("marker")
.data(["arrow"])
.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5");
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
d3.json("graph.json", function(json) {
root = json;
//Give nodes ids and initialize variables
for(var i=0; i<root.nodes.length; i++) {
var node = root.nodes[i];
node.id = i;
node.collapsing = 0;
node.collapsed = false;
}
//Give links ids and initialize variables
for(var i=0; i<root.links.length; i++) {
var link = root.links[i];
link.source = root.nodes[link.source];
link.target = root.nodes[link.target];
link.id = i;
}
update();
});
function update() {
//Keep only the visible nodes
var nodes = root.nodes.filter(function(d) {
return d.collapsing == 0;
});
var links = root.links;
//Keep only the visible links
links = root.links.filter(function(d) {
return d.source.collapsing == 0 && d.target.collapsing == 0;
});
force
.nodes(nodes)
.links(links)
.start();
// Update the links…
link = link.data(links, function(d) { return d.id; });
// Exit any old links.
link.exit().remove();
// Enter any new links.
link.enter().insert("line", ".node")
.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; })
.attr("marker-end", "url(#arrow)");
// Update the nodes…
node = node.data(nodes, function(d){ return d.id; }).style("fill", color);
// Exit any old nodes.
node.exit().remove();
// Enter any new nodes.
node.enter().append("circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) { return Math.sqrt(d.size) / 10 || 4.5; })
.style("fill", color)
.on("click", click)
.call(force.drag);
}
function tick() {
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; });
}
// Color leaf nodes orange, and packages white or blue.
function color(d) {
return d.collapsed ? "#3182bd" : d.children ? "#c6dbef" : "#fd8d3c";
}
// Toggle children on click.
function click(d) {
if (!d3.event.defaultPrevented) {
//check if link is from this node, and if so, collapse
root.links.forEach(function(l) {
if(l.source.id == d.id) {
if(d.collapsed){
l.target.collapsing--;
} else {
l.target.collapsing++;
}
}
});
d.collapsed = !d.collapsed;
}
update();
}
</script>
Try this:
var width = 960,height = 500;
var force = d3.layout.force().size([width, height]).charge(-400)
.linkDistance(40)
.on("tick", tick);
var drag = force.drag().on("dragstart", dragstart);
var svg = d3.select("body").append("svg").attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
d3.json("graph.json", function(error, graph) {
force.nodes(graph.nodes).links(graph.links)
.start();
link = link.data(graph.links).enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 12)
.call(drag);
});
function tick() {
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 dragstart(d) {
d3.select(this).classed("fixed", d.fixed = true);
}
You should use json file like this:
graph.json
{
"nodes": [
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314},
],
"links": [
{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0},
{"source": 1, "target": 3},
{"source": 3, "target": 2},
]
}
I would like to get the coordinates of a point on a line by clicking on the line using the following code:
var lineData = [ { "x": 1, "y": 5}, { "x": 20, "y": 20},
{ "x": 40, "y": 10}, { "x": 60, "y": 40},
{ "x": 80, "y": 5}, { "x": 100, "y": 60}];
var lineFunction = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("linear");
var svgContainer = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
var lineGraph = svgContainer.append("path")
.data([lineData]).attr("d", lineFunction)
//.attr("d", lineFunction(lineData))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none")
.on('mousedown', function(d) {
console.log({"x":d.x, "y":d.y})
});
(I updated the code to address the comments, but I still get "Object {x: undefined, y: undefined}")
I keep getting an "undefined" when clicking on the line. Am I missing a step?
You can get the coordinates of an event using d3.event:
.on("mousedown", function() {
console.log({"x": d3.event.x, "y": d3.event.y});
});
use mouse event
.on('mousedown', function(d) {
var m = d3.mouse(this);
console.log("x:"+m[0]+" y:"+m[1]);
});
in your function m[0] and m[1] gives you X and Y.