I have a multiline chart that allows a user to click on the legend to hide/show different lines. As this happens the Y axis is re-calculated and the lines also update based on the new max y axis value.
This works fine except for one bug I can't figure out. If you deselect the topmost line and another line (2 deselected lines) and then re-enable the topmost line the circles only move part way through the transition. Enabling the second line seems to finish them and the circles then end up where they should be.
Here is a fiddle: https://jsfiddle.net/goodspeedj/5ewLxpre/
.on("click", function(d) {
var selectedPath = svg.select("path." + d.key);
//var totalLength = selectedPath.node().getTotalLength();
if (d.visible === 1) {
d.visible = 0;
} else {
d.visible = 1;
}
rescaleY();
updateLines();
updateCircles();
svg.select("rect." + d.key).transition().duration(500)
.attr("fill", function(d) {
if (d.visible === 1) {
return color(d.key);
} else {
return "white";
}
})
svg.select("path." + d.key).transition().duration(500)
.delay(150)
.style("display", function(d) {
if(d.visible === 1) {
return "inline";
}
else return "none";
})
.attr("d", function(d) {
return line(d.values);
});
svg.selectAll("circle." + d.key).transition().duration(500)
//.delay(function(d, i) { return i * 10; })
.style("display", function(a) {
if(d.visible === 1) {
return "inline";
}
else return "none";
});
})
.on("mouseover", function(d) {
d3.select(this)
.attr("height", 12)
.attr("width", 27)
d3.select("path." + d.key).transition().duration(200)
.style("stroke-width", "4px");
d3.selectAll("circle." + d.key).transition().duration(200)
.attr("r", function(d, i) { return 4 })
// Fade out the other lines
var otherlines = $(".line").not("path." + d.key);
d3.selectAll(otherlines).transition().duration(200)
.style("opacity", 0.3)
.style("stroke-width", 1.5)
.style("stroke", "gray");
var othercircles = $("circle").not("circle." + d.key);
d3.selectAll(othercircles).transition().duration(200)
.style("opacity", 0.3)
.style("stroke", "gray");
})
.on("mouseout", function(d) {
d3.select(this)
.attr("height", 10)
.attr("width", 25)
d3.select("path." + d.key).transition().duration(200)
.style("stroke-width", "1.5px");
d3.selectAll("circle." + d.key).transition().duration(200)
.attr("r", function(d, i) { return 2 })
// Make the other lines normal again
var otherlines = $('.line').not("path." + d.key);
d3.selectAll(otherlines).transition().duration(100)
.style("opacity", 1)
.style("stroke-width", 1.5)
.style("stroke", function(d) { return color(d.key); });
var othercircles = $("circle").not("circle." + d.key);
d3.selectAll(othercircles).transition().duration(200)
.style("opacity", 1)
.style("stroke", function(d) { return color(dimKey(d)); });
});
To reproduce:
Deselect PFOS (red)
Deselect PFBA (blue)
Enable PFOS (red) again. The circles do not line up with the line.
Enable PFBA (blue) again. The circles complete and end up where they should be
The problem is one transition is cancelling the other transition.
i.e when updateLines transition function is running on all lines.
You are running another transition on the clicked line inside the click function:
svg.select("path." + d.key).transition().duration(500)
.delay(150)...
So one approach would be to run one transition and on transition complete run the other.
Other approach would be to not have transition in updateCircle and updateLines
working code here (using approach 2)
Related
I am new to d3. I created a bar chart. Append text and percentage in the bar chart with animation. When bar chart draw then the number and percentage go from bottom to top to the desired location. Here is the code
svg.selectAll(".bar")
.data(data)
.enter()
.append("g")
.attr("class", "g rect")
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.label); })
.attr("y", h)
.on("mouseover", onMouseOver) //Add listener for the mouseover event
... // attach other events
.transition()
.ease(d3.easeLinear)
.duration(2000)
.delay(function (d, i) {
return i * 50;
})
.attr("y", function(d) { return y(d.percentage.slice(0, -1)); })
.attr("width", x.bandwidth() - 15) // v4’s console.log(bands.bandwidth()) replaced v3’s console.log(bands.rangeband()).
.attr("height", function(d) { return h - y(d.percentage.slice(0, -1)); }) // use .slice to remove percentage sign at the end of number
.attr("fill", function(d) { return d.color; });
var legend = svg.append("g");
svg.selectAll(".g.rect").append("text")
.text(function(d) { return d.value })
.attr("x", function(d) { return x(d.label) + x.bandwidth() / 2 - 15; })
.attr("y", h)
.transition()
.ease(d3.easeLinear)
.duration(2000)
.attr("y", function(d) { return y(d.percentage.slice(0, -1) / 2);}) // use slice to remove percentage sign from the end of number
.attr("dy", ".35em")
.style("stroke", "papayawhip")
.style("fill", "papayawhip");
svg.selectAll(".g.rect").append("text")
.text(function(d) { return d.percentage; })
.attr("x", function(d) { return x(d.label) + x.bandwidth() / 2 - 20; })
.attr("y", h)
.transition()
.ease(d3.easeLinear)
.duration(2000)
.attr("y", function(d) { return y(d.percentage.slice(0, -1)) - 10; }) // use slice to remove percentage sign from the end of number
.attr("dy", ".35em")
.attr("fill", function(d) { return d.color; });
Now I want to apply text transition. Like instead of just printing say 90%(d.percentage). I want that it starts from 0 and goes to d.percentage gradually. How can I apply text transition in this case. I tried the following but it didn't work
svg.selectAll(".g.rect").append("text")
.text(function(d) { return d.percentage; })
.attr("x", function(d) { return x(d.label) + x.bandwidth() / 2 - 20; })
.attr("y", h)
.transition()
.ease(d3.easeLinear)
.duration(2000)
.tween("text", function(d) {
var i = d3.interpolate(0, d.percentage.slice(0, -1));
return function(t) {
d3.select(this).text(i(t));
};
})
.attr("y", function(d) { return y(d.percentage.slice(0, -1)) - 10; }) // use slice to remove percentage sign from the end of number
.attr("dy", ".35em")
.attr("fill", function(d) { return d.color; });
The problem is the this value.
Save it in the closure (that).
Use a number interpolator so you can round the result to a number of decimals.
var ptag = d3.select('body').append('p');
ptag.transition()
.ease(d3.easeLinear)
.duration(2000)
.tween("text", function(d) {
var that = this;
var i = d3.interpolate(0, 90); // Number(d.percentage.slice(0, -1))
return function(t) {
d3.select(that).text(i(t).toFixed(2));
};
})
<script src="https://d3js.org/d3.v5.min.js"></script>
Your problem is that you return function(t) { ... } and try to access this of parent function inside. The solution is to return arrow function, which does not shadow this value:
return t => d3.select(this).text(i(t));
(by the way, you may also want to round percentage before printing)
-------------Edit --------------
Here is the working code
var numberFormat = d3.format("d");
svg.selectAll(".g.rect").append("text")
.attr("x", function(d) { return x(d.label) + x.bandwidth() / 2 - 20; })
.attr("y", h)
.transition()
.ease(d3.easeLinear)
.duration(2000)
.tween("text", function(d) {
var element = d3.select(this);
var i = d3.interpolate(0, d.percentage.slice(0, -1));
return function(t) {
var percent = numberFormat(i(t));
element.text(percent + "%");
};
//return t => element.text(format(i(t)));
})
.attr("y", function(d) { return y(d.percentage.slice(0, -1)) - 10; }) // use slice to remove percentage sign from the end of number
.attr("dy", ".35em")
.attr("fill", function(d) { return d.color; });
Thanks :)
I'm working on a map (found here), that is using the svg viewbox attribute to scale with the size of the client.
Unfortunately the project I'm using, d3.geoAlbersUsa() does not seem to scale the tooltip correctly with the rest of the SVG. As in, it suddenly places the tooltip in the same spot it would be if the client width had been the original 960x500 specs.
Here's the tooltip code:
d3.tsv("CitiesTraveledTo.tsv",cityVisited, function(data) {
var cities = svg.selectAll(".city")
.data(data)
.enter()
.append("g")
.classed("city",true);
cities.append("line")
.attr("x1", function(d) {
return projection([d.Longitude, d.Latitude])[0];
})
.attr("x2", function(d) {
return projection([d.Longitude, d.Latitude])[0];
})
.attr("y1", function(d) {
return projection([d.Longitude, d.Latitude])[1]-pinLength;
})
.attr("y2", function(d) {
return (projection([d.Longitude, d.Latitude])[1]);
})
.attr("stroke-width",function(d) {
return 2;
})
.attr("stroke",function(d) {
return "grey";
});
cities.append("circle")
.attr("cx", function(d) {
return projection([d.Longitude, d.Latitude])[0];
})
.attr("cy", function(d) {
return projection([d.Longitude, d.Latitude])[1]-pinLength;
})
.attr("r", function(d) {
return 3;
})
.style("fill", function(d) {
if (d.Reason === "Work") {
return "rgb(214, 69, 65)";
}
else if (d.Reason === "Fun") {
return "rgb(245, 215, 110)";
}
else {
return "rgb(214, 69, 65)";
}
})
.style("opacity", 1.0)
// Modification of custom tooltip code provided by Malcolm Maclean, "D3 Tips and Tricks"
// http://www.d3noob.org/2013/01/adding-tooltips-to-d3js-graph.html
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.text(d.City + ", " + d.State)
.style("left", function() {
var centerCircle = (projection([d.Longitude, d.Latitude])[0]);
return (centerCircle-26) + "px";
})
.style("top", function() {
var centerCircle = projection([d.Longitude, d.Latitude])[1];
var circleRadius = 3;
return ( centerCircle - circleRadius - 33-pinLength) + "px";
});
div.append("div").attr("class","arrow-down");
})
// fade out tooltip on mouse out
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});
I thought that the scaling should just happen automatically for the tooltip as well. Wrong. I then tried to reset the height and width passed to the projection and that didn't work. What's the best way to get the element bound to a data "d" node?
I ask because it will likely be easier to say "for this node, get me this element, give me the bound html element", so that I can set the position of the tooltip relative to the new position of the bound element.
I am working on the modification of Mike Bostock's general update pattern III block and having a hard time understanding why, though the enter and exit values show up, the update values are not. I've read that assigning the specific value instead of using the data array value will help, as with a key, but this did not work. How do I modify this so entering values show up with their fill style, red color? I have read SO posts and re-read "How Selections Work" but still can't make it work.
Here is the code:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
text {
font: bold 28px monospace;
}
.enter {
fill: green;
}
.update {
fill: red;
}
.exit {
fill: blue;
}
</style>
<body>
<script src="../d3.v3.js"></script>
<script>
function randomData() {
return Math.floor(Math.random() * 200);
}
var the_values = [];
function randomEntry() {
var numlist = [];
var randomEntry;
var maximum,minimum;
maximum = 10; minimum = 1
var random_in_range = Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;
var length_of_array = random_in_range;
console.log("length_of_array", length_of_array);
for (i = 0; i < length_of_array; i++) {
numlist.push([randomData(), randomData()]);
}
return numlist;
}
the_values = randomEntry();
console.log("the_values", the_values);
var width = 360,
height = 400;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(32," + (height / 2) + ")");
function update(data) {
// DATA JOIN
// Join new data with old elements, if any.
var text = svg.selectAll("text")
.data(data, function(d) {
return d;
})
.attr("transform", "translate(20," + (30) + ")");
var circles = svg.selectAll("circle")
.data(data, function(d) {
return d;
})
.attr("transform", "translate(20," + (30) + ")");
// UPDATE
// Update old elements as needed.
circles.attr("class", "update")
.transition()
.duration(750)
.attr("opacity", 0.3)
.attr("cx", function(d, i) {
return d[0];
})
.attr("cy", function(d, i) {
return d[1];
})
text.attr("class", "update")
.transition()
.duration(750)
.attr("x", function(d, i) {
return d[0];
})
.attr("y", function(d, i) {
return d[1];
})
// ENTER
// Create new elements as needed.
circles.enter().append("circle")
.attr("class", "enter")
.attr("opacity", 0.3)
.attr("r", 25)
.attr("cx", function(d, i) {
return d[0];
})
.attr("cy", function(d, i) {
return d[1];
})
.style("fill-opacity", 1e-6)
.transition()
.duration(750)
.attr("r", 30)
.style("fill-opacity", 1);
text.enter().append("text")
.attr("class", "enter")
.attr("dy", ".25em")
.attr("x", function(d) {
return d[0];
})
.attr("y", function(d) {
return d[1];
})
.style("fill-opacity", 1e-6)
.text(function(d) {
return d[0];
})
.transition()
.duration(750)
.style("fill-opacity", 1);
// EXIT
// Remove old elements as needed.
text.exit()
.attr("class", "exit")
.transition()
.duration(750)
.attr("y", 60)
.style("fill-opacity", 1e-6)
.remove();
circles.exit()
.attr("class", "exit")
.transition()
.duration(750)
.style("fill-opacity", 1e-6)
.remove();
}
// The initial display.
update(the_values);
// Grab a random sample of letters from the alphabet, in alphabetical order.
setInterval(function() {
update(randomEntry());
}, 1500);
</script>
From a quick glance at your code, it seems to be doing what you are looking for. Your enter circles are actually filled green, so you are actually seeing those. Updates are changed to red, but you don't see many of those because you are picking a few random numbers from 1-200. It's just unlikely that you will end up with any in the update selection, because that means that you selected the same number twice in a row.
To see some update circles, change:
return Math.floor(Math.random() * 200);
To:
return Math.floor(Math.random() * 10);
This throws the positions off, but you should soon see some red circles.
The reason is that in the update function you are always changing the whole array of input.
You are doing:
setInterval(function() {
update(randomEntry());//this will change the full array set
}, 1500);
This should have been:
setInterval(function() {
the_values.forEach(function(d){
//change the data set for update
})
update(the_values);
}, 1500);
Please note above i have not created a new array but I am passing the same array with changes to the update function.
Working fiddle here
Hope this helps!
First time using d3.js and am experimenting with using this example: http://bl.ocks.org/christophermanning/1734663 in a site I'm making, but I want the fill of the shapes to be images instead of a purple color.
I think the relevant code is inside the update function as follows:
path = path.data(d3_geom_voronoi(vertices))
path.enter().append("path")
// drag node by dragging cell
.call(d3.behavior.drag()
.on("drag", function(d, i) {
vertices[i] = {x: vertices[i].x + d3.event.dx, y: vertices[i].y + d3.event.dy}
})
)
.style("fill", function(d, i) { return color(0) })
path.attr("d", function(d) { return "M" + d.join("L") + "Z"; })
.transition().duration(150).style("fill", function(d, i) { return color(d3.geom.polygon(d).area()) })
path.exit().remove();
So first I comment out..
.style("fill", function(d, i) { return color(d3.geom.polygon(d).area()) })
..so my image fill wont be overwritten on update, and then I need to replace..
.style("fill", function(d, i) { return color(0) })
..with "something", but before I replace it I define the image by adding..
var image = svg.append("image")
.attr("xlink:href", "http://lorempixel.com/g/400/400/")
.attr("x", 0)
.attr("y", 0)
.attr("width", 400)
.attr("height", 400)
.attr("id", "fillImage");
..before the update function, and then I've tried replacing the fill code with stuff like:
.style("fill", function(d, i) { return "url('#fillImage')" })
or
.style("fill", function(d, i) { return "url(#fillImage)" })
or
.style("fill", "url('#fillImage')")
or
.style("fill", "url(#fillImage)")
or all the same except using "attr" instead of "style", as well as not using an id but the image varible, so: image, image[0], image[0][0], image[0][0].outerHTML. But nothing seems to work, so any ideas?
I am new to D3 and having trouble setting the bounds for my force directed layout. I have managed to piece together (from examples) what I would like, but I need the graph to be contained. In the tick function, a transform/translate will display my graph correctly, but when i use cx and cy with Math.max/min (See commented code), the nodes are pinned to the
top left corner while the lines are contained properly.
Here is what I have below... what am I doing wrong??
var w=960, h=500, r=8, z = d3.scale.category20();
var color = d3.scale.category20();
var force = d3.layout.force()
.linkDistance( function(d) { return (d.value*180) } )
.linkStrength( function(d) { return (1/(1+d.value)) } )
.charge(-1000)
//.gravity(.08)
.size([w, h]);
var vis = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + w / 4 + "," + h / 3 + ")");
vis.append("svg:rect")
.attr("width", w)
.attr("height", h)
.style("stroke", "#000");
d3.json("miserables.json", function(json) {
var link = vis.selectAll("line.link")
.data(json.links);
link.enter().append("svg: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.source.x; })
.attr("y2", function(d) { return d.source.y; })
.style("stroke-width", function(d) { return (1/(1+d.value))*5 });
var node = vis.selectAll("g.node")
.data(json.nodes);
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.on("mouseover", fade(.1))
.on("mouseout", fade(1))
.call(force.drag);
nodeEnter.append("svg:circle")
.attr("r", r)
.style("fill", function(d) { return z(d.group); })
.style("stroke", function(d) { return
d3.rgb(z(d.group)).darker(); });
nodeEnter.append("svg:text")
.attr("text-anchor", "middle")
.attr("dy", ".35em")
.text(function(d) { return d.name; });
force
.nodes(json.nodes)
.links(json.links)
.on("tick", tick)
.start();
function tick() {
// This works
node.attr("transform", function(d) { return "translate(" + d.x + ","
+ d.y + ")"; });
// This contains the lines within the boundary, but the nodes are
stuck in the top left corner
//node.attr("cx", function(d) { return d.x = Math.max(r, Math.min(w
- r, d.x)); })
// .attr("cy", function(d) { return d.y = Math.max(r, Math.min(h -
r, 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; });
}
var linkedByIndex = {};
json.links.forEach(function(d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
function isConnected(a, b) {
return linkedByIndex[a.index + "," + b.index] ||
linkedByIndex[b.index + "," + a.index] || a.index == b.index;
}
function fade(opacity) {
return function(d) {
node.style("stroke-opacity", function(o) {
thisOpacity = isConnected(d, o) ? 1 : opacity;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.style("stroke-opacity", opacity).style("stroke-opacity",
function(o) {
return o.source === d || o.target === d ? 1 : opacity;
});
};
}
});
There's a bounding box example in my talk on force layouts. The position Verlet integration allows you to define geometric constraints (such as bounding boxes and collision detection) inside the "tick" event listener; simply move the nodes to comply with the constraint and the simulation will adapt accordingly.
That said, gravity is definitely a more flexible way to deal with this problem, since it allows users to drag the graph outside the bounding box temporarily and then the graph will recover. Depend on the size of the graph and the size of the displayed area, you should experiment with different relative strengths of gravity and charge (repulsion) to get your graph to fit.
A custom force is a possible solution too. I like this approch more since not only the displayed nodes are repositioned but the whole simulation works with the bounding force.
let simulation = d3.forceSimulation(nodes)
...
.force("bounds", boxingForce);
// Custom force to put all nodes in a box
function boxingForce() {
const radius = 500;
for (let node of nodes) {
// Of the positions exceed the box, set them to the boundary position.
// You may want to include your nodes width to not overlap with the box.
node.x = Math.max(-radius, Math.min(radius, node.x));
node.y = Math.max(-radius, Math.min(radius, node.y));
}
}
The commented code works on node which is, from your definition, a svg g(rouping) element and does not operate the cx/cy attributes. Select the circle element inside node to make these attributes come alive:
node.select("circle") // select the circle element in that node
.attr("cx", function(d) { return d.x = Math.max(r, Math.min(w - r, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(r, Math.min(h - r, d.y)); });