I'm trying to position labels on map overlapping-free by using using d3fc-label-label.js in combination with d3.js. While labeling the map by basic d3 functions works well, the approach with the help of d3fc-label-label.js (heavily inspired by this example) produces a map with all the labels placed in top left corner.
Here's the javascript part that does the job
var width = 1300,
height = 960;
var projection = d3.geoMercator()
.scale(500)
// Center the Map to middle of shown area
.center([10.0, 50.5])
.translate([width / 2, height / 2]);
// ??
var path = d3.geoPath()
.projection(projection)
.pointRadius(2);
// Set svg width & height
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// var g = svg.append("g");
d3.json("europe_wgs84.geojson", function(error, map_data) {
if (error) return console.error(error);
// var places = topojson.feature(map_data, map_data.objects.places);
// "path" instead of ".subunit"
svg.selectAll("path")
.data(map_data.features)
.enter().append("path")
.attr("d", path)
.attr("class", function(d) { return "label " + d.id})
var labelPadding = 2;
// the component used to render each label
var textLabel = fc.layoutTextLabel()
.padding(labelPadding)
//.value(function(d) { return map_data.properties.iso; });
.value(function(d) { return d.properties.iso; });
// use simulate annealing to find minimum overlapping text label positions
var strategy = fc.layoutGreedy();
// create the layout that positions the labels
var labels = fc.layoutLabel(strategy)
.size(function(_, i, g) {
// measure the label and add the required padding
var textSize = d3.select(g[i])
.select('text')
.node()
.getBBox();
return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
})
.position(function(d) { return projection(d.geometry.coordinates); })
.component(textLabel);
// render!
svg.datum(map_data.features)
.call(labels);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.0/d3.min.js"></script>
See the gist that includes the data and a HTML file.
I would guess the issue is related to append the labels correctly to path of the map. Sadly, I haven't figured it out and would greatly appreciate any help!
I believe the problem lies in the fact that you are not passing single coordinates as the label's position.
layoutLabel.position(accessor)
Specifies the position for each item in the associated array. The
accessor function is invoked exactly once per datum, and should return
the position as an array of two values, [x, y].
In the example you show, that you are basing the design on, the variable places contains point geometries, it is to these points that labels are appended. Looking in the topojson we find places looking like:
"places":{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[5868,5064],"properties":{"name":"Ayr"}},{"type":"Point","coordinates":[7508,6637],"properties":{"name":"Aberdeen"}},{"type":"Point","coordinates":[6609,5933],"properties":{"name":"Perth"}},...
Note that geometries.coordinates of each point contains one coordinate. However, in your code, d.geometry.coordinates contains an array of coordinates as it contains the boundary points of the entire path of each feature. This will cause errors in label placement. Instead, you might want to use path.centroid(d), this will return a single coordinate that is at the center of each country/region/path. Placement might not be perfect, as an extreme example, a series of countries arranged as concentric rings will have the same centroid. Here is a basic block showing placement using path.centroid (this shows only the placement - not the formatting of the labels as I'm not familiar with this library extension).
If you were wondering why the linked example's regional labels appear nicely, in the example each region has a label appended at its centroid, bypassing d3fc-label-layout altogether:
svg.selectAll(".subunit-label")
.data(subunits.features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id; })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; });
Related
I'm rolling through Mike Bostock's unbelievable collection of examples, and I am currently trying to get my own version of Pie Update II working (http://bl.ocks.org/mbostock/1346410). I've co-mingled with code shown here (http://jonsadka.com/blog/how-to-create-adaptive-pie-charts-with-transitions-in-d3/) to help me better understand what's actually going on. By this, I mean I've been tweaking the mbostock example by individually adding bits of code from the other example to see what works and what doesn't.
Regardless, I have a simple HTML page set up with radio buttons declared like so:
<form>
<label><input type="radio" name="dataset" value="photoCounts2" checked> Photos</label>
<label><input type="radio" name="dataset" value="videoCounts2"> Videos</label>
</form>
I have an array of integers of equal size (10), named videoCounts2 and photoCounts2. I'm trying to be able to click on a radio button and have the pie chart adjust to the corresponding dataset. I don't require a smooth transition just yet, I'd first like to get the initial transition working. Here is the JavaScript:
/* ----------------------------------------------------------------------- */
// Global vars
/* ----------------------------------------------------------------------- */
var width = 960,
height = 500,
radius = Math.min(width, height) / 2;
/* ----------------------------------------------------------------------- */
// Data Agnostic - Setup page elements
/* ----------------------------------------------------------------------- */
var color = d3.scale.category20();
var pie = d3.layout.pie()
.value(function(d) { return d; })
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 20);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
/* ----------------------------------------------------------------------- */
// Data Dependent
/* ----------------------------------------------------------------------- */
var path = g.datum(photoCounts2).selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; }); // store the initial angles
d3.selectAll("input")
.on("change", change);
function change() {
console.log("made it to change");
data = ((this.value === "photoCounts2") ? photoCounts2 : videoCounts2); // This needs to be changed if the dataset is different than video count.
console.log(this.value);
// Transition the pie chart
g.datum(data).selectAll("path")
.data(pie)
.transition()
.attr("d", arc);
// Add any new data
g.datum(data).selectAll("path")
.data(pie)
.enter()
.append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; }); // store the initial angles
// Remove any unused data
g.datum(data).selectAll("path")
.data(pie)
.exit()
.remove();
}
I actually get the desired functionality, and nothing visually breaks, but I continually get the following error printed to the console when I switch between datasets (by choosing a different radio input):
Error: Invalid value for <path> attribute d="M1.4083438190194563e-14,-230A230,230 0 0.000006912,1 217.6110300447649,-74.45790482200897L141.92023698571626,-48.559503144788465A150,150 0 0.000006912,0 9.184850993605149e-15,-150Z"
(anonymous function) # d3.js:8717
tick # d3.js:8914
(anonymous function) # d3.js:8906
d3_timer_mark # d3.js:2159
d3_timer_step # d3.js:2139
I truly believe I've done my due diligence looking elsewhere for solutions, but I'm stumped. Any help is greatly appreciated!
You're seeing the error because some of the intermediate d values for the paths that are generated by the transition are invalid. As each of these is only visible for a few milliseconds, you don't actually see the error.
The root cause of the problem is that D3's default transition can't interpolate pie chart segments correctly. To make it work properly, you need to use a custom attribute tween, e.g. as in this example:
path.transition().duration(750).attrTween("d", arcTween);
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
This tells D3 how to generate all the intermediate values; in this case, by interpolating the angle of the segment and using the arc generator to generate the path for the changed angle (as opposed to interpolating the path itself without knowledge on how it was generated in the first place).
I am trying to build a bar graph that I can switch between the amount of data displayed based on a particular length of time. So far the code that I have is this,
var margin = {
top : 20,
right : 20,
bottom : 30,
left : 50
}, width = 960 - margin.left - margin.right, height = 500
- margin.top - margin.bottom;
var barGraph = function(json_data, type) {
if (type === 'month')
var barData = monthToArray(json_data);
else
var barData = dateToArray(json_data);
var y = d3.scale.linear().domain([ 0, Math.round(Math.max.apply(null,
Object.keys(barData).map(function(e) {
return barData[e]['Count']}))/100)*100 + 100]).range(
[ height, 0 ]);
var x = d3.scale.ordinal().rangeRoundBands([ 0, width ], .1)
.domain(d3.entries(barData).map(function(d) {
return barData[d.key].Date;
}));
var xAxis = d3.svg.axis().scale(x).orient("bottom");
var yAxis = d3.svg.axis().scale(y).orient("left");
var svg = d3.select("#chart").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 + ")");
svg.append("g").attr("class", "x axis").attr("transform",
"translate(0," + height + ")").call(xAxis);
svg.append("g").attr("class", "y axis").call(yAxis).append(
"text").attr("transform", "rotate(-90)").attr("y", 6)
.attr("dy", ".71em").style("text-anchor", "end").text(
"Total Hits");
svg.selectAll(".barComplete").data(d3.entries(barData)).enter()
.append("rect").attr("class", "barComplete").attr("x",
function(d) {
return x(barData[d.key].Date)
}).attr("width", x.rangeBand() / 2).attr("y",
function(d) {
return y(barData[d.key].Count);
}).attr("height", function(d) {
return height - y(barData[d.key].Count);
}).style("fill", "orange");
var bar = svg.selectAll(".barHits").data(d3.entries(barData))
.enter().append("rect").attr("class", "barHits").attr(
"x", function(d) {
return x(barData[d.key].Date) + x.rangeBand() / 2
}).attr("width", x.rangeBand() / 2).attr("y",
function(d) {
return y(barData[d.key].Count);
}).attr("height", function(d) {
return height - y(barData[d.key].Count);
}).style("fill", "red");
};
This does a great job displaying my original data set, I have a button set up to switch between the data sets which are all drawn at the beginning of the page. all of the arrays exist but no matter how hard I try I keep appending a new graph to the div so after the button click I have two graphs, I have tried replacing all of the code in the div, using the enter() exit() remove() functions but when using these I get an error stating that there is no exit() function I got this by following a post that someone else had posted here and another one here but to no avail. Any ideas guys?
What you are most likely doing is drawing a new graph probably with a new div each time the button is clicked, one thing you can try doing is to hold on to the charts container element somewhere, and when the button is clicked, you simply clear it's children and re-draw the graphs.
In practice, I almost never chain .data() and .enter().
// This is the syntax I prefer:
var join = svg.selectAll('rect').data(data)
var enter = join.enter()
/* For reference: */
// Selects all rects currently on the page
// #returns: selection
svg.selectAll('rect')
// Same as above but overwrites data of existing elements
// #returns: update selection
svg.selectAll('rect').data(data)
// Same as above, but now you can add rectangles not on the page
// #returns: enter selection
svg.selectAll('rect').data(data).enter()
Also important:
# selection.enter()
The enter selection merges into the update selection when you append or insert.
Okay, I figured it out So I will first direct all of your attention to here Where I found my answer. The key was in the way I was drawing the svg element, instead of replacing it with the new graph I was just continually drawing a new one and appending it to the #chart div. I found the answer that I directed you guys to and added this line in the beginning of my existing barGraph function.
d3.select("#barChart").select("svg").remove();
and it works like a charm, switches the graphs back and forth just as I imagined it would, now I have a few more tasks for the bargraph itself and I can move on to another project.
Thank you all for all of your help and maybe I can return the favor one day!
I have the following plunkr: http://plnkr.co/edit/FLSz6swyiDuNchTYo2Xf?p=preview
Inside the plunkr you'll see that I'm trying to build a graph with 1+ paths/lines on which I'm moving some circle elements on mouse move. To accommodate the X and Y axes I need some margin. When I translate the paths/lines on X, to make room for the X axis, then the circles that move along the paths/lines aren't following the correct path anymore.
The lines are added directly to the svg element and their definition looks like this:
var line = d3.svg.line()
.x(function (d, i) {
//return margin.left + xScale(i);
return xScale(i);
})
.y(function (d) {
return margin.top + yScale(d);
// return yScale(d);
})
.interpolate("cardinal");
Does anybody have an idea why?
Instead of
var lines = svg.selectAll(".gLine")
...
you want to add the lines to the group you created and (correctly) translated:
var lines = lineGroup.selectAll(".gLine")
Similarly, you need to move the circles into a translated group:
var circles = lineGroup.selectAll("circle")
This "reuses" lineGroup, which is fine, or you can create a "circleGroup" if you'd like.
That would fix the representation, but the captured mouse coordinates would still be offset. One way to fix it is to adjust the x of the captured mouse position:
mouseUnderlay.on("mousemove", function () {
var x = d3.mouse(this)[0] - margin.left;
Here it is working
I need to show value at corresponding place while mouseover a line/bar chart using d3.js
var toolTip = svg.selectAll("path")
.append("svg:title")
.text(getmouseoverdata(data)
);
function getmouseoverdata(d) {
return d;
}
Here i get all the data in the array while mouseover at any place in the graph.
But I want to show the value at corresponding place. How can I achieve it?
You can use d3js mouse event handler like this
var coordinates = [0, 0];
coordinates = d3.mouse(this);
var x = coordinates[0];
var y = coordinates[1];
It will provide you the current mouse coordinates.
If you're only looking to display the data elements when you mouseover the path/rect elements, you could try to add the titles directly onto those elements while they are being created?
For example:
var data = [0, 1, 2, 3, 4, 5],
size = 300;
var canvas = d3.select("body")
.append("svg:svg")
.attr("width", size)
.attr("height", size);
var pixels = size / data.length;
canvas.selectAll("rect").data(data).enter().append("svg:rect")
.attr("fill", "red")
.attr("height", function(d){return d * pixels})
.attr("width", pixels/2)
.attr("x", function(d,i){return i * pixels})
.attr("y", 0)
.append("title") //Adding the title element to the rectangles.
.text(function(d){return d});
This code should create five rectangles with their data element present in a tooltip if you mouseover the rectangle.
Hope this helps.
Edit based on comment:
For a bottom to top graph, you can change the y attribute like so:
.attr("y", function(d){return size - d * pixels})
This addition will cause the bar to start at the difference between the maxHeight of your graph and the size of the bar, effectively turning a top-to-bottom graph into a bottom-to-top graph.
I have a chart with some circles on it. When the user hovers over a circle, I want to create a mouseover event and pass the x and y coordinates of the center of that circle. How do I do that?
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return x(d.number); })
.attr("cy", function(d) { return y(d.area); })
.call(d3.my_helper.tooltip(function(d, i){return "Area: "+ d.area;}));
d3.my_helper.tooltip = function(accessor){
return function(selection){
var circle_x = ???; // the center of the circle
var circle_y = ???; // the center of the circle
selection.on("mouseover", function(d, i){
// do stuff here with circle_x and circle_y
});
};
};
.on('mouseover', function(d) {
var target_x = d3.event.target.cx.animVal.value*scale + k_x;
var target_y = d3.event.target.cx.animVal.value*scale + k_y;
}
you might need to +/- some constant k_x, k_y to correct for static offsets as well as access to the scale factor if you are using the scale method on the graph, otherwise you can ignore these
*note you probably don't want to try and mix jQuery and D3 if you can use D3 since the event properties likely contain references to the data that can be used, for example, in rendering tooltips
You will need to find the offset of the svg elem itself and then add the "cy" attribute (center y) to the y coordinate and the "cx" attribute (center x) to the x coordinate accordingly:
$('circle').hover(function (ev) {
var svgPos = $('svg').offset(),
x = svgPos.left + $(ev.target).attr('cx'),
y = svgPos.top + $(ev.target).attr('cy'),
coords = [x, y];
// coords now has your coordinates
});
If you are not using jQuery, consider using a usual hover event listener as well as .offsetTop and .offsetLeft on the element.