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.
Related
There are many cases online how to plot couple of lines in d3 if you add svg object only once, such as
svg.selectAll("line")
.data(dataset)
.enter().append("line")
.style("stroke", "black") // colour the line
.attr("x1", function(d) { console.log(d); return xScale(d.x1); })
.attr("y1", function(d) { return yScale(d.y1); })
.attr("x2", function(d) { return xScale(d.x2); })
.attr("y2", function(d) { return yScale(d.y2); });
This plot create one line. I want to create many different lines in an array smth like
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
for (a_ind=1; a_ind<3; a_ind++){
dataset_a=dataset.filter(function(d) { return (d.a==a_ind)})
svg.selectAll("line")
.data(dataset_a) - //!!! using new dataset in each cycle
.enter().append("line")
.style("stroke", "black") // colour the line
.attr("x1", function(d) { console.log(d); return xScale(d.x1); })
.attr("y1", function(d) { return yScale(d.y1); })
.attr("x2", function(d) { return xScale(d.x2); })
.attr("y2", function(d) { return yScale(d.y2); });
}
I was told it's impossible. Or maybe there is the way? And also how to access then line from dataset_a if i want to delete it with the click of the mouse?
Well, if you want to plot lines, I suggest that you append...<line>s!
The thing with a D3 enter selection is quite simple: the number of appended elements is the number of objects in the data array that doesn't match any element.
So, you just need a data array with several objects. For instance, let's create 50 of them:
var data = d3.range(50).map(function(d) {
return {
x1: Math.random() * 300,
x2: Math.random() * 300,
y1: Math.random() * 150,
y2: Math.random() * 150,
}
});
And, as in the below demo I'm selecting null, all of them will be in the enter selection. Here is the demo:
var svg = d3.select("svg");
var data = d3.range(50).map(function(d) {
return {
x1: Math.random() * 300,
x2: Math.random() * 300,
y1: Math.random() * 150,
y2: Math.random() * 150,
}
});
var color = d3.scaleOrdinal(d3.schemeCategory20);
var lines = svg.selectAll(null)
.data(data)
.enter()
.append("line")
.attr("x1", function(d) {
return d.x1
})
.attr("x2", function(d) {
return d.x2
})
.attr("y1", function(d) {
return d.y1
})
.attr("y2", function(d) {
return d.y2
})
.style("stroke", function(_, i) {
return color(i)
})
.style("stroke-width", 1);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Finally, a tip: as this is JavaScript you can use for loops anywhere you want. However, do not use for loops to append elements in a D3 code. It's unnecessary and not idiomatic.
That being said, whoever told you that it is impossible was wrong, it's clearly possible. Here is a demo (but don't do that, it's a very cumbersome and ugly code):
var svg = d3.select("svg");
var data = d3.range(50).map(function(d, i) {
return {
x1: Math.random() * 300,
x2: Math.random() * 300,
y1: Math.random() * 150,
y2: Math.random() * 150,
id: "id" + i
}
});
var color = d3.scaleOrdinal(d3.schemeCategory20);
for (var i = 0; i < data.length; i++) {
var filteredData = data.filter(function(d) {
return d.id === "id" + i
});
var lines = svg.selectAll(null)
.data(filteredData)
.enter()
.append("line")
.attr("x1", function(d) {
return d.x1
})
.attr("x2", function(d) {
return d.x2
})
.attr("y1", function(d) {
return d.y1
})
.attr("y2", function(d) {
return d.y2
})
.style("stroke", function() {
return color(i)
})
.style("stroke-width", 1);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
I would do something like this. Make each data set (1 data set per line), an array inside the final data array .enter().append() will then work properly. To remove the line on click, I added an event handler that will select the line just clicked and remove it.
var data = [[dataset_a], [dataset_b], [dataset_c], [dataset_d], [dataset_e]];
var xValue = function(d){return d.x;}
var yValue = function(d){return d.y;}
var lineFunction = d3.line()
.x(function(d) { return xScale(xValue(d)); })
.y(function(d) { return yScale(yValue(d)); });
var lines = d3.select("svg").selectAll("path")
lines.data(data)
.enter().append("path")
.attr("d", lineFunction)
.on("click", function(d){
d3.select(this).remove();
});
I have created a cluster tree layout and I want to add custom node styles to selected nodes. To be more precise, I'm adding treemap as node.
I managed to add those, but they are not positioned in the center of node.
I have tried all sort of x,y attributes and translations but I quess I don't get svg that much yet.
Part of code where I add the node is here (for JSfiddle see below):
nodeEnter.each(function(d) {
if (d.status == "D") {
var treemap = d3.layout.treemap()
.size([20, 20])
.sticky(true)
.value(function(d) {
return 1;
});
var cell = d3.select(this)
.selectAll("g")
.data(function(d) {
return treemap.nodes(d.annotations);
})
.enter().append("g")
.attr("class", "cell")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
cell.append("rect")
.attr("width", function(d) {
return d.dx;
})
.attr("height", function(d) {
return d.dy;
})
.style("fill", function(d) {
return d.children ? null : hex2rgb(color(d.parent.name));
});
}
})
Any help would be appreciated
Here is my JSfiddle.
L.
Assuming you wanted the lines to connect to the middle of the appended rect. I just added a third .attr to your JSfiddle
cell.append("rect")
.attr("width", function(d) {
return d.dx;
})
.attr("height", function(d) {
return d.dy;
})
.attr("transform","translate(0,-10)")
.style("fill", function(d) {
return d.children ? null : hex2rgb(color(d.parent.name));
});
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)
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!
Following the County Bubbles example, it's easy to add a bubble for each county. This is how it is added in the example:
svg.append("g")
.attr("class", "bubble")
.selectAll("circle")
.data(topojson.feature(us, us.objects.counties).features
.sort(function(a, b) { return b.properties.population - a.properties.population; }))
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("r", function(d) { return radius(d.properties.population); })
.append("title")
.text(function(d) {
return d.properties.name
+ "\nPopulation " + formatNumber(d.properties.population);
});
However, rather than using a variable from the json file (population), I need to update the radii according to a variable which dynamically changes (so I cannot put it in the json file beforehand as was done in the example). I call updateRadii() when a county is clicked, which needs access to the FIPS.
var currFIPS,
flowByFIPS;
var g = svg.append("g");
queue()
.defer(d3.json, "us.json")
.defer(d3.csv, "census.csv", function(d) {
return {
work: +d.workplace,
home: +d.residence,
flow: +d.flow
}
})
.await(ready);
function ready(error, us, commute) {
// Counties
g.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("d", path)
.on("click", function(d) {
// Get FIPS of selected county
currFIPS = d.id;
// Filter on selected county (i.e., grab
// people who work in the selected county)
var data = commute.filter(function(d) {
return d.work == currFIPS;
});
// Create d3.map for where these people live
flowByFIPS = d3.map(data, function(d) {
return d.home;
});
// Update radii at "home" counties to reflect flow
updateRadii();
});
// Bubbles
g.append("g")
.attr("class", "counties")
.selectAll("circle")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("circle")
.attr("id", function(d) { return d.id; })
.attr("transform", function(d) {
return "translate(" + path.centroid(d) + ")";
})
.attr("r", 0); // invisible before a county is clicked
}
function updateRadii() {
svg.selectAll(".counties circle")
.transition()
.duration(300)
.attr("r", function(d) {
return flowByFIPS.get(d.id).flow
});
}
According to the error code, I believe that the circles do not have an id (FIPS code) attached. How do I get them to have an id? (I tried nesting the circle with the path using .each as explained in this answer, but could not get it working.)
Note that the above code works for updating fill on paths (rather than circles). For example, sub updateRadii(); for updateFill(); with the function as:
function updateFill() {
svg.selectAll(".counties path")
.transition()
.duration(300)
.attr("fill", function(d) {
return flowByFIPS.get(d.id).color; // e.g., "#444"
});
}
The problem here is that you don't supply d3 with data in the update function. I will recommend you update the data loaded from the file on the clicks, and from there you update the svg.
var update = function() {
g.selectAll(".country")
.data(data)
.attr("r", function(d) { return d.properties.flow_pct });
};
var data = topojson.feature(us, us.objects.counties).features;
data.forEach(function(x) { x.properties.flow_pct = /* calc the value */; })
g.append("g")
.attr("class", "counties")
.selectAll(".country")
.data(data)
.enter()
.append("circle")
.attr("class", "country")
.attr("transform", function(d) {
return "translate(" + path.centroid(d) + ")";
})
.on("click", function(d) {
// more code
data.forEach(function(x) { x.properties.flow_pct = /* calc the NEW value */; })
update();
});
update();
I've tried to use as much as the same code as before, but still trying to straiten it a bit out. The flow is now more d3-like, since the update function works on the changed data.
Another plus which this approach is both first render and future updates use the same logic for finding the radius.
It turns out to be an obvious solution. I forgot to check for the cases where a flow doesn't exist. The code above works if updateRadii() is changed to:
function updateRadii() {
svg.selectAll(".counties circle")
.transition()
.duration(300)
.attr("r", function(d) {
if (currFIPS == d.id) {
return 0;
}
var county = flowByFIPS.get(d.id);
if (typeof county === "undefined") {
return 0;
} else {
return county.flow;
}
});
}