d3-zoom mouseup event workaround - d3.js

I'm aware that starting with v4 d3-zoom swallows up certain events for some reasons I don't fully understand. I've read some discussions about this, and I know that if I stopPropagation() on mousedown, then the zoom behavior won't get a chance to consume the event, and mouseup will consequently fire. The problem with that is that then the zoom doesnt work.
I haven't found a workaround for the case of needing to handle the mouseup event AND still have the zoom work. I'm particularly interested in the dragging case only. When the user does mousedown and starts to drag the canvas, I want to change the cursor to a clenched-hand, and when the user stops dragging and lets go of the mouse I want to change the cursor back.
How is this possible to do with the new d3-zoom behavior without resorting to a timeout? 'click' event is also not an option since that doesn't fire if there's a mousemove event in between.

I am concluding from your question that you are not able to track mouseup event after dragging the canvas. If that is the case then we can use events provided by "zoom" functionality - zoomstart and zoomend.
We can simply add that to zoom behavior and track when the user starts zoom and ends zoom. And in this way we can easily change cursor property.
Please find below code snippet and working code as well. Please let me know if i am missing anything.
var margin = {
top: -5,
right: -5,
bottom: -5,
left: -5
},
width = 460 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
var zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed)
.on("start", function() {
document.getElementsByTagName("svg")[0].style.cursor = "grab";
})
.on("end", function() {
document.getElementsByTagName("svg")[0].style.cursor = "default";
})
console.log(zoom.scaleExtent()[0], zoom.scaleExtent()[1]);
var drag = d3.drag()
.subject(function(d) {
return d;
})
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
var slider = d3.select("body").append("p").append("input")
.datum({})
.attr("type", "range")
.attr("value", zoom.scaleExtent()[0])
.attr("min", zoom.scaleExtent()[0])
.attr("max", zoom.scaleExtent()[1])
.attr("step", (zoom.scaleExtent()[1] - zoom.scaleExtent()[0]) / 100)
.on("input", slided);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.right + ")")
.call(zoom);
var rect = svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all");
var container = svg.append("g");
container.append("g")
.attr("class", "x axis")
.selectAll("line")
.data(d3.range(0, width, 10))
.enter().append("line")
.attr("x1", function(d) {
return d;
})
.attr("y1", 0)
.attr("x2", function(d) {
return d;
})
.attr("y2", height);
container.append("g")
.attr("class", "y axis")
.selectAll("line")
.data(d3.range(0, height, 10))
.enter().append("line")
.attr("x1", 0)
.attr("y1", function(d) {
return d;
})
.attr("x2", width)
.attr("y2", function(d) {
return d;
});
dots = [{
x: 100,
y: 100,
}]
dot = container.append("g")
.attr("class", "dot")
.selectAll("circle")
.data(dots)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.call(drag);
function dottype(d) {
d.x = +d.x;
d.y = +d.y;
return d;
}
function zoomed(event) {
const currentTransform = d3.event.transform;
container.attr("transform", currentTransform);
slider.property("value", currentTransform.k);
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
function slided(d) {
zoom.scaleTo(svg, d3.select(this).property("value"));
}
.dot circle {
fill: lightsteelblue;
stroke: steelblue;
stroke-width: 1.5px;
}
.dot circle.dragging {
fill: red;
stroke: brown;
}
.axis line {
fill: none;
stroke: #ddd;
shape-rendering: crispEdges;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
</style>
<title></title>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
</body>
</html>
var zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed)
.on("start", function() {
document.getElementsByTagName("svg")[0].style.cursor = "grab";
})
.on("end", function() {
document.getElementsByTagName("svg")[0].style.cursor = "default";
})

Related

Real-time scatter plot with D3. Having a hard time to deal with the data joining

For one of my projects, I want to create a real time scatter plot using D3. The sample rate needs to be around 30 updates per seconds. I managed to create a graph that updates itself but I am facing two issues :
1) I cannot figure out how to remove data using the exit() and remove() function
2) The page freezes up pretty quickly
I am also wondering if it was the most efficient way to create a real-time scatterplot. I have adapted code from https://bost.ocks.org/mike/path/
I have looked throughout blogs and tutorials to no avail. Any help would be deeply appreciated. Thanks !
EDIT : As suggested by rioV8, I ported the script to d3.v5. A few things changed but the problem is the same. If you can give me suggestion on how to make this real-time scatterplot works, that would be amazing. I realized that there is no example of such graph on internet, only line plot. Thanks !
Here is my code.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.line {
fill: none;
stroke: #000;
stroke-width: 1.5px;
}
</style>
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v5.js"></script>
<script>
var n = 40,
random = d3.randomNormal(0, .2)
var data = [{x:10,y:Math.random()},{x:15,y:Math.random()},{x:20,y:Math.random()},{x:25,y:Math.random()}];
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 20, left: 40},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleLinear()
.domain([0, n - 1])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, 1])
.range([height, 0]);
g.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + y(0) + ")")
.call(d3.axisBottom(x));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y));
var points = svg.selectAll("dot")
.data(data)
points.enter().append("circle")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.transition()
.duration(500)
.ease(d3.easeLinear)
.on("start", tick);
points.exit().remove();
function tick() {
// Push a new data point onto the back.
data = [{x:10,y:Math.random()},{x:15,y:Math.random()},{x:20,y:Math.random()},{x:25,y:Math.random()}];
// Redraw the line.
console.log("bob")
points.data(data).enter().append("circle")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.transition()
.duration(500)
.ease(d3.easeLinear)
.on("start", tick);
}
</script>

How to fix this toggle function in d3?

I have 2 problems here:
1/ These buttons (you can run the code snippet below) should reverse back to their former opacity at the second click but they don't ! Can someone explain me what's wrong with the code ?
2/ How do I reverse back to the text former color on the second click without having to define that former color (again!) in the toggle function. ?
Explanation: As you can see when running the following snippet, the text (1,2,3...) turns white at the first click. I'd like to set it back to its former color at the second click without having to write:
var newTextFill = active ? "white" : "**former_color**";
For instance, in this example the author doesn't specify the font-size the legend should return to on a second click and yet you can see it shrinking back to its original size. How can I do the same thing here with the font colors ?
var name = ["123456789"];
var color = d3.scaleOrdinal()
.range(["#00baff", "#0014fe", "#00dbaf", "#f4bf02", "#ffa600", "#ff0000", "#ff00c4", "#ee693e", "#99958f"]);
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height",300)
var legende = svg.selectAll("bar")
.data(name)
.enter()
.append("g")
.attr("x", 0)
.attr("y", function (d,i) {return 12 + i * 20})
legende.append("text")
.text(function(d,i) { return name[i]})
.attr("class", "legtext")
.style("font-family", "helvetica")
.style("fill-opacity", 0.8)
.attr("x", 4)
.attr("y", function (d,i) {return 12 + i * 20})
.style("font-size", 10)
.style("fill", function(d,i) { return color(i)})
.style("text-decoration", "none")
.style("opacity", 1);
legende.append("rect")
.attr("class", "recta")
.attr("x", 2)
.attr("y", function (d,i) {return 1 + i * 20})
.attr("width", 65)
.attr("height", 15)
.style("fill", function(d,i) { return color(i)})
.style("opacity", 0.09);
legende.append("rect")
.attr("class", "rectastroke")
.attr("id", function(d,i) {return "str" + i})
.attr("x", 2)
.attr("y", function (d,i) {return 1 + i * 20})
.attr("width", 65)
.attr("height", 15)
.style("stroke", function(d,i) { return color(i)})
.style("stroke-width", 0.5)
.style("fill", "none")
.attr("display", "none");
legende.on("click", function (d,i)
{var active = d.active ? false : true;
var newOpacity = active ? 1 : 0.1;
var newTextFill = active ? "white" : "blue";
d3.select(this)
.select("rect")
.style("opacity", newOpacity)
d3.select(this)
.select("text")
.style("fill", newTextFill)
.raise()
d.active = active });
legende.on("mouseover", function(d, i)
{d3.select(this).selectAll(".rectastroke").attr("display", "true")});
legende.on("mouseout", function(d, i)
{d3.select(this).selectAll(".rectastroke").attr("display", "none")});
body {font-family: 'Open Sans', sans-serif;
font-size: 11px;
font-weight: 300;
fill: #242424;
text-align: center;
cursor: default;}
.legende {cursor: pointer;}
.recta {cursor: pointer;}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<div class="colored buttons"></div>
</body>
</html>
In the bl.ocks you linked she is, in fact, specifying the font size the legend will return to on a second click... in the CSS.
What's happening is that if active is false, the setter will return undefined...
d3.select(this)
.style("font-size", function() {
if (active) {return "25px"}
})
... and the UA will set the style based on the CSS.
You can do the same thing to your rectangles without specifying any newOpacity:
d3.select(this)
.select("rect")
.style("opacity", function() {
if (active) return 1;
})
Here is the demo:
var data = "123456789".split("").map(function(d) {
return {
value: d
}
});
var color = d3.scaleOrdinal()
.range(["#00baff", "#0014fe", "#00dbaf", "#f4bf02", "#ffa600", "#ff0000", "#ff00c4", "#ee693e", "#99958f"]);
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 300)
var legende = svg.selectAll("bar")
.data(data)
.enter()
.append("g")
.attr("x", 0)
.attr("y", function(d, i) {
return 12 + i * 20
})
legende.append("text")
.text(function(d, i) {
return d.value
})
.attr("class", "legtext")
.style("font-family", "helvetica")
.style("fill-opacity", 0.8)
.attr("x", 4)
.attr("y", function(d, i) {
return 12 + i * 20
})
.style("font-size", 10)
.style("fill", function(d, i) {
return color(i)
})
.style("text-decoration", "none")
.style("opacity", 1);
legende.append("rect")
.attr("class", "recta")
.attr("x", 2)
.attr("y", function(d, i) {
return 1 + i * 20
})
.attr("width", 65)
.attr("height", 15)
.style("fill", function(d, i) {
return color(i)
});
legende.on("click", function(d, i) {
var active = d.active ? false : true;
d3.select(this)
.select("rect")
.style("opacity", function() {
if (active) return 1;
})
d3.select(this)
.select("text")
.style("fill", function(e, j) {
if (active) {
return "white";
} else {
return color(i)
}
})
.raise()
d.active = active
});
.legende {
cursor: pointer;
}
.recta {
opacity: 0.1;
cursor: pointer;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
For the text color, however, since you are not specifying it in the CSS, you'll have to use a normal if...else code.

d3.js brush with multiline chart

I'm making a multi-line chart and using a brush to select time periods. It's broadly based on Mike Bostock's example at http://bl.ocks.org/mbostock/1667367
My chart is at http://lowercasen.com/dev/d3/general/piezobrush.html
My problem is in selecting the multiple lines in my 'focus' area to apply the brush to. I've nested the data based on a key, so the data is within a function. Because the function that calls my brush is outside that function, it can't access the data and I'm getting a TypeError: undefined is not an object (evaluating 'data.length')
Here's the code that nests the data:
dataNest.forEach(function(d, i) {
focus.append("path")
.attr("class", "line")
.attr("id", d.key.replace(/\s+/g, '')) //the replace stuff is getting rid of spaces
.attr("d", levelFocus(d.values));
context.append("path")
.attr("class", "line")
.attr("id", d.key.replace(/\s+/g, '')) //the replace stuff is getting rid of spaces
.attr("d", levelContext(d.values));
and at the bottom I have the function for the brush:
function brushed() {
xFocus.domain(brush.empty() ? xContext.domain() : brush.extent());
focus.selectAll(".line").attr("d", levelFocus(d.values));
focus.select(".x.axis").call(xAxisFocus);
}
It works fine for the x axis (if I comment out the line where I'm trying to select the lines) but I don't know how to select the lines correctly.
Apologies for any garbled syntax or confusing language, my coding skills are basic at best.
Any help is greatly appreciated, I've searched for hours for a solution.
Here's the full code as requested by Lars
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Multiline with brush</title>
<script src="http://d3js.org/d3.v3.js"></script>
<script src="d3/tooltip.js"></script>
<link href="styles/evidentlySoCharts.css" rel="stylesheet">
<meta name="viewport" content="initial-scale=1">
<style>
svg {
font: 10px sans-serif;
}
path {
stroke-width: 1;
fill: none;
}
#Stream1, #Nebo1D {
stroke: #009390;
}
#Stream1Legend, #Nebo1DLegend {
fill: #009390;
}
#Stream2, #Nebo2D {
stroke: #8dc63f;
}
#Stream2Legend, #Nebo2DLegend {
fill: #8dc63f;
}
#Stream3, #Nebo1S {
stroke: #132d46;
}
#Stream3Legend, #Nebo1SLegend {
fill: #132d46;
}
#Stream4, #Nebo2S {
stroke: #aaa813;
}
#Stream4Legend, #Nebo2SLegend {
fill: #aaa813;
}
#Stream5, #Nebo3 {
stroke: #619dd4;
}
#Stream5Legend, #Nebo3Legend {
fill: #619dd4;
}
.pn1d, .pn2d {
fill: none;
clip-path: url(#clip);
}
.pn1d {
stroke: #009390;
}
.pn2d {
stroke: #1b4164;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
stroke-width: 1px;
shape-rendering: crispEdges;
}
.brush .extent {
stroke: #fff;
fill-opacity: .125;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<script>
var marginFocus = {top: 10, right: 10, bottom: 250, left: 40},
marginContext = {top: 430, right: 10, bottom: 170, left: 40},
width = 960 - marginFocus.left - marginFocus.right,
heightFocus = 650 - marginFocus.top - marginFocus.bottom,
heightContext = 650 - marginContext.top - marginContext.bottom;
legendOffset = 550;
var parseDate = d3.time.format("%d/%m/%y %H:%M").parse;
var xFocus = d3.time.scale().range([0, width]),
xContext = d3.time.scale().range([0, width]),
yFocus = d3.scale.linear().range([heightFocus, 0]),
yContext = d3.scale.linear().range([heightContext, 0]);
var xAxisFocus = d3.svg.axis().scale(xFocus).orient("bottom"),
xAxisContext = d3.svg.axis().scale(xContext).orient("bottom"),
yAxisFocus = d3.svg.axis().scale(yFocus).orient("left");
var levelFocus = d3.svg.line()
.interpolate("linear")
.x(function(d) { return xFocus(d.date); })
.y(function(d) { return yFocus(d.level); });
var levelContext = d3.svg.line()
.interpolate("linear")
.x(function(d) { return xContext(d.date); })
.y(function(d) { return yContext(d.level); });
var svg = d3.select("body").append("svg")
.attr("width", width + marginFocus.left + marginFocus.right)
.attr("height", heightFocus + marginFocus.top + marginFocus.bottom);
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", heightFocus);
var focus = svg.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + marginFocus.left + "," + marginFocus.top + ")");
var context = svg.append("g")
.attr("class", "context")
.attr("transform", "translate(" + marginContext.left + "," + marginContext.top + ")");
d3.csv("data/PiezoNeboNestSimple.csv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.level = +d.level;
});
xFocus.domain(d3.extent(data.map(function(d) { return d.date; })));
yFocus.domain([d3.min(data.map(function(d) { return d.level; })) -2,0]);
xContext.domain(xFocus.domain());
yContext.domain(yFocus.domain());
// Nest the entries by piezo
var dataNest = d3.nest()
.key(function(d) {return d.piezo;})
.entries(data);
legendSpace = width/dataNest.length; // spacing for legend // ******
var brush = d3.svg.brush()
.x(xContext)
.on("brush", brushed);
focus.selectAll("g").data(dataNest)
.enter()
.append("g")
.attr("class", "line")
.attr("id", function(d) { return d.key.replace(/\s+/g, '') }) //the replace stuff is getting rid of spaces
.append("path")
.attr("d", function(d) { return levelFocus(d.values); });
context.selectAll("g").data(dataNest)
.enter()
.append("g")
.attr("class", "line")
.attr("id", function(d) { return d.key.replace(/\s+/g, '') }) //the replace stuff is getting rid of spaces
.append("path")
.attr("d", function(d) { return levelContext(d.values); });
focus.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + heightFocus + ")")
.call(xAxisFocus);
focus.append("g")
.attr("class", "y axis")
.call(yAxisFocus);
context.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + heightContext + ")")
.call(xAxisContext);
context.append("g")
.attr("class", "x brush")
.call(brush)
.selectAll("rect")
.attr("y", -6)
.attr("height", heightContext + 7);
function brushed() {
xFocus.domain(brush.empty() ? xContext.domain() : brush.extent());
focus.selectAll(".line").attr("d", levelFocus(dataNest.values));
focus.select(".x.axis").call(xAxisFocus);
}
});
</script>
</body>
</html>
It mostly boils down to two things as far as I can see. First, the elements you're selecting to be updated are the g and not the path elements and second, you need to reference the data bound to the elements in order to set d. Both are easily fixed and the brushed function looks something like this.
function brushed() {
xFocus.domain(brush.empty() ? xContext.domain() : brush.extent());
focus.selectAll("path").attr("d", function(d) { return levelFocus(d.values); });
focus.select(".x.axis").call(xAxisFocus);
}
Complete demo here. Note that some bits are still missing, in particular the clip path to restrict the lines to the chart area. This can be copied and pasted directly from the example you've referenced though.

Integrating d3 with meteor? Trying to draw pie charts

I'm trying to draw pie charts in Meteor, but I'm very new to both d3 and Meteor and am not really understanding what is going on.
The following d3 code to draw pie charts from a csv file works for me outside of Meteor:
<!DOCTYPE html>
<meta charset="utf-8">
<link href='http://fonts.googleapis.com/css?family=Montserrat:400,700' rel='stylesheet' type='text/css'>
<style>
body {
font: 30px "Montserrat";
text-transform:uppercase;
}
svg {
padding: 10px 0 0 10px;
}
.arc {
stroke: #fff;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var radius = 150,
padding = 10;
var color = d3.scale.ordinal()
.range(["#f65c55","#c8e7ec"]);
var arc = d3.svg.arc()
.outerRadius(radius)
.innerRadius(radius - 40);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.population; });
d3.csv("data.csv", function(error, data) {
color.domain(d3.keys(data[0]).filter(function(key) { return key !== "Criteria"; }));
data.forEach(function(d) {
d.ages = color.domain().map(function(name) {
return {name: name, population: +d[name]};
});
});
var legend = d3.select("body").append("svg")
.attr("class", "legend")
.attr("width", radius * 2)
.attr("height", radius * 2)
.selectAll("g")
.data(color.domain().slice().reverse())
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * 50 + ")"; });
legend.append("rect")
.attr("width", 40)
.attr("height", 40)
.style("fill", color);
legend.append("text")
.attr("x", 50)
.attr("y", 20)
.attr("dy", ".35em")
.attr("font-size","20px")
.text(function(d) { return d; });
var svg = d3.select("body").selectAll(".pie")
.data(data)
.enter().append("svg")
.attr("class", "pie")
.attr("width", radius * 2)
.attr("height", radius * 2)
.append("g")
.attr("transform", "translate(" + radius + "," + radius + ")");
svg.selectAll(".arc")
.data(function(d) { return pie(d.ages); })
.enter().append("path")
.attr("class", "arc")
.attr("d", arc)
.style("fill", function(d) { return color(d.data.name); });
svg.append("text")
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.Criteria; });
});
</script>
I also have a Meteor template as follows that I want to draw these charts in:
<div class="tab-pane active" id="playback">
{{> playback}}
</div>
However, when I try and follow web tutorials to integrate the two, the graphs don't get drawn. Can anyone help me understand why? Thanks in advance!
EDIT: forgot to mention, data.csv looks like this:
Criteria,Disapproval, Approval
Too Fast,1,2
Too Slow,5,6
Clarity,2,3
Legibility,202070,343207
The first line is for the legend, and the rest are for 4 separate graphs.
You have to make sure that the template is rendered before you access the DOM elements by code. So put your D3 code inside a template rendered method, like this:
Template.playback.rendered = function() {
// your D3 code
}
or on the body tag e.g.:
UI.body.rendered = function() {
// your D3 code
}
Template.chart.rendered = function(){
Deps.autorun(function () {
//d3 codeing here!!
}
}
It's working for me. If you're coding without Deps.autorun() it's will not render.
Oh!! one morething at html page in you case maybe .
However for my case I using nvd3.org http://nvd3.org/livecode/index.html#codemirrorNav. And this I hope you will clearify.

d3.js: map with concentric circles emanating from dots

Been away from d3.js for a few months... and I've inherited a simple US map with features that someone else started.
The features are represented by simple dots of varying sizes.
I want to add emanating concentric circles to each dot, similar to the classic Onion example by Mike Bostock: http://bl.ocks.org/mbostock/4503672 (maybe not so ominous looking though)
I've got a block set up here: http://bl.ocks.org/mbostock/4503672
(Not sure why the states aren't rendering correctly in the block, but it probably doesn't matter for this.)
In Mike's example there is only one dot, so I'm have trouble understanding how to translate what he did to what I've got (many dots).
Here's my script:
/**
* Page initialization
*/
$(function() {
renderMap('#map-container');
});
function renderMap(container) {
var width = 960,
height = 500,
active;
var projection = d3.geo.albersUsa()
.scale(960)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var radius = d3.scale.sqrt()
.domain([0, 1e7])
.range([0, 10]);
var path2 = d3.geo.path()
.projection(projection);
// Remove svg, if already exist
d3.select(container).select('svg').remove();
var svg = d3.select(container).append("svg")
.attr("width", width)
.attr("height", height);
svg.append("rect")
.attr("width", width)
.attr("height", height);
//.on("click", reset);
var g = svg.append("g");
queue()
.defer(d3.json, "/mbostock/raw/4090846/us.json")
.defer(d3.json, "dots.json")
.await(function (error, us, centroid) {
g.append("g")
.attr("id", "states")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path)
.attr("class", "state");
//.on('click', click);
g.append('path')
.attr("id", "state-borders")
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
.attr("d", path)
.attr("class", "mesh");
var dots = g.append("g")
.attr("id", "dots")
.selectAll("path")
.data(centroid.data)
.enter().append("path")
.attr("class", "dot")
.attr("d", path2.pointRadius(function(d) { return radius(d.properties.pool); }));
}
);
}
and the key part of Mike's example for making the rings is:
setInterval(function() {
svg.append("circle")
.attr("class", "ring")
.attr("transform", "translate(" + projection([100, -8]) + ")")
.attr("r", 6)
.style("stroke-width", 3)
.style("stroke", "red")
.transition()
.ease("linear")
.duration(6000)
.style("stroke-opacity", 1e-6)
.style("stroke-width", 1)
.style("stroke", "brown")
.attr("r", 160)
.remove();
}, 750);
how do I get the rings positioned on the dots?
Review the differences between the two methods to learn a little bit more about how functional/declarative programming abstracts away the pain of iterative programming.
Approach with D3 idioms:
fiddle: http://jsfiddle.net/blakedietz/E66eT/1/
update: D3 Way
<!DOCTYPE html>
<html>
<head>
<title></title>
<style>
body {
background: #192887;
}
.graticule {
fill: none;
stroke: #fff;
stroke-width: .5px;
}
.land {
fill: #007421;
}
.dot {
fill: #c7141a;
}
.ring {
fill: none;
stroke: #c7141a;
}
</style>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<script>
var width = 960,
height = 500;
var projection = d3.geo.mercator()
.center([113, -3])
.scale(1275)
.translate([width / 2, height / 2])
.clipExtent([[0, 0], [width, height]])
.precision(.1);
var path = d3.geo.path()
.projection(projection);
var graticule = d3.geo.graticule()
.step([5, 5]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
var data = [{x:-8,y:100},{x:-10,y:110},{x: -12,y:120}];
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("class","dot")
.attr("transform",translateCircle)
.attr("r",8);
function translateCircle(datum, index)
{
return "translate(" + projection([datum.y, datum.x]) + ")";
};
setInterval(function(){
svg
.selectAll("ring")
.data(data)
.enter()
.append("circle")
.attr("class", "ring")
.attr("transform", translateCircle)
.attr("r", 6)
.style("stroke-width", 3)
.style("stroke", "red")
.transition()
.ease("linear")
.duration(6000)
.style("stroke-opacity", 1e-6)
.style("stroke-width", 1)
.style("stroke", "brown")
.attr("r", 160)
.remove();
}, 750)
d3.select(self.frameElement).style("height", height + "px");
</script>
</body>
</html>
So I didn't create a fully d3 idiomatic approach for this solution, but it will work. If you can get this to work implicitly within a svg.selectAll("circle"/"unique selection"...), etc, that would be even more awesome. I'll work on that in the mean time. Until then there's this more explicitly iterative approach.
With Mike's example you're only appending a single element to the D.O.M. in the setInterval call. In order to expedite the binding process, I've created a projection method which operates on a set of coordinates: the translateCircle will operate on a datum within a collection of coordinates allowing access to the internal attributes of each collection element.
Within each setInterval call the forEach method iterates over the collection of coordinates and then calls the same internals within the setInterval method that was called by Mike originally.
Not So D3
<style>
body {
background: #192887;
}
.graticule {
fill: none;
stroke: #fff;
stroke-width: .5px;
}
.land {
fill: #007421;
}
.dot {
fill: #c7141a;
}
.ring {
fill: none;
stroke: #c7141a;
}
</style>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<script>
var width = 960,
height = 500;
var projection = d3.geo.mercator()
.center([113, -3])
.scale(1275)
.translate([width / 2, height / 2])
.clipExtent([[0, 0], [width, height]])
.precision(.1);
var path = d3.geo.path()
.projection(projection);
var graticule = d3.geo.graticule()
.step([5, 5]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
var data = [{x:-8,y:100},{x:-10,y:110},{x: -12,y:120}];
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("class","dot")
.attr("transform",translateCircle)
.attr("r",8);
function translateCircle(datum, index)
{
return "translate(" + projection([datum.y, datum.x]) + ")";
};
setInterval(function(){
data.forEach(function(datum)
{
svg
.append("circle")
.attr("class", "ring")
.attr("transform", translateCircle(datum))
.attr("r", 6)
.style("stroke-width", 3)
.style("stroke", "red")
.transition()
.ease("linear")
.duration(6000)
.style("stroke-opacity", 1e-6)
.style("stroke-width", 1)
.style("stroke", "brown")
.attr("r", 160)
.remove();
})
}, 750)
d3.select(self.frameElement).style("height", height + "px");
</script>
</body>
</html>

Resources