I've been playing around with this example here for a little while. What I'm trying to do is highlight a single node/circle in the plot (by making it larger with a border; later I want to add text or a letter inside it too).
Currently, I've made the circle for Bhutan larger in the plot like the following:
.attr("r",
function(d){return ( d.countryName === "Bhutan" ? r + 4 : r);})
.attr("stroke", function(d){if (d.countryName==="Bhutan"){return "black"}})
However, it overlaps with the other circles. What would be the best approach to avoid these collisions/overlaps? Thanks in advance.
Link to Plunkr - https://plnkr.co/edit/rG6X07Kzkg9LeVVuL0PH?p=preview
I tried the following to add a letter inside the bhutan circle
//find bhutan circle and add a "B" to it
countriesCircles
.data(data)
.enter().append("text")
.filter(function(d) { return d.countryName === "Bhutan"; })
.text("B");
Updated Plunkr - https://plnkr.co/edit/Bza5AMxqUr2HW9CYdpC6?p=preview
This is a slightly different problem than in this question here: How to change the size of dots in beeswarm plots in D3.js
You have a few options that I can think of:
Set the forceCollide to be your largest possible radius * 1.33, e.g. (r + 4) * 1.33. This will prevent overlapping, but spread things out a lot and doesn't look that great.
Add the radius property to each entry in your array and make the collide work based off that, which will look a bit better but not perform as awesomely for large sets.
Here's an example of how to do that:
...
d3.csv("co2bee.csv", function(d) {
if (d.countryName === "Bhutan") {
d.r = r + 4;
} else {
d.r = r;
}
return d;
}, function(error, data) {
if (error) throw error;
var dataSet = data;
...
var simulation = d3.forceSimulation(dataSet)
...
.force("collide", d3.forceCollide(function(d) { return d.r * 1.33; }))
...
countriesCircles.enter()
.append("circle")
.attr("class", "countries")
.attr("cx", 0)
.attr("cy", (h / 2)-padding[2]/2)
.attr("r", function(d){ return d.r; })
....
Use the row function in d3.csv to add a property to each member of the array called r, and check the country name to determine which one gets the larger value. Then use that value wherever you need to mess with the radius.
I guess it would've been possible to check the country name everywhere the radius was impacted (e.g. .force("collide", d3.forceCollide(function(d) { return d.countryName === "Bhutan" ? (r + 4) * 1.33 : r * 1.33; }), etc.). This feels a bit cleaner to me, but it might be cleaner still by abstracting out the radius from the data entries themselves...
Forked your plunk here: https://plnkr.co/edit/Tet1DVvHtC7mHz91eAYW?p=preview
Related
I have a geoJSON looking like so
{"type":"FeatureCollection",
"crs":{"type":"name",
"properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}},
"features":[{"type":"Feature",
"properties":{"scalerank":10,"natscale":1,"labelrank":8,"featurecla":"Admin-1 capital","name":"Colonia del Sacramento","namepar":null,"namealt":null,"diffascii":0,"nameascii":"Colonia del Sacramento","adm0cap":0,"capalt":0,"capin":null,"worldcity":0,"megacity":0,"sov0name":"Uruguay","sov_a3":"URY","adm0name":"Uruguay","adm0_a3":"URY","adm1name":"Colonia","iso_a2":"UY","note":null,"latitude":-34.479999,"longitude":-57.840002,"changed":4,"namediff":1,"diffnote":"Added missing admin-1 capital. Population from GeoNames.","pop_max":21714,"pop_min":21714,"pop_other":0,"rank_max":7,"rank_min":7,"geonameid":3443013,"meganame":null,"ls_name":null,"ls_match":0,"checkme":0},
"geometry":{"type":"Point","coordinates":[-57.8400024734013,-34.4799990054175]
}}]
}
I want to set to use colorbrewer to chose colors, depending on the value pop_max takes. Then I want to display this point data on a leaflet map through overlaying a svg ontop of leaflet. I can easily display the points and chose the color like so:
var feature = g.selectAll("path")
.data(collection.features)
.enter()
.append("path")
.style("fill", function(d) {
if(d.properties.pop_max) < 1000 {
return("red")
} else if {....
};
});
However, inconvenient.
So i tried:
var colorScale = d3.scale.quantize()
.range(colorbrewer.Greens[7])
.domain(0,30000000);
var feature = g.selectAll("path")
.data(collection.features)
.enter()
.append("path")
.style("fill", function(d) {
colorScale(d.properties.pop_max);
});
That does not display any points at all... Note that I estimated my domain. 0 is not necessarily the lowest number nor 30000000 the highest.
Any ideas?
First you'll need to find the max and min pop_max, something like this should work:
var extent = d3.extent(geojson.features, function(d) { return d.properties.pop_max; });
Second, since you want colors to represent 7 ranges of values you should be using d3.scale.threshold:
var N = 7,
step = (extent[1] - extent[0]) / (N + 1),
domain = d3.range(extent[0], extent[1] + step, step);
var colorScale = d3.scale.threshold()
.domain(domain)
.range(colorbrewer.Greens[N]);
EDITS
Looks like quantile can do this easier:
d3.scale.quantile()
.domain([extent[0], extent[1]])
.range(colorbrewer.Greens[N]);
I'm trying to combine click-to-zoom functionality (e.g. http://bl.ocks.org/mbostock/2206590) with d3's force layout (e.g. http://bl.ocks.org/mbostock/1093130). The idea being that after clicking on a node in a force layout diagram, a zoom transition is applied with the node clicked moving to the centre (while any other force-related "tick-ing" is also allowed to continue).
Does anyone know of existing examples combining these pieces of functionality? I've been trying to adapt the click-to-zoom example myself but although the clicked() function seems pretty simple, I'm stuck on the role of the "path" variable and how it seems to be linked to here...
g.append("path")
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
.attr("id", "state-borders")
.attr("d", path);
For example, I have a force layout where all nodes/links are appended to a parent g-element, do I really need an equivalent datum line above? And what is the function of the d attribute that the path variable is being fed into? I know I'm doing something wrong because at the moment when the clicked function evaluates "path.centroid(d)" it's always NaN.
Thanks for any thoughts at all!
Adding my current simplified implementation of automated click/zoom (triggered from within my nodeClick function) that works perfectly until the force layout moves nodes further (due to paramaters like charge, gravity, friction etc)...
var k;
if (d && centered !== d) {
k = 4;
centered = d;
} else {
k = 1;
centered = null;
}
svg.transition()
.duration(750)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -d.x + "," + -d.y + ")");
I would like to create a mathematical function for my D3 page.
Every node has got a "score" that I can get via d.score
I like to create a function that draws the radius of a circles based on that score.
I thought about following:
r <= 10, r >= 3
10 - (d.score / 50) = r
How could I do this in D3?
.attr('r', function(d) {
return node_size( * * ? ? ? * * );
});
Kind regards,
Marc
Please be advised, that this might be a bad question, due to it's "please give me code for this problem" character.
Anyway, i think this is what you want to try i guess:
.attr('r', function(d) {
var r = 10 - (d.score / 50);
if (r > 10) {r = 10;}
else if (r < 3) {r = 3;}
return r;
});
First you need to define a linear scale that would map the value of your node_size to an actual radius, as follows (example):
r = d3.scale.linear().range([3,10]);
Then specify as a domain, the values of your score parameter as follows (assuming DATA is your data structure)
// first define your radius according to your formula specified in your question.
DATA.forEach(function(d){
d.radius=10 - (d.score / 50);
})
//Then, assign is as a domain of your radius function
r.domain(d3.extent(DATA.map(function(d){return d.radius;})))
Finally, when plotting your data you should do something like this:
.attr('r', function(d) {return r(d.radius)});
You could merge or optimize these steps, but I think it is a good start.
Hope this helps.
Totally new (never coded anything), but slowly evolving enthusiast of D3 js. I'd like to show some financial data in a new manner at my workplace, which I consider cool and interesting.
By means of different techniques ( reading, learning, finally putting different pieces of code together of those who can code well) I achieved pretty good results and to my surprise it worked, namely Treediagram with children nodes.
There is one thing I can not get done, I want to change the size of special symbols (bigger numbers -> bigger symbol) by relating them to data by means of scale to make them visible & scalable.
Below what I tried to do:
-- fragment of data
data = [{"name":"AA ","parent":"null","actual":391,"plan":349,"year":349,"children": [
{"name":"AB ","parent":"AA","actual":11346,"plan":14429,"year":14429,},
{"name":"AC ","parent":"AA","actual":559,"plan":558,"year":558,},]}]
var xScale = d3.scale.linear()
.domain([d3.min(data, function(d) { return d[2]; }), d3.max(data, function(d) { return d[2]; })])
.range([100, 200]);
... some code ....
nodeEnter.append("path")
.style("stroke", "black")
.style("fill", "white")
.style("fill", function(d) {
var a = d.actual ;
var y = d.year;
if ( a/y > 1 && a/y <= 1.2 ) { return "rgb(255,192,203)";}
else if (a/y > 1.2) { return "rgb(255,0,0)";}
else if (a/y > 0 && a/y <= 0.8) { return "rgb(34,139,34)";}
else if (a/y > 0.8 && a/y < 1) { return "rgb(152,251,152)";}
else { return "rgb(255,255,0)";}
}
)
.attr("d", d3.svg.symbol()
.size(xScale(d[2]))
.type(function(d) { if
(d.actual - d.year> 0) { return "triangle-down"; } else if
(d.actual - d.year < 0) { return "triangle-up";} else
{ return "square";}
})
);
With just .size(200) all work well with .size(xScale(d[2]), no way. I know that despite reading a lot of articles and tutorials (never encountered a solution) I know nothing, so You are my last resort. I need just a little hint where to look for solution.
Thank You for your time & help
Zbyszek (PL)
I am trying to complete the last bit of a d3 project which dynamically creates these blue arcs, over which I need to place arc text, as shown in this image:
The image above is something I've done by placing the arc text statically, through trial and error, but I want to place it dynamically, based on the blue arcs which sit beneath the text. This is the code that dynamically creates the arcs:
var groupData = data_group.selectAll("g.group")
.data(nodes.filter(function(d) { console.log(d.__data__.key); return (d.key=='Employers' ||{exp:channel:entries category="13" backspace="2"} d.key == '{url_title}' ||{/exp:channel:entries}) && d.children; }))
.enter().append("group")
.attr("class", "group");
arc_group.selectAll("g.arc")
.data(groupData[0])
.enter().append("svg:path")
.attr("d", groupArc)
.attr("class", "groupArc")
.style("fill", "#1f77b4")
.style("fill-opacity", 0.5);
The {exp:} content is preparsed data I'm pulling from my content management system in expression engine if it looks confusing.
So, I have my arcs. Now you'll notice in the groupData code block I have a console.log statement, that will give me the names I want to appear in the arc text:
console.log(d.__data__.key);
Now, the code I was using to place the arc text statically was this:
var arcData = [
{aS: 0, aE: 45,rI:radius - chartConfig.linePadding + chartConfig.arcPadding,rO:radius - chartConfig.linePadding + chartConfig.textPadding-chartConfig.arcPadding}
];
var arcJobsData = d3.svg.arc().innerRadius(arcData[0].rI).outerRadius(arcData[0].rO).startAngle(degToRad(1)).endAngle(degToRad(15));
var g = d3.select(".chart").append("svg:g").attr("class","arcs");
var arcJobs = d3.select(".arcs").append("svg:path").attr("d",arcJobsData).attr("id","arcJobs").attr("class","arc");
g.append("svg:text").attr("x",3).attr("dy",15).append("svg:textPath").attr("xlink:href","#arcJobs").text("JOBS").attr("class","arcText"); //x shifts x pixels from the starting point of the arc. dy shifts the text y units from the top of the arc
And in this above code, the only thing left that I should need to do is dynamically assign an ID to the arcs, and then reference that ID in the xlink:href attribute, as well as replace the text("JOBS") with text that pulls from d.data__key. Given the code above which dynamically creates the arcs, and given that I know how to dynamically create and retrieve the text I want to place in the arcs using d.__data.key, I should be able to finish this thing off, but I can't figure out how write code in d3 that will take the data and place it in the arcs. Can anybody help with this?
You should give this blog post on nested selections a read; I believe it'll explain what you're trying to do.
Here's the gist. When you add data to your selection, assign the selection to a variable:
var g = data_group.selectAll("g.group")
.data(nodes.filter(function(d) { /* stuff */ }));
That way, you can perform subselections on it, which will receive a single element of the data bound to your g selection. You can use this to add your arcs and text:
g.enter().append('group') // Question: Are you sure you mean 'group' here?
.attr('class', 'group')
g.selectAll('g.arc')
.data(function(d, i) { return d; })
enter().append('path')
// Setup the path here
g.selectAll('text')
.data(function(d, i) { return d; })
.enter().append('text')
.attr('text', function(d) { return d.__data__.key })
The functions that are being used to do data binding in the nested selections (i.e., the g.selectAll()s) are being passed a single element of the data attached to g as d, and i is its index.
Figured this out. Changed the structure of things a bit so it made a little more sense, but essentially what I did is this:
var groupData = data_group.selectAll("g.group")
.data(nodes.filter(function(d) { return (d.key=='Employers' ||{exp:channel:entries category="13" backspace="2"} d.key == '{url_title}' ||{/exp:channel:entries}) && d.children; }))
.enter().append("group")
.attr("class", "group"); //MH - why do we need this group - these elements are empty. Shouldn't this just be an array? Find out how to delete the svg elements without getting rid of the data, which is needed below.
var groupArc = d3.svg.arc()
.innerRadius(ry - 177)
.outerRadius(ry - 157)
.startAngle(function(d) { return (findStartAngle(d.__data__.children)-2) * pi / 180;})
.endAngle(function(d) { console.log(d.__data__.key); return (findEndAngle(d.__data__.children)+2) * pi / 180});
var arc_and_text = arc_group.selectAll("g.arc")
.data(groupData[0])
.enter().append("svg:g")
.attr("class","arc_and_text");
var arc_path = arc_and_text.append("svg:path")
.attr("d", groupArc)
.attr("class", "groupArc")
.attr("id", function(d, i) { return "arc" + i; })
.style("fill", "#1f77b4")
.style("fill-opacity", 0.5); //MH: (d.__data__.key) gives names of groupings
var arc_text = arc_and_text.append("text")
.attr("class","arc_text")
.attr("x", 3)
.attr("dy", 15);
arc_text.append("textPath")
.attr("xlink:href", function(d, i) { return "#arc" + i; })
.attr("class","arc_text_path")
.style("fill","#ffffff")
.text(function(d, i) { return d.__data__.key; });
D3 still mystifies me a bit, and I'm sure this code could be much improved, but it works.