I am using D3 to visualize some data that updates with time, and I have a list of (x,y) coordinates; for example:
[[0.1,0.2],
[0.3,0.4],
[0.5,0.4],
[0.7,0.2]]
I would like to draw triangles from (0,0) to each of the pairs of adjacent coordinates, for example, the first triangle would have coordinates (0,0), (0.1,0.2), (0.3,0.4) and the second triangle would have coordinates (0,0), (0.3,0.4), (0.5,0.4) and so on.
My question is whether there is a way to access "neighboring" values in D3; the D3 paradigm seems to be to pass in a function that gets access to each data value separately. So I was able to do this, but only by explicitly constructing a new data set of the triangle coordinates from the entire data set of the individual points:
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// add the graph canvas to the body of the webpage
var svg = d3.select("div#plot1").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var axis = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xsc = d3.scaleLinear()
.domain([-2, 2]) // the range of the values to plot
.range([ 0, width ]); // the pixel range of the x-axis
var ysc = d3.scaleLinear()
.domain([-2, 2])
.range([ height, 0 ]);
var closedLine = d3.line()
.x(function(d){ return xsc(d[0]); })
.y(function(d){ return ysc(d[1]); })
.curve(d3.curveLinearClosed);
function attrfunc(f,attr) {
return function(d) {
return f(d[attr]);
};
}
function doit(data)
{
var items = axis.selectAll("path.item")
.data(data);
items.enter()
.append("path")
.attr("class", "item")
.merge(items)
.attr("d", attrfunc(closedLine, "xy"))
.attr("stroke", "gray")
.attr("stroke-width", 1)
.attr("stroke-opacity", function(d) { return 1-d.age;})
.attr("fill", "gray")
.attr("fill-opacity", function(d) {return 1-d.age;});
items.exit().remove();
}
var state = {
t: 0,
theta: 0,
omega: 0.5,
A: 1.0,
N: 60,
history: []
}
d3.timer(function(elapsed)
{
var S = state;
if (S.history.length > S.N)
S.history.shift();
var dt = Math.min(0.1, elapsed*1e-3);
S.t += dt;
S.theta += S.omega * dt;
var sample = {
t: S.t,
x: S.A*(Math.cos(S.theta)+0.1*Math.cos(6*S.theta)),
y: S.A*(Math.sin(S.theta)+0.1*Math.sin(6*S.theta))
}
S.history.push(sample);
// Create triangular regions
var data = [];
for (var k = 0; k < S.history.length-1; ++k)
{
var pt1 = S.history[k];
var pt2 = S.history[k+1];
data.push({age: (S.history.length-1-k)/S.N,
xy:
[[0,0],
[pt1.x,pt1.y],
[pt2.x,pt2.y]]
});
}
doit(data);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.min.js"></script>
<div id="plot1">
</div>
You can get any point in the data array using the second argument, which is the index.
When you pass a datum to any D3 method, traditionally using a parameter named d (for datum), you are in fact using data[i], i being the current index. You can change this index to get data points before or after the current datum.
Thus, in any D3 method:
.attr("foo", function(d, i){
console.log(d)//this is the current datum
console.log(data[i])//this is the same current datum!
console.log(data[i + 1])//this is the next (adjacent) datum
});
Here is a simple snippet showing this:
var data = ["foo", "bar", "baz", "foobar", "foobaz"];
var foo = d3.selectAll("foo")
.data(data)
.enter()
.append("foo")
.attr("foo", function(d, i) {
if (data[i + 1]) {
console.log("this datum is " + d + ", the next datum is " + data[i + 1]);
}
})
<script src="https://d3js.org/d3.v4.min.js"></script>
Have a look at the if statement: we have to check if there is a data[i + 1] because, of course, the last data point has no adjacent data after it.
Here is a demo using your data array:
var svg = d3.select("svg");
var scale = d3.scaleLinear()
.domain([-1, 1])
.range([0, 150]);
var data = [
[0.1, 0.2],
[0.3, 0.4],
[0.5, 0.4],
[0.7, 0.2]
];
var triangles = svg.selectAll("foo")
.data(data)
.enter()
.append("polygon");
triangles.attr("stroke", "teal")
.attr("stroke-width", 2)
.attr("fill", "none")
.attr("points", function(d, i) {
if (data[i + 1]) {
return scale(0) + "," + scale(0) + " " + scale(data[i][0]) + "," + scale(data[i][1]) + " " + scale(data[i + 1][0]) + "," + scale(data[i + 1][1]) + " " + scale(0) + "," + scale(0);
}
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="150" height="150"></svg>
PS: I'm not using your snippet because I'm drawing the triangles using polygons, but the principle is the same.
Related
EDIT: And here is a link to a codepen of mine where I have the simpler hover functionality working.
I am new to D3 and trying to create a fairly tricky hover effect on a hexbin graph. I attached the image of the hexes below to describe my effect.
An individual hexagon in a hex graph like this (unless its on the edge) borders 6 other hexagons. My goal is that when a user hovers over a hex, the radius of both that hex, as well as the 6 surrounding hexes, increases, to give a sort of pop up effect.
Using Bostocks starter hexbin code here and adjusting it a bit (adding a radiusScale and hover effect), I made the following code snippet below that has a simpler hover effect:
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, 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 + ")");
const randomX = d3.randomNormal(width / 2, 80),
randomY = d3.randomNormal(height / 2, 80),
points = d3.range(2000).map(function() { return [randomX(), randomY()]; });
const color = d3.scaleSequential(d3.interpolateLab("white", "steelblue"))
.domain([0, 20]);
const hexbin = d3.hexbin()
.radius(20)
.extent([[0, 0], [width, height]]);
const x = d3.scaleLinear()
.domain([0, width])
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, height])
.range([height, 0]);
// radiusScale
const radiusScale = d3.scaleSqrt()
.domain([0, 10]) // domain is # elements in hexbin
.range([0, 8]); // range is mapping to pixels (or coords) for radius
g.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
g.append("g")
.attr("class", "hexagon")
.attr("clip-path", "url(#clip)")
.selectAll("path")
.data(hexbin(points))
.enter().append("path")
.attr("d", d => hexbin.hexagon(radiusScale(d.length)))
// .attr("d", hexbin.hexagon())
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.attr("fill", function(d) { return color(d.length); })
.on('mouseover', function(d) {
d3.select(this)
.attr("d", d => hexbin.hexagon(radiusScale((5+d.length)*2)))
})
.on('mouseout', function(d) {
d3.select(this)
.attr("d", d => hexbin.hexagon(radiusScale(d.length)))
})
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y).tickSizeOuter(-width));
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickSizeOuter(-height));
.hexagon {
stroke: #000;
stroke-width: 0.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
<svg width="500" height="400"></svg>
This effect only increases the radius of the single hexagon being hovered over, not also the surrounding hexagons.
To begin addressing the issue of increasing the radius of surrounding hexagons, I wrote this function that takes the binned data, an (x,y) location (center of a hexagon), and a radius that is wide enough to capture the (x,y) centers of neighbor hexagons:
// hexbinData, which was created using the hexbin() function,
// has a .x and .y value for each element, and the .x and .y values
// represent the center of that hexagon.
const findNeighborHexs = function(hexbinData, xHex, yHex, radius) {
var neighborHexs = hexbinData
.filter(row => row.x < (xHex+radius) & row.x > (xHex-radius))
.filter(row => row.y < (yHex+radius) & row.y > (yHex-radius))
return neighborHexs;
}
And here is where I'm stuck... I'm not sure how to use findNeighborHexs to (1) select those elements on hovering and (2) change those elements sizes. As a very tough (3), I think I may need to move the (x,y) centers for these neighbox hexes too to account for larger radius.
Thanks in advance for any help with this. I know this is a long post but I've got a bunch of stuff done already for this and this would be a very cool hover effect I'm working on so any help is appreciated!
Here is a slightly modified version of your code which also plays with adjacent hexagons of the hovered hexagon:
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, 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 + ")");
const randomX = d3.randomNormal(width / 2, 80),
randomY = d3.randomNormal(height / 2, 80),
points = d3.range(2000).map(function() { return [randomX(), randomY()]; });
const color = d3.scaleSequential(d3.interpolateLab("white", "steelblue"))
.domain([0, 20]);
const hexbin = d3.hexbin()
.radius(20)
.extent([[0, 0], [width, height]]);
const x = d3.scaleLinear()
.domain([0, width])
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, height])
.range([height, 0]);
// radiusScale
const radiusScale = d3.scaleSqrt()
.domain([0, 10]) // domain is # elements in hexbin
.range([0, 8]); // range is mapping to pixels (or coords) for radius
g.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
function unique(arr) {
var u = {}, a = [];
for(var i = 0, l = arr.length; i < l; ++i){
if(!u.hasOwnProperty(arr[i])) {
a.push(arr[i]);
u[arr[i]] = 1;
}
}
return a;
}
var xs = unique(hexbin(points).map(h => parseFloat(h.x))).sort(function(a,b) { return a - b;});
var ys = unique(hexbin(points).map(h => parseFloat(h.y))).sort(function(a,b) { return a - b;});
g.append("g")
.attr("class", "hexagon")
.attr("clip-path", "url(#clip)")
.selectAll("path")
.data(hexbin(points))
.enter().append("path")
.attr("id", d => xs.indexOf(d.x) + "-" + ys.indexOf(d.y))
.attr("length", d => d.length)
.attr("d", d => hexbin.hexagon(radiusScale(d.length)))
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.attr("fill", function(d) { return color(d.length); })
.on('mouseover', function(d) {
d3.select(this).attr("d", d => hexbin.hexagon(radiusScale((5 + d.length) * 2)));
var dx = xs.indexOf(d.x);
var dy = ys.indexOf(d.y);
[[-2, 0], [-1, -1], [1, -1], [2, 0], [1, 1], [-1, 1]].forEach( neighbour => {
var elmt = document.getElementById((dx + neighbour[0]) + "-" + (dy + neighbour[1]))
if (elmt) {
var elmtLength = parseInt(elmt.getAttribute("length"));
elmt.setAttribute("d", hexbin.hexagon(radiusScale(5 + elmtLength)));
}
});
})
.on('mouseout', function(d) {
d3.select(this).attr("d", d => hexbin.hexagon(radiusScale(d.length)));
var dx = xs.indexOf(d.x);
var dy = ys.indexOf(d.y);
[[-2, 0], [-1, -1], [1, -1], [2, 0], [1, 1], [-1, 1]].forEach( neighbour => {
var elmt = document.getElementById((dx + neighbour[0]) + "-" + (dy + neighbour[1]))
if (elmt) {
var elmtLength = parseInt(elmt.getAttribute("length"));
elmt.setAttribute("d", hexbin.hexagon(radiusScale(elmtLength)));
}
});
})
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y).tickSizeOuter(-width));
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickSizeOuter(-height));
.hexagon {
stroke: #000;
stroke-width: 0.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
<svg width="500" height="400"></svg>
The idea is to give each hexagon an id in order to be able to select it.
If the hexagon being hovered is the 6th from the left and the 3rd from the top, then we can give it the id #6-3.
This way when this hexagon is hovered, we can play with its adjacent hexagons by selecting them by their id, the one on its left for instance has the id #5-3.
In order to give each hexagon an id, as d3's hexbin(input) replaces our input with only the hexagons' x and y coordinates, we'll have to find find all xs and ys produced:
var xs = unique(hexbin(points).map(h => parseFloat(h.x))).sort(function(a,b) { return a - b;});
var ys = unique(hexbin(points).map(h => parseFloat(h.y))).sort(function(a,b) { return a - b;});
where unique is whatever function keeping only distinct values.
This way, our hexagons can be given an id this way:
...
.data(hexbin(points))
.enter().append("path")
.attr("id", d => xs.indexOf(d.x) + "-" + ys.indexOf(d.y))
...
Now that our hexagons have an id, we can modify our mouseover and mouseout to play with these adjacent hexagons:
Adjacent hexagons are the ones for which we need to sum x and y of the hovered hexagon by:
[[-2, 0], [-1, -1], [1, -1], [2, 0], [1, 1], [-1, 1]]
which gives for the mouseover (in addition to modifying the size of the hovered hexagon):
.on('mouseover', function(d) {
d3.select(this).attr("d", d => hexbin.hexagon(radiusScale((5 + d.length) * 2)));
var dx = xs.indexOf(d.x);
var dy = ys.indexOf(d.y);
[[-2, 0], [-1, -1], [1, -1], [2, 0], [1, 1], [-1, 1]].forEach( neighbour => {
var elmt = document.getElementById((dx + neighbour[0]) + "-" + (dy + neighbour[1]))
if (elmt) {
var elmtLength = parseInt(elmt.getAttribute("length"));
elmt.setAttribute("d", hexbin.hexagon(radiusScale(5 + elmtLength)));
}
});
})
Note that in addition to setting the id of each hexagon, we also include the length attribute in order to easily change the hovered size of hexagons.
you could amend you mouseover and mouseout functions to be the following, which selects all the hexagons and sets the size based on whether they fall within your defined radius:
.on('mouseover', function(d) {
let dx = d.x
let dy = d.y
let r = 50 //set this to be an appropriate size radius
d3.selectAll(".hexagon").selectAll("path")
.attr("d", function(f) {
if ((f.x < (dx + r) & f.x > (dx - r)) & (f.y < (dy + r) & f.y > (dy - r))) {
return hexbin.hexagon(radiusScale((5+f.length)*2))
}
else {
return hexbin.hexagon(radiusScale((f.length)))
}
})
})
.on('mouseout', function(d) {
d3.selectAll(".hexagon").selectAll("path")
.attr("d", d => hexbin.hexagon(radiusScale(d.length)))
})
I created a jsfiddle here.
I do have a graph - in this case a sine wave - and want to move a circle along this line (triggered by a click event), stop at certain x and y value pairs that are on this graph and then move on to the last point of the graph from where it jumps to the first again (ideally this should go on until I press a stop button).
My current problem is that the circle only moves horizontally but not in the ordinate direction and also the delay is visible only once (in the very beginning).
The relevant code is this one (the entire running example can be found in the link above):
Creation of the circle:
// the circle I want to move along the graph
var circle = svg.append("circle")
.attr("id", "concindi")
.attr("cx", x_scale(xval[0]))
.attr("cy", y_scale(yval[0]))
.attr("transform", "translate(" + (0) + "," + (-1 * padding + 15) + ")")
.attr("r", 6)
.style("fill", 'red');
The moving process:
var coordinates = d3.zip(xval, yval);
svg.select("#concindi").on("click", function() {
coordinates.forEach(function(ci, indi){
//console.log(ci[1] + ": " + indi);
//console.log(coordinates[indi+1][1] + ": " + indi);
if (indi < (coordinates.length - 1)){
//console.log(coordinates[indi+1][1] + ": " + indi);
console.log(coordinates[indi + 1][0]);
console.log(coordinates[indi + 1][1]);
d3.select("#concindi")
.transition()
.delay(2000)
.duration(5000)
.ease("linear")
.attr("cx", x_scale(coordinates[indi + 1][0]))
.attr("cy", y_scale(coordinates[indi + 1][1]));
}
});
I am pretty sure that I use the loop in a wrong manner. The idea is to start at the first x/y pair, then move to the next one (which takes 5s), wait there for 2s and move on to the next and so on. Currently, the delay is only visible initially and then it just moves horizontally.
How would this be done correctly?
Why don't you use Bostock's translateAlong function?
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
Here is the demo:
// function to generate some data
function get_sin_val(value) {
return 30 * Math.sin(value * 0.25) + 35;
}
var width = 400;
var height = 200;
var padding = 50;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var xrange_min = 0;
var xrange_max = 50;
var yrange_min = 0;
var yrange_max = 100;
var x_scale = d3.scale.linear()
.domain([xrange_min, xrange_max])
.range([padding, width - padding * 2]);
var y_scale = d3.scale.linear()
.domain([yrange_min, yrange_max])
.range([height - padding, padding]);
// create the data
var xval = d3.range(xrange_min, xrange_max, 1);
var yval = xval.map(get_sin_val);
// just for convenience
var coordinates = d3.zip(xval, yval);
//defining line graph
var lines = d3.svg.line()
.x(function(d) {
return x_scale(d[0]);
})
.y(function(d) {
return y_scale(d[1]);
})
.interpolate("linear");
//draw graph
var sin_graph = svg.append("path")
.attr("d", lines(coordinates))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
// the circle I want to move along the graph
var circle = svg.append("circle")
.attr("id", "concindi")
.attr("transform", "translate(" + (x_scale(xval[0])) + "," + (y_scale(yval[0])) + ")")
.attr("r", 6)
.style("fill", 'red');
svg.select("#concindi").on("click", function() {
d3.select(this).transition()
.duration(5000)
.attrTween("transform", translateAlong(sin_graph.node()));
});
// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
You have to understand that forEach will loop to the end of the array almost instantaneously. Thus, you cannot make the circle jumping to one coordinate to the other with your approach right now (thus, unfortunately, you are correct here:"I am pretty sure that I use the loop in a wrong manner").
If you want to add the 2s waiting period between one point and another, the best idea is chaining the transitions. Something like this (I'm reducing the delay and the duration times in the demo, so we can better see the effect):
var counter = 0;
transit();
function transit() {
counter++;
d3.select(that).transition()
.delay(500)
.duration(500)
.attr("transform", "translate(" + (x_scale(coordinates[counter][0]))
+ "," + (y_scale(coordinates[counter][1])) + ")")
.each("end", transit);
}
Here is the demo:
// function to generate some data
function get_sin_val(value) {
return 30 * Math.sin(value * 0.25) + 35;
}
var width = 400;
var height = 200;
var padding = 50;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var xrange_min = 0;
var xrange_max = 50;
var yrange_min = 0;
var yrange_max = 100;
var x_scale = d3.scale.linear()
.domain([xrange_min, xrange_max])
.range([padding, width - padding * 2]);
var y_scale = d3.scale.linear()
.domain([yrange_min, yrange_max])
.range([height - padding, padding]);
// create the data
var xval = d3.range(xrange_min, xrange_max, 1);
var yval = xval.map(get_sin_val);
// just for convenience
var coordinates = d3.zip(xval, yval);
//defining line graph
var lines = d3.svg.line()
.x(function(d) {
return x_scale(d[0]);
})
.y(function(d) {
return y_scale(d[1]);
})
.interpolate("linear");
//draw graph
var sin_graph = svg.append("path")
.attr("d", lines(coordinates))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
// the circle I want to move along the graph
var circle = svg.append("circle")
.attr("id", "concindi")
.attr("transform", "translate(" + (x_scale(xval[0])) + "," + (y_scale(yval[0])) + ")")
.attr("r", 6)
.style("fill", 'red');
svg.select("#concindi").on("click", function() {
var counter = 0;
var that = this;
transit();
function transit() {
counter++;
d3.select(that).transition()
.delay(500)
.duration(500)
.attr("transform", "translate(" + (x_scale(coordinates[counter][0])) + "," + (y_scale(coordinates[counter][1])) + ")")
.each("end", transit);
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I am trying to display a grid on a world map where each grid cell is filled with a color based on some data (e.g., temperature or humidity). I am trying to adapt the simple world map example here: http://techslides.com/demos/d3/d3-worldmap-boilerplate.html
I thought I might be able to use the built-in d3 graticule and add a fill color, like this:
g.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path)
.style("fill", function(d, i) { return color(Math.floor((Math.random() * 20) + 1)); });
That doesn't work, though. Is there a way to fill in the grid cells generated by graticule? If not, what's the best way to go about overlaying a lat,long grid on the map with filled cells?
I created d3-grid-map to solve a specific problem of placing sparse global 0.5 degree grid cells on a d3 map by drawing on canvas layers. It should support other grid sizes with some effort. It handles a couple of forms of javascript typed array inputs, but it could use more generalization.
To do something like this, first create data set with all the N/S/E/W to define the limits.
var data set = [{W: -5.0, N: 50.0, E: 10.0, S: 40.0 }, {W: -95.0, N: 50.0, E: -40.0, S: 40.0 }];
Next post you load your world JSON add the path like this.
d3.json("http://techslides.com/demos/d3/data/world-topo.json", function(error, world) {
var countries = topojson.feature(world, world.objects.countries).features;
topo = countries;
draw(topo);
//iterate over the dataset created above for making paths.
dataset.forEach(function(bb){
var arc = d3.geo.graticule()
.majorExtent([[bb.W, bb.S], [bb.E, bb.N]])
//this will append the path to the g group so that it moves accordingly on translate/zoom
g.append("path")
.attr("class", "arc")
.attr("d", path(arc.outline()));
});
});
On Css add:
.arc {
fill: red;[![enter image description here][1]][1]
fill-opacity: 0.3;
stroke: black;
stroke-opacity: 0.5;
}
Full JS here:
d3.select(window).on("resize", throttle);
var zoom = d3.behavior.zoom()
.scaleExtent([1, 8])
.on("zoom", move);
var width = document.getElementById('container').offsetWidth-60;
var height = width / 2;
var dataset = [{W: -5.0, N: 50.0, E: 10.0, S: 40.0 }, {W: -95.0, N: 50.0, E: -40.0, S: 40.0 }];
var topo,projection,path,svg,g;
var tooltip = d3.select("#container").append("div").attr("class", "tooltip hidden");
setup(width,height);
function setup(width,height){
projection = d3.geo.mercator()
.translate([0, 0])
.scale(width / 2 / Math.PI);
path = d3.geo.path()
.projection(projection);
svg = d3.select("#container").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.call(zoom);
g = svg.append("g");
}
d3.json("http://techslides.com/demos/d3/data/world-topo.json", function(error, world) {
var countries = topojson.feature(world, world.objects.countries).features;
topo = countries;
draw(topo);
dataset.forEach(function(bb){
var arc = d3.geo.graticule()
.majorExtent([[bb.W, bb.S], [bb.E, bb.N]])
g.append("path")
.attr("class", "arc")
.attr("d", path(arc.outline()));
});
});
function draw(topo) {
var country = g.selectAll(".country").data(topo);
country.enter().insert("path")
.attr("class", "country")
.attr("d", path)
.attr("id", function(d,i) { return d.id; })
.attr("title", function(d,i) { return d.properties.name; })
.style("fill", function(d, i) { return d.properties.color; });
//ofsets plus width/height of transform, plsu 20 px of padding, plus 20 extra for tooltip offset off mouse
var offsetL = document.getElementById('container').offsetLeft+(width/2)+40;
var offsetT =document.getElementById('container').offsetTop+(height/2)+20;
//tooltips
country
.on("mousemove", function(d,i) {
var mouse = d3.mouse(svg.node()).map( function(d) { return parseInt(d); } );
tooltip
.classed("hidden", false)
.attr("style", "left:"+(mouse[0]+offsetL)+"px;top:"+(mouse[1]+offsetT)+"px")
.html(d.properties.name)
})
.on("mouseout", function(d,i) {
tooltip.classed("hidden", true)
});
}
function redraw() {
width = document.getElementById('container').offsetWidth-60;
height = width / 2;
d3.select('svg').remove();
setup(width,height);
draw(topo);
}
function move() {
var t = d3.event.translate;
var s = d3.event.scale;
var h = height / 3;
t[0] = Math.min(width / 2 * (s - 1), Math.max(width / 2 * (1 - s), t[0]));
t[1] = Math.min(height / 2 * (s - 1) + h * s, Math.max(height / 2 * (1 - s) - h * s, t[1]));
zoom.translate(t);
g.style("stroke-width", 1 / s).attr("transform", "translate(" + t + ")scale(" + s + ")");
}
var throttleTimer;
function throttle() {
window.clearTimeout(throttleTimer);
throttleTimer = window.setTimeout(function() {
redraw();
}, 200);
}
Image:
I'm building a set of bar charts that will be updated dynamically with json data.
There will be occasions where the x.domain value is equal to zero or null so in those cases I don't want to draw the rectangle, and would like the overall height of my chart to adjust. However, the bars are being drawn based on data.length which may contain 9 array values, but some of those values are zeros, but render a white space within the graph.
I've attached an image of what is happening. Basically there are 9 data entries and only one of those actually contains the positive value, but the bars are still being drawn for all 9 points.
Here is my code:
d3.json("data/sample.json", function(json) {
var data = json.cand;
var margin = {
top: 10,
right: 20,
bottom: 30,
left: 0
}, width = parseInt(d3.select('#graphic').style('width'), 10),
width = width - margin.left - margin.right -50,
bar_height = 55,
num_bars = (data.length),
bar_gap = 18,
height = ((bar_height + bar_gap) * num_bars);
var x = d3.scale.linear()
.range([0, width]);
var y = d3.scale.ordinal()
.range([height, 0]);
var svg = d3.select('#graphic').append('svg')
.attr('width', width + margin['left'] + margin['right'])
.attr('height', height + margin['top'] + margin['bottom'])
.append('g')
.attr('transform', 'translate(' + margin['left'] + ',' + margin['top'] + ')');
var dx = width;
var dy = (height / data.length) + 8;
// set y domain
x.domain([0, d3.max(data, function(d) {
return d.receipts;
})]);
y.domain(0, d3.max(data.length))
.rangeBands([0, data.length * bar_height ]);
var xAxis = d3.svg.axis().scale(x).ticks(2)
.tickFormat(function(d) {
return '$' + formatValue(d);
});
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
.ticks(0)
.tickFormat(function(d) {
return '';
});
// set height based on data
height = y.rangeExtent()[1] +12;
d3.select(svg.node().parentNode)
.style('height', (height + margin.top + margin.bottom) + 'px');
// bars
var bars = svg.selectAll(".bar")
.data (data, function (d) {
if (d.receipts >= 1) {
return ".bar";
} else {
return null;
}
})
.enter().append('g');
bars.append('rect')
.attr("x", function(d, i) {
return 0;
})
.attr("y", function(d, i) {
if (d.receipts >= 1) {
return i * (bar_height + bar_gap - 4);
}else {
return null;
}
})
.attr("width", function(d, i) {
if (d.receipts >= 1) {
return dx * d.receipts;
} else {
return 0;
}
});
//y and x axis
svg.append('g')
.attr('class', 'x axis bottom')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis.orient('bottom'));
svg.append('g')
.call(yAxis.orient('left'));
});
I’d like to select a node in a callback without using d3.select(this).
I have some code that draws a pie…
function drawPie(options) {
options || (options = {});
var data = options.data || [],
element = options.element,
radius = options.radius || 100,
xOffset = Math.floor(parseInt(d3.select(element).style('width'), 10) / 2),
yOffset = radius + 20;
var canvas = d3.select(element)
.append("svg:svg")
.data([data])
.attr("width", options.width)
.attr("height", options.height)
.append("svg:g")
.attr("transform", "translate(" + xOffset + "," + yOffset + ")");
var arc = d3.svg.arc()
.outerRadius(radius);
var pie = d3.layout.pie()
.value(function(data) {
return data.percentageOfSavingsGoalValuation;
});
var arcs = canvas.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice");
arcs.append("svg:path")
.on("mouseover", divergeSlice);
You’ll notice at the end I have a call to divergeSlice(). That looks like this:
function divergeSlice(datum, index) {
var angle = (datum.endAngle + datum.startAngle) / 2,
x = Math.sin(angle) * 10,
y = -Math.cos(angle) * 10;
d3.select(this)
.transition()
.attr("transform", "translate(" + x + ", " + y + ")");
}
This works, but I’d like to accomplish this without using this as I mentioned earlier. When I log the datum object, I get something like the following:
{
data: {
uniqueID: "XX00X0XXXX00"
name: "Name of value"
percentageOfValuation: 0.4
totalNetAssetValue: 0
}
endAngle: 5.026548245743669
innerRadius: 80
outerRadius: 120
startAngle: 2.5132741228718345
value: 0.4
}
How could I use d3.select() to find a path that holds datum.data.uniqueID that is equal to "XX00X0XXXX00"?
You can't do this directly with .select() as that uses DOM selectors. What you can do is select all the candidates and then filter:
d3.selectAll("g")
.filter(function(d) { return d.data.uniqueID === myDatum.data.uniqueID; });
However, it would be much easier to simply assign this ID as an ID to the DOM element and then select based on that:
var arcs = canvas.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("id", function(d) { return d.data.uniqueID; })
.attr("class", "slice");
d3.select("#" + myDatum.data.uniqueID);