I'm having trouble getting a transition to repeat, for a series of elements, in this case a set of three lines. The animation runs just fine once, but when it is repeated (with the same data), all three lines merge into a single line (the last array in data). What am I doing wrong?
(function() {
var w = 100, h = 100
var div = d3.select('#sketches').append('div')
var svg = div.append("svg")
.attr("width", w)
.attr("height", h)
var x = 0, y = 55
var data = [
[x, y, x+20, y-40],
[x+10, y, x+30, y-40],
[x+20, y, x+40, y-40]
];
(function lines() {
svg.selectAll('line')
.data(data).enter().append('line')
.attr("stroke", "black")
.attr('x1', function(d) {return d[0]})
.attr('y1', function(d) {return d[1]})
.attr('x2', function(d) {return d[2]})
.attr('y2', function(d) {return d[3]})
.transition()
.duration(3000)
.ease('linear')
.attr('x1', function(d) {return d[0] + 60})
.attr('y1', function(d) {return d[1]})
.attr('x2', function(d) {return d[2] + 60})
.attr('y2', function(d) {return d[3]})
.each('end', function(d) {
d3.select(this).remove()
lines()
})
})()
})()
body {
padding: 1rem;
}
svg {
background-color: silver;
stroke: black;
stroke-width: 1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="sketches"></div>
The issue is the each function will initiate for each line you have. So actually what you are doing is calling lines() three times every time. Why it's yieling the output of one line I'm not entirely sure (still looking into it) but for some reason, it seems like data defaults to the last array so its only setting the drawing to be based on data[3].
To fix it, you want to make sure lines() only gets called after it has finished going through removing all the lines so it only runs once. I'm pretty sure there is better way (i.e. a promise of some kind so after all of each has ran, it'll run a function, but what you can do is set a count and then just run lines() every N times where N is the number of lines you want removed. Because you go through array data and append a line for each index, N is data.length.
(I'm gonna see if there's a cleaner way to do this and I'll edit my answer if I find a way but hopefully this helps you understand the issue at the very least)
(function() {
var w = 100, h = 100
var div = d3.select('#sketches').append('div')
var svg = div.append("svg")
.attr("width", w)
.attr("height", h)
var x = 0, y = 55
var data = [
[x, y, x+20, y-40],
[x+10, y, x+30, y-40],
[x+20, y, x+40, y-40]
];
var count = 0;
(function lines() {
svg.selectAll('line')
.data(data).enter().append('line')
.attr("stroke", "black")
.attr('x1', function(d) {return d[0]})
.attr('y1', function(d) {return d[1]})
.attr('x2', function(d) {return d[2]})
.attr('y2', function(d) {return d[3]})
.transition()
.duration(3000)
.ease('linear')
.attr('x1', function(d) {return d[0] + 60})
.attr('y1', function(d) {return d[1]})
.attr('x2', function(d) {return d[2] + 60})
.attr('y2', function(d) {return d[3]})
.each('end', function(d) {
d3.select(this).remove()
count++;
if (count == data.length) {
count = 0;
lines();
}
})
})()
})()
body {
padding: 1rem;
}
svg {
background-color: silver;
stroke: black;
stroke-width: 1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="sketches"></div>
Related
I'm trying to create a map of coordinates from some data I got in a csv file. The converting of the X/Y axes works perfectly, the circles (or rather dots) get drawn but the mouseover tooltip always displays the last values (or rather the last values +1 which is in my array out of bounds even though the tooltip should be set with the current values of the array.
Longitude and altitude are my two array names
var svgContainer = d3.select("body").append("svg")
.attr("width", 700)
.attr("height", 250)
.style("border", "1px solid black");
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
for (i = 0; i < longitude.length; i++) {
var circleSelection = svgContainer.append("circle")
.attr("cx", longitude[i])
.attr("cy", altitude[i])
.attr("r", 2)
.style("fill", "purple")
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div .html("X: " + longitude[i] + " Y: " + altitude[i])
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});
}
and here's the css but I doubt the problem's to be found in here
<style>
div.tooltip {
position: absolute;
text-align: center;
width: 60px;
height: 28px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
Every clue is much appreciated
As a general rule: do not use loops for appending elements in a D3 code. Not only this is not the idiomatic D3 but, more importantly, things will break (as you're seeing right now).
Before anything, here is an explanation of why all the values are the same: JavaScript closure inside loops – simple practical example
Let's see this, hover over any circle:
var data = ["foo", "bar", "baz"];
var svg = d3.select("svg");
for (var i = 0; i < data.length; i++) {
svg.append("circle")
.attr("cy", 75)
.attr("cx", 50 + i * 100)
.attr("r", 20)
.attr("fill", "teal")
.on("mouseover", function() {
console.log(data[i - 1])
})
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Things get better using let:
var data = ["foo", "bar", "baz"];
var svg = d3.select("svg");
for (let i = 0; i < data.length; i++) {
svg.append("circle")
.attr("cy", 75)
.attr("cx", 50 + i * 100)
.attr("r", 20)
.attr("fill", "teal")
.on("mouseover", function() {
console.log(data[i])
})
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
However, even if using let gives the correct result, it is not a good solution, because you are not binding any data.
The best solution is: use a D3 "enter" selection, binding data to the elements:
var data = ["foo", "bar", "baz"];
var svg = d3.select("svg");
svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cy", 75)
.attr("cx", function(d, i) {
return 50 + i * 100
})
.attr("r", 20)
.attr("fill", "teal")
.on("mouseover", function(d) {
console.log(d)
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
I was hoping somebody could help, I'm completely new to javascript but have been learning it in order to start producing interactive outputs in D3.
So I've started with the basics and produced line graphs etc, now I want to add an interactive element.
So I have a line graph, a slider and a function, the question is how do I link these up? playing with some online examples I understand how I can get the slider to update attributes of objects such as text, but I want it to update parameters in a loop to perform a calculation, which then runs and gives the line graph output.
My code is as follows and I've annotated the loop which I want to update:
<!DOCTYPE html>
<style type="text/css">
path {
stroke-width: 2;
fill: none;
}
line {
stroke: black;
}
text {
font-family: Arial;
font-size: 9pt;
}
</style>
<body>
<p>
<label for="repRate"
style="display: inline-block; width: 240px; text-align: right">
R = <span id="repRate-value">…</span>
</label>
<input type="range" min="0.0" max="1.0" step="0.01" id="repRate">
</p>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
d3.select("#repRate").on("input", function() {
update(+this.value);
});
update(0.1);
function update(repRate) {
// adjust slider text
d3.select("#repRate-value").text(repRate);
d3.select("#repRate").property("value", repRate);
}
//This is the function I want to update when the slider moves, I want the parameter R to update
//with the slider value, then loop through and produce a new graph
function parHost (R){
var i = 0;
var result = [];
do {
//I want to be able to keep it as a loop or similar, so that I can add more complex
//equations into it, but for now I've kept it simple
Nt1 = R*i
result.push(Nt1++) ;
Nt = Nt1
i++;
}
while (i < 50);
return result};
var data = parHost(0.5),
w = 900,
h = 200,
marginY = 50,
marginX = 20,
y = d3.scale.linear().domain([0, d3.max(data)]).range([0 + marginX, h - marginX]),
x = d3.scale.linear().domain([0, data.length]).range([0 + marginY, w - marginY])
var vis = d3.select("body")
.append("svg:svg")
.attr("width", w)
.attr("height", h)
var g = vis.append("svg:g")
.attr("transform", "translate(0, 200)");
var line = d3.svg.line()
.x(function(d,i) { return x(i); })
.y(function(d) { return -1 * y(d); })
g.append("svg:path").attr("d", line(data)).attr('stroke', 'blue');
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -1 * y(0))
.attr("x2", x(w))
.attr("y2", -1 * y(0))
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -1 * y(0))
.attr("x2", x(0))
.attr("y2", -1 * y(d3.max(data)))
g.selectAll(".xLabel")
.data(x.ticks(5))
.enter().append("svg:text")
.attr("class", "xLabel")
.text(String)
.attr("x", function(d) { return x(d) })
.attr("y", 0)
.attr("text-anchor", "middle")
g.selectAll(".yLabel")
.data(y.ticks(4))
.enter().append("svg:text")
.attr("class", "yLabel")
.text(String)
.attr("x", 0)
.attr("y", function(d) { return -1 * y(d) })
.attr("text-anchor", "right")
.attr("dy", 4)
g.selectAll(".xTicks")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xTicks")
.attr("x1", function(d) { return x(d); })
.attr("y1", -1 * y(0))
.attr("x2", function(d) { return x(d); })
.attr("y2", -1 * y(-0.3))
g.selectAll(".yTicks")
.data(y.ticks(4))
.enter().append("svg:line")
.attr("class", "yTicks")
.attr("y1", function(d) { return -1 * y(d); })
.attr("x1", x(-0.3))
.attr("y2", function(d) { return -1 * y(d); })
.attr("x2", x(0))
</script>
</body>
Any help with this would be much appreciated.
On each slider input event you have to update the parts of the chart (e.g. line, axisTicks, etc.) which depend on your data. You could e.g. extend your update function like this:
function update(repRate) {
// adjust slider text
d3.select("#repRate-value").text(repRate);
d3.select("#repRate").property("value", repRate);
// Generate new Data depending on slider value
var newData = parHost(repRate);
// Update the chart
drawChart(newData);
}
where the drawChart(newData) could look like this:
function drawChart(newData) {
// Delete the old elements
g.selectAll("*").remove();
g.append("svg:path").attr("d", line(newData)).attr('stroke', 'blue');
...
}
Another method is to declare data depending elements as variables and just change their attribute on an update (which i would recommend):
...
var dataLine = g.append("svg:path");
...
function drawChart(newData) {
path.attr("d", line(newData)).attr('stroke', 'blue');
}
Here is an example plunker.
Check out also this example.
I'm trying to chain a transition in D3 and I can't quite figure out how to make it work properly. I've read through some of the examples and I feel like I'm missing something with regards to the selections (possibly because my selections are across different layers).
You can see an example below, clicking 'Objectives' should animate a pulse of light to the "Service" node. Once the pulse arrives I want the Service node to fill to orange with a transition. At the moment I'm aware of the fact my selection will fill both circles - I'll fix that shortly.
What happens however is that when the pulse arrives nothing happens:
var t0 = svg.transition();
var t1 = t0.selectAll(".pulse")
.duration(2000)
.ease("easeInOutSine")
.attr("cx", function(d) { return d.x2; })
.attr("cy", function(d) { return d.y2; });
t1.selectAll(".node")
.style("fill", "#F79646");
The only way I seem to be able to get a change is if I change the final bit of code to:
t0.selectAll(".node")
.style("fill", "#F79646");
However that causes the node to fill instantly, rather than waiting for the pulse to arrive. It feels like the selection isn't "expanding" to select the .node instances, but I'm not quite sure
var nodes = [
{ x: 105, y: 105, r: 55, color: "#3BAF4A", title: "Objectives" },
{ x: 305, y: 505, r: 35, color: "#F79646", title: "Service" }
];
var links = [
{ x1: 105, y1: 105, x2: 305, y2: 505 }
];
var svg = d3.select("svg");
var relationshipLayer =svg.append("g").attr("id", "relationships");
var nodeLayer = svg.append("g").attr("id", "nodes");
// Add the nodes
var nodeEnter = nodeLayer.selectAll("circle").data(nodes).enter();
var nodes = nodeEnter.append("g")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")";})
.on("click", function (d) {
d3.select(this)
.select("circle")
.transition()
.style("stroke", "#397F42")
.style("fill", "#3BAF4A");
pulse(d);
});
var circles = nodes.append("circle")
.attr("class", "node")
.attr("r", function (d) { return d.r; })
.style("fill", "#1C1C1C")
.style("stroke-width", "4px")
.style("stroke", function (d) { return d.color; });
var texts = nodes.append("text")
.text(function (d) { return d.title; })
.attr("dx", function(d) { return -d.r / 2; })
.style("fill", "white");
function pulse(d) {
function distanceFunction(x1, y1, x2, y2) {
var a = (x2 - x1) * (x2 - x1);
var b = (y2 - y1) * (y2 - y1);
return Math.sqrt(a + b);
};
var lineFunction = d3.svg.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; })
.interpolate("linear");
var lines = relationshipLayer
.selectAll("line")
.data(links)
.enter()
.append("line")
.attr("x1", function(d) { return d.x1; })
.attr("y1", function(d) { return d.y1; })
.attr("x2", function(d) { return d.x2; })
.attr("y2", function(d) { return d.y2; })
.attr("stroke-dasharray", function(d) { return distanceFunction(d.x1, d.y1, d.x2, d.y2); })
.attr("stroke-dashoffset", function(d) { return distanceFunction(d.x1, d.y1, d.x2, d.y2); });
var pulse = relationshipLayer
.selectAll(".pulse")
.data(links)
.enter()
.append("circle")
.attr("class", "pulse")
.attr("cx", function(d) { return d.x1; })
.attr("cy", function(d) { return d.y1; })
.attr("r", 50);
lines.transition()
.duration(2000)
.ease("easeInOutSine")
.attr("stroke-dashoffset", 0);
var t0 = svg.transition();
var t1 = t0.selectAll(".pulse")
.duration(2000)
.ease("easeInOutSine")
.attr("cx", function(d) { return d.x2; })
.attr("cy", function(d) { return d.y2; });
t1.selectAll(".node")
.style("fill", "#F79646");
};
svg {
background: #222234;
width: 600px;
height: 600px;
font-size: 10px;
text-align: center;
font-family: 'Open Sans', Arial, sans-serif;
}
circle {
fill: url(#grad1);
}
line {
fill: none;
stroke: #fff;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg id="svg">
<defs>
<radialGradient id="grad1" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
<stop offset="5%" style="stop-color:rgb(255,255,255); stop-opacity:1" />
<stop offset="10%" style="stop-color:rgb(255,255,255); stop-opacity:0.8" />
<stop offset="20%" style="stop-color:rgb(255,255,255); stop-opacity:0.6" />
<stop offset="60%" style="stop-color:rgb(255,255,255);stop-opacity:0.0" />
</radialGradient>
</defs>
</svg>
The reason why you're not seeing a change for the second transition is that it's not applied to anything. The selection for your first transition contains all the elements with class pulse, and then you're selecting the elements with class node from the elements of this first selection. There are no elements that have both classes, therefore your selection is empty and the change is applied to no elements.
In general, you can't chain transitions in the way that you're currently using when changing selections. Instead, use the .each() event handler of the transition, which allows you to install a handler function that is executed when the transition finishes. In your case, this would look like this:
svg.selectAll(".pulse")
.transition()
.duration(2000)
.ease("easeInOutSine")
.attr("cx", function(d) { return d.x2; })
.attr("cy", function(d) { return d.y2; })
.each("end", function() {
svg.selectAll(".node")
.transition()
.duration(2000)
.style("fill", "#F79646");
});
This will select all the elements that have class node and change their fill to orange with a transition.
There are two problems with the above code -- first, as you have already observed, it changes the fill of all the nodes and not just the target, and second, the end event handler is executed for each element in the transition, not just once. For your particular example, this isn't a problem because you have only one link that's animated, but if you had several, the function (and therefore the transition) would be executed more than once.
Both problems can be fixed quite easily with the same code. The idea is to filter the selection of node elements to include only the target of the line. One way of doing this is to compare the target coordinates of the line with the coordinates of the elements in the selection:
svg.selectAll(".pulse")
.transition()
.duration(2000)
.ease("easeInOutSine")
.attr("cx", function(d) { return d.x2; })
.attr("cy", function(d) { return d.y2; })
.each("end", function(d) {
svg.selectAll(".node")
.filter(function(e) {
return e.x == d.x2 && e.y == d.y2;
})
.transition()
.duration(2000)
.style("fill", "#F79646");
});
The argument d to the handler function is the data bound to the element that is being transitioned, which contains the target coordinates. After the filter() line, the selection will contain only the circle that the line moves towards. It is safe to execute this code several times for multiple lines as long as their targets are different.
Complete demo here.
Hey guys I recently started learning D3.js and have ran into a problem: http://i.stack.imgur.com/Nqghl.png. How can I stop drawing the line at the outer edge of these circles?
Possibly another solution could be to re-arrange the layers so that the circle one is on top of the line one.
Here is my sample code
data = [{name: 'one', parent: 'one', a: 1}, {name: 'two', parent: 'one', a: 2}, {name: 'three', parent: 'one', a: 2}]
r = 30;
var centerX = function (d, i) {
return (i * ((r * 2) + 20)) + r;
};
var centerY = function (a, i) {
return (a * 160) + (r * 2);
}
var global = d3.select('body')
.append('svg')
.attr('width', 500)
.attr('height', 500)
global.selectAll("circle")
.data(data)
.enter()
.append("circle")
.style("stroke", "gray")
.style("fill", "aliceblue")
.attr('r', r)
.attr('cx', function(d, i ) {return centerX(r, i)})
.attr('cy', function(d, i) {return centerY(d.a, i)})
.attr('id', function(d) { return 'one'});
global.selectAll("line")
.data(data)
.enter()
.append("line")
.style("stroke", "rgb(6,120,155)")
.style("stroke-width", 4)
.style('stroke-opacity', .4)
.attr('x1', function(d, i) {return centerX(r, i)})
.attr('y1', function(d, i) {return centerY(d.a, i)})
.attr('x2', function(d) {
var selector = "[id="+d.parent+"]";
return global.select(selector).attr('cx');
})
.attr('y2', function(d) {
var selector = "[id="+d.parent+"]";
return global.select(selector).attr('cy');
})
Any ideas? Thanks in advance!
You are right about switching the order in which you append the lines and circles, lines first, then circles. You just have to be careful to preserve the line selection in a variable that you can use to apply the line attributes that are dependent on the circles AFTER drawing the circles.
FIDDLE example
lines
.attr('x2', function(d) {
var selector = "[id="+d.parent+"]";
return global.select(selector).attr('cx');
})
.attr('y2', function(d) {
var selector = "[id="+d.parent+"]";
return global.select(selector).attr('cy');
});
I've just started with trying out the d3 library.
I am trying to create an interactive line chart where people can plot their own points. You can find it over here: http://jsfiddle.net/6FjJ2/
My question is: how can I make sure that plotting can only be done on the x-axis' lines? If you check out my example, you will see it kind of works, but with a lot of cheating. Check out the ok variable... What would be the correct way of achieving this? I have no idea how I can achieve this with a ... so I'm getting a lot of seperate 's.
var data = [2, 3, 4, 3, 4, 5],
w = 1000,
h = 300,
monthsData = [],
months = 18;
for(i = 0; i < months; i++) {
monthsData.push(i);
}
var max = d3.max(monthsData),
x = d3.scale.linear().domain([0, monthsData.length]).range([0, w]),
y = d3.scale.linear().domain([0, max]).range([h, 0]),
pointpos = [];
lvl = [0, 10],
lvly = d3.scale.linear().domain([0, d3.max(lvl)]).range([h, 0]);
svg = d3.select(".chart")
.attr("width", w)
.attr("height", h);
svg.selectAll('path.line')
// Return "data" array which will form the path coordinates
.data([data])
// Add path
.enter().append("svg:path")
.attr("d", d3.svg.line()
.x(function(d, i) { return x(i); })
.y(y));
// Y-axis ticks
ticks = svg.selectAll(".ticky")
// Change number of ticks for more gridlines!
.data(lvly.ticks(10))
.enter().append("svg:g")
.attr("transform", function(d) { return "translate(0, " + (lvly(d)) + ")"; })
.attr("class", "ticky");
ticks.append("svg:line")
.attr("y1", 0)
.attr("y2", 0)
.attr("x1", 0)
.attr("x2", w);
ticks.append("svg:text")
.text( function(d) { return d; })
.attr("text-anchor","end")
.attr("dy", 2)
.attr("dx", -4);
// X-axis ticks
ticks = svg.selectAll(".tickx")
.data(x.ticks(monthsData.length))
.enter().append("svg:g")
.attr("transform", function(d, i) { return "translate(" + (x(i)) + ", 0)"; })
.attr("class", "tickx");
ticks.append("svg:line")
.attr("y1", h)
.attr("y2", 0)
.attr("x1", 0)
.attr("x2", 0);
ticks.append("svg:text")
.text( function(d, i) { return i; })
.attr("y", h)
.attr("dy", 15)
.attr("dx", -2);
// var d = $(".tickx:first line").css({"stroke-width" : "2", opacity : "1"});
var line;
var ok = -55;
svg.on("mousedown", mouseDown)
.on("mouseup", mouseUp);
function mouseDown() {
var m = d3.mouse(this);
line = svg.append("line")
.data(monthsData)
/* .attr("x1", m[0]) */
.attr("x1", function(d, i) { pointpos.push(m[0]); ok += 55; return ok;})
.attr("y1", m[1])
.attr("x2", function(d, i) { return ok + 56; })
/* .attr("x2", function(d, i) {return 300; }) */
.attr("y2", m[1]);
svg.on("mousemove", mouseMove);
var m = d3.mouse(this);
var point = svg.append("circle")
.attr("cx", function(d, i) { return ok; })
.attr("cy", function(d, i) { return m[1]; })
.attr("r", 8);
lvl.push(100);
}
function mouseMove() {
var m = d3.mouse(this);
line.attr("y2", m[1]);
/* .attr("y1", m[0]); */
}
function mouseUp() {
// Change null to mousemove for a graph kinda draw mode
svg.on("mousemove", mouseMove);
}
Excuse my bad code!
Thanks in advance.
It looks like you need:
histogram layout for binning your points.
ordinal scales for restricting their x-axis positions according to the bin
As a sidenote, you can use d3.svg.axis to draw the axis for you.