I'm trying to conditionally color these voronoi segments based on the 'd.lon' value. If it's positive, I want it to be green, if it's negative I want it to be red. However at the moment it's returning every segment as green.
Even if I swap my < operand to >, it still returns green.
Live example here: https://allaffects.com/world/
Thank you :)
JS
// Stating variables
var margin = {top: 20, right: 40, bottom: 30, left: 45},
width = parseInt(window.innerWidth) - margin.left - margin.right;
height = (width * .5) - 10;
var projection = d3.geo.mercator()
.center([0, 5 ])
.scale(200)
.rotate([0,0]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var path = d3.geo.path()
.projection(projection);
var voronoi = d3.geom.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.clipExtent([[0, 0], [width, height]]);
var g = svg.append("g");
// Map data
d3.json("/world-110m2.json", function(error, topology) {
// Cities data
d3.csv("/cities.csv", function(error, data) {
g.selectAll("circle")
.data(data)
.enter()
.append("a")
.attr("xlink:href", function(d) {
return "https://www.google.com/search?q="+d.city;}
)
.append("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("r", 5)
.style("fill", "red");
});
g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries)
.geometries)
.enter()
.append("path")
.attr("d", path)
});
var voronoi = d3.geom.voronoi()
.clipExtent([[0, 0], [width, height]]);
d3.csv("/cities.csv", function(d) {
return [projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat]) [1]];
}, function(error, rows) {
vertices = rows;
console.log(vertices);
drawV(vertices);
}
);
function polygon(d) {
return "M" + d.join("L") + "Z";
}
function drawV(d) {
svg.append("g")
.selectAll("path")
.data(voronoi(d), polygon)
.enter().append("path")
.attr("class", "test")
.attr("d", polygon)
// This is the line I'm trying to get to conditionally fill the segment.
.style("fill", function(d) { return (d.lon < 0 ? "red" : "green" );} )
.style('opacity', .7)
.style('stroke', "pink")
.style("stroke-width", 3);
}
JS EDIT
d3.csv("/static/cities.csv", function(data) {
var rows = [];
data.forEach(function(d){
//Added third item into my array to test against for color
rows.push([projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat]) [1], [+d.lon]])
});
console.log(rows); // data for polygons and lon value
console.log(data); // data containing raw csv info (both successfully log)
svg.append("g")
.selectAll("path")
.data(voronoi(rows), polygon)
.enter().append("path")
.attr("d", polygon)
//Trying to access the third item in array for each polygon which contains the lon value to test
.style("fill", function(data) { return (rows[2] < 0 ? "red" : "green" );} )
.style('opacity', .7)
.style('stroke', "pink")
.style("stroke-width", 3)
});
This is what's happening: your row function is modifying the objects of rows array. At the time you get to the function for filling the polygons there is no d.lon anymore, and since d.lon is undefined the ternary operator is evaluated to false, which gives you "green".
Check this:
var d = {};
console.log(d.lon < 0 ? "red" : "green");
Which also explains what you said:
Even if I swap my < operand to >, it still returns green.
Because d.lon is undefined, it doesn't matter what operator you use.
That being said, you have to keep your original rows structure, with the lon property in the objects.
A solution is getting rid of the row function...
d3.csv("cities.csv", function(data){
//the rest of the code
})
... and creating your rows array inside the callback:
var rows = [];
data.forEach(function(d){
rows.push([projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat]) [1]])
});
Now you have two arrays: rows, which you can use to create the polygons just as you're using now, and data, which contains the lon values.
Alternatively, you can keep everything in just one array (just changing your row function), which is the best solution because it would make easier to get the d.lon values inside the enter selection for the polygons. However, it's hard providing a working answer without testing it with your actual code (it normally ends up with the OP saying "it's not working!").
Related
I want to add a toolkit that show the type of the disaster, which is the key of the stack datum, how can i get it?
The format of .csv file is like this: (Forgive me can not take pictures)
AllNaturalDisasters,Drought,Earthquake,ExtremeTemperature,ExtremeWeather,Flood,Impact,Landslide,MassMovementDry,VolcanicActivity,Wildfire,Year
5,2,null,null,1,1,null,null,null,1,null,1900
2,null,2,null,null,null,null,null,null,null,null,1901
Here I create a stack
var stack = d3.stack()
.keys(["Drought", "Earthquake", "ExtremeTemperature", "ExtremeWeather", "Flood", "Impact", "Landslide", "MassMovementDry", "VolcanicActivity", "Wildfire"]);
and then I pass it my data:var series = stack(dataset);. dataset is the all data from the csv file. Then I create a chart using stack-layout, like this:
var groups = svg.selectAll("g")
.data(series)
.enter()
.append("g")
.style("fill", function(d, i) {
return colors(i);
});
var rects = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("height", function(d) {
return yScale(d[0]) - yScale(d[1]);
})
.attr("width", xScale.bandwidth())
.append("title")
.text(function (d) {
return d.data.Year;
});
The problem is right here:
.append("title")
.text(function (d) {
return d.data.Year;
});
I want to add a toolkit to show the type of the disaster, which is the key of this datum in series , how can I get it instead of the year?!
Each rectangle contains information on the column (year of disaster), but each g has information on the "row" (type of disaster).
The stack produces a nested array, the parent level (which we use to create the g elements) contains the key, or type of disaster
The child level represents the columns, which contains the year.
The grandchild level just contains individual rectangles.
So, we can get a key by selecting the parent g:
.append("title")
.text(function() {
var rect = this.parentNode; // the rectangle, parent of the title
var g = rect.parentNode; // the g, parent of the rect.
return d3.select(g).datum().key; // now we get the key.
})
Of course this could be simplified a bit, but I broke it out to comment it better.
This allows for more flexible sorting - rather than relying on fixed indexes.
Here it is using your data:
var csv = d3.csvParse(d3.select("pre").text());
var stack = d3.stack().keys(["Drought", "Earthquake", "ExtremeTemperature", "ExtremeWeather", "Flood", "Impact", "Landslide", "MassMovementDry", "VolcanicActivity", "Wildfire"]);
var series = stack(csv);
var colors = d3.scaleOrdinal()
.range(d3.schemeCategory10);
var xScale = d3.scaleBand()
.domain([0,1])
.range([0,300])
var yScale = d3.scaleLinear()
.domain([0,6])
.range([200,0]);
var svg = d3.select("svg");
var groups = svg.selectAll("g")
.data(series)
.enter()
.append("g")
.style("fill", function(d, i) {
return colors(i);
});
var rects = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("height", function(d) {
return yScale(d[0]) - yScale(d[1]);
})
.attr("width", xScale.bandwidth())
.append("title")
.text(function (d) {
var rect = this.parentNode;
var g = rect.parentNode;
return d3.select(g).datum().key;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="300"></svg>
<pre>AllNaturalDisasters,Drought,Earthquake,ExtremeTemperature,ExtremeWeather,Flood,Impact,Landslide,MassMovementDry,VolcanicActivity,Wildfire,Year
5,2,0,0,1,1,0,0,0,1,0,1900
2,0,2,0,0,0,0,0,0,0,0,1901</pre>
Well, I have fixed this problem by a very 'low' method. I have created a simple function:
function getKeys(d) {
return series[parseInt(groups.selectAll("rect").data().indexOf(d) / series[0].length)].key;
}
Well, it so simple and crude, and I still want to know a more efficient method!!!
I have rewritten most of my d3 code to v4, but the new update pattern is throwing me off. The example below is for a force diagram. A duplicate circle is created within the first container upon every update. The data in my example does not actually change, but it's irrelevant. If I use new data, the same issue (a duplicate circle) occurs.
var w = 800,
h = 500;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
var dataset = {};
function setData() {
dataset.nodes = [{
value: 200
}, {
value: 100
}, {
value: 50
}];
}
setData();
var rScale = d3.scaleSqrt()
.range([0, 100])
.domain([0, d3.max(dataset.nodes.map(function(d) {
return d.value;
}))]);
var node = svg.append("g")
.attr("class", "nodes")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")")
.selectAll(".node");
var simulation = d3.forceSimulation(dataset.nodes)
.force("charge", d3.forceManyBody().strength(-1600))
.force("x", d3.forceX())
.force("y", d3.forceY())
.alphaDecay(.05)
.on("tick", ticked);
function ticked() {
node.selectAll("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
function restart() {
// Apply the general update pattern to the nodes.
node = node.data(dataset.nodes, function(d) {
return d.id;
});
node.exit().remove();
node = node.enter().append("g")
.attr("class", "node")
.merge(node);
node.append("circle")
.attr("r", function(d) {
return rScale(d.value);
});
// Update and restart the simulation.
simulation.nodes(dataset.nodes);
simulation.alpha(1).restart();
}
restart();
function update() {
setData();
restart();
}
d3.select("#update").on("click", update);
If you click the Update button in this codepen (https://codepen.io/cplindem/pen/wpQbQe), you will see all three circles animate as the simulation restarts, but behind the largest circle, there is another, identical circle that does not animate. You can also see the new circle appear in the html if you inspect it.
What am I doing wrong?
Your first problem seems to be that you are keying the data on an 'id' field, but your data doesn't have any ids, so that needs changed or you just keep adding new groups:
function setData() {
dataset.nodes = [{
value: 200,
id: "A"
}, {
value: 100,
id: "B"
}, {
value: 50,
id: "C"
}];
console.log("dataset", dataset);
}
The second problem is you merge the new and updated selection and then append new circles to all of them, even the existing ones (so you have multiple circles per group on pressing update). I got it to work by doing this: make the new nodes, merge with existing selection, add circles to just the new nodes, update the circles in all the nodes:
node.exit().remove();
var newNodes = node.enter().append("g");
node = newNodes
.attr("class", "node")
.merge(node);
newNodes.append("circle");
node.select("circle")
.attr("r", function(d) {
return rScale(d.value);
});
Whether that 2nd bit is optimal I don't know, I'm still more anchored in v3 myself...
https://codepen.io/anon/pen/WdLexR
I'm trying to create dynamic treemap graphic with lovely d3.js library.
Here are sample of my code
element = d3.select(element[0]);
var margin = 10,
width = parseInt(element.style('width')) - margin * 2,
height = parseInt(element.style('height')) - margin * 2;
var color = d3.scale.category10();
var canvas = d3.select('.treemap').append('svg')
.attr('width', width)
.attr('height', height)
.attr('transform', 'translate(-.5,-.5)')
.style('margin', margin);
var treemap = d3.layout.treemap()
.size([width, height])
.value(function(d) { return d.value; })
.sticky(false);
function redraw(data) {
d3.selectAll('.cell').remove();
var treemapData = {};
treemapData.children = data.map(function(d) {
return {
name: d.name,
value: d.value
};
});
var leaves = treemap(treemapData);
var cells = canvas.selectAll("g")
.data(leaves);
var cellsEnter = cells.enter()
.append('rect')
.attr('class', 'cell')
.attr('x', function(d) { return d.x; })
.attr('y', function(d) { return d.y; })
.attr('width', function(d) { return d.dx; })
.attr('height', function(d) { return d.dy; })
.attr('fill', function(d) { return d.children ? null : color(d.name); })
.attr('stroke', "#fff")
.style('fill-opacity', 0);
console.log(cells.exit(), cells.enter());
}
And here I have stucked.
console.log() shows that whole new data are enter(), and none are exit() !
Input data presents like
[{value: 590, name:"A1"}, {...}, ...]
without root object field, so that's why I remapped data in treemapData object.
Š¢hanks that you at least spent your time for reading this post so far, hope you have any suggestions.
UDP. you can check working version of my code here: https://jsfiddle.net/qtbfm08k/
The following works:
remove d3.selectAll('.cell').remove();
use the code below
See the fiddle: https://jsfiddle.net/b6meLedn/4/
var cells = canvas.selectAll('.cell') //select all cells
.data(leaves); //map the data
cells.exit().remove(); //remove old extra elements
cells.enter()
.append('rect') //create new rectangles as necessary
.attr('class', 'cell')
cells //take all cells (old cells that have new data+extra new cells)
.attr('x', function(d) { return d.x; })
...
All:
I am trying to make a customized line generator like D3 line(), but with ability to customize line segments style when there is data missing(like using dash line)
one thing I did not know how to implement is its .interpolate() function. The math seems complicated, what I am trying to do is just use existing D3 line function to draw those continus segments and connect them with dash line, but I can not figure out how to generate interpolated line?
In the code example below, u can see the dash line is not exactly overlap the solid line:
var data = [];
for(var i=0; i<20; i++){
if( i>0 && (i%4==0) ){
data.push(null);
}else {
data.push({x:i, y:Math.random(i)})
}
}
var x = d3.scale.linear();
var y = d3.scale.linear();
x.domain([0, 19])
.range([10, 390])
y.domain([0, 1])
.range([10, 360]);
var svg = d3.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 400);
var lg = svg.append("g")
.classed("lineGroup", true);
var xg = svg.append("g")
.classed("xaxis", true)
.attr("transform", function(){
return "translate(0, 380)";
});
var line = d3.svg.line()
.interpolate("monotone")
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });
line.defined(function(d) { return d!=null; });
lg.append("path")
.classed("shadowline", true)
.attr("d", function(){
return line(data.filter(function(d){return d!=null;}));
})
.style("fill", "none")
.style("stroke", "steelblue")
.style("stroke-width", "3px")
.attr("stroke-dasharray", "5,5");
lg.append("path")
.attr("d", function(){
return line(data);
})
.style("fill", "none")
.style("stroke", "steelblue")
.style("stroke-width", "3px");
lg.selectAll("circle")
.data(data.filter(function(d){return d!=null;}))
.enter()
.append("circle")
.style("fill", "orange")
.style("stroke", "red")
.style("stroke-width", "3px")
.attr("r",5)
.attr("cx", function(d){return x(d.x);})
.attr("cy", function(d){return y(d.y);})
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
xg.call(xAxis);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Any help? Thanks
I've come up with a weird algorithm to hide some parts of your line, first of all you have to realize that the interpolation algorithm you chose works by analyzing the previous and next points of any t between the previous and next point, therefore even if you want to generate only segment of the path you have to use the same interpolation algorithm otherwise the first/last points won't have the required curve
With that in mind my algorithm to solve your problem is as follows
render the solid path
render some segments of this solid path but with a white stroke so that it works like a mask
render the dashed path
Implementation
first render the solid path with the desired interpolation
in the data find the extremes of all the gaps e.g. gaps([0, 1, null, 3, 4, null, 5]) is transformed to [[1, 3], [4, 5]]
compute the length of the path at those points, this involves an exhaustive brute force check since there's no api to get the length from the origin of a path to a determined point that lies on it, since your data is increasing on x I did binary search but for the general case as I've said you need to do a brute force check
make a lot of samples between the gap endpoints (seen as path lengths) with path. getPointAtLength and finally render a path for each collection of points, the trick is to set a white stroke
render the dashed path
NOTE: I changed the interpolation function to 'cardinal' so that curves are a lot more noticed and you can see the masks in action
var data = [];
for(var i=0; i<20; i++){
if( i>0 && (i%4==0) ){
data.push(null);
}else {
data.push({x:i, y:Math.random(i)})
}
}
var x = d3.scale.linear();
var y = d3.scale.linear();
x.domain([0, 19])
.range([10, 390])
y.domain([0, 1])
.range([10, 360]);
var svg = d3.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 400);
var lg = svg.append("g")
.classed("lineGroup", true);
var xg = svg.append("g")
.classed("xaxis", true)
.attr("transform", function(){
return "translate(0, 380)";
});
var line = d3.svg.line()
.interpolate("cardinal")
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });
function lineFiltered(data) {
return line(data.filter(function (d) { return !!d }))
}
var basePath = lg.append("path")
.attr("d", function () { return lineFiltered(data) })
.style("fill", "none")
.style("stroke", "steelblue")
.style("stroke-width", "3px");
function getPathLengthAtPoint(path, point, samples) {
// binary search to find the length of a path closest to point
samples = samples || 100
var lo = 0, hi = path.getTotalLength()
var res = 0
for (var i = 0; i < samples; i += 1) {
var mid = lo + (hi - lo) / 2
var pMid = path.getPointAtLength(mid)
if (pMid.x < x(point.x)) {
res = lo = mid
} else {
hi = mid
}
}
return res
}
// gets endpoints from where there's a gap
// it assumes that a gap has only length 1
function getGapsEndPoints(data) {
var j = 0
var gaps = []
for (var i = 0; i < data.length; i += 1) {
if (typeof data[i] !== 'number') {
gaps.push([data[i - 1], data[i + 1]])
}
}
return gaps
}
// generates multiple points per path
function generatePaths(data, path, samples) {
samples = samples || 50
return data.map(function (d) {
var lo = d[0], hi = d[1]
var points = []
for (var i = 0; i <= samples; i += 1) {
var point = path.getPointAtLength(lo + i/samples * (hi - lo))
points.push({
x: x.invert(point.x),
y: y.invert(point.y)
})
}
return points
})
}
var missingData = data.map(function (d) {
return d && getPathLengthAtPoint(basePath.node(), d)
})
missingData = getGapsEndPoints(missingData)
missingData = generatePaths(missingData, basePath.node())
// finally create the mask paths using the same line generator
lg.selectAll('path.mask').data(missingData)
.enter().append('path').classed('mask', true)
.attr('d', lineFiltered)
.style("fill", "none")
.style("stroke", "white")
.style("stroke-width", "3px")
lg.append("path")
.classed("shadowline", true)
.attr("d", function () { return lineFiltered(data) })
.style("fill", "none")
.style("stroke", "steelblue")
.style("stroke-width", "3px")
.attr("stroke-dasharray", "5,5");
lg.selectAll("circle")
.data(data.filter(function(d){return d!=null;}))
.enter()
.append("circle")
.style("fill", "orange")
.style("stroke", "red")
.style("stroke-width", "3px")
.attr("r",5)
.attr("cx", function(d){return x(d.x);})
.attr("cy", function(d){return y(d.y);})
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
xg.call(xAxis);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Here is my attempt:
Imagine you have an array like this:
[1,2,3, null, 4,5, null,null, 8,9]
Break it into two array groups
datachunks = [[1,2,3],[4,5][8,9]
brokenDataChunks = [[3,4][5,8]]
Now draw the datachunks like this:
datachunks.forEach(function(dc){
lg.append("path")
.attr("d", function(){
return line(dc);
})
.style("fill", "none")
.style("stroke", "steelblue")
.style("stroke-width", "3px")
})
Now draw the brokenDataChunks like this:
brokendatachunks.forEach(function(dc){
lg.append("path")
.classed("shadowline", true)
.attr("d", function(){
return line(dc);
})
.style("fill", "none")
.style("stroke", "red")
.style("stroke-width", "3px")
.attr("stroke-dasharray", "5,5");
})
The main challenge was to get the array split into this fashion:
var datachunks = [];//hold data chunks for the blue line connected one
var brokendatachunks = [];//hold data chunks for the red dashed line disconnected ones
var tempconnected =[];
var tempbroken =[];
data.forEach(function(d, i){
if(d){//if not null
tempconnected.push(d); //push in tempconnected
if (tempbroken.length > 0){
tempbroken.push(d);//if broken was detected before
brokendatachunks.push(tempbroken);//add this array into brokendatachunks.
tempbroken = [];//set the new value in temp broken to get new set of values
}
} else {
if(data[i-1])
tempbroken.push(data[i-1]);//push previous value don't want to insert null here.
datachunks.push(tempconnected);
tempconnected = [];
}
});
if (tempconnected.length > 0){
datachunks.push(tempconnected);
}
if (tempbroken.length > 0)
brokendatachunks.push(tempbroken);
}
working code here
complex case with lot of broken points in between here
In complex case I have put point generation like this
for(var i=0; i<20; i++){
if( i>0 && (i%4==0 || i%3 ==0) ){
data.push(null);
}else {
data.push({x:i, y:Math.random(i)})
}
}
I have a line chart (or, more properly a connected scatterplot) where I plot pies/donuts around the points. So, there is a data set that specifies the date and mood for plotting the points along with two other parameters, pos and neg, providing the values to go into the pie chart. The overall data that describes the points is called plotPoints.
That all works great, but what I still would like to do is to set the radius of the pie to be a function of the sum of pos + neg.
When I plot out the points, I can access all of the data with a function(d). In each pie, however, function(d) returns the data about the slices, one at a time. d contains the data for the current slice, not the overall data. For each pie, the first time arc is called it has the frequency for the first pie slice and the second time it is called it has the frequency for the second pie slice.
How do I refer to the current plotPoints properties when drawing the arc so that I can change the radius of the pie/donut to represent the sum of plotPoints[i].pos + plotPoints[i].neg?
The relevant code looks like this:
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(8);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d; });
var p = moodChart.selectAll(".pieContainer")
.data(plotPoints).enter()
.append("g")
.attr("class","pieContainer")
.attr("transform", function(d,i) {return "translate(" + (x(d.date)) + "," + (y(d.mood)) + ")"});
p.append("title")
.text(function(d) { return shortDateFormat(d.date) +", " + d.mood.toFixed(2) });
var g = p.selectAll(".arc")
.data(function (d) {return pie([d.neg, d.pos])})
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d,i) { return i==0 ? "brown" : "green"; });
It's tough to answer this authoritatively without a bit more code/data to look at but in this situation I usually stash the needed variables in my data-bindings so they are available later:
var g = p.selectAll(".arc")
.data(function (d) {
var total = d.neg + d.pos,
pie_data = pie([d.neg, d.pos]),
point_arc = d3.svg.arc()
.outerRadius((total * radius) - 10) //<-- set radius based on total
.innerRadius((total * radius) - 8);
pie_data.forEach(function(d1){
d1.data.arc = point_arc; //<-- stash the arc for this point in our data bindings
});
return pie_data;
});
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", function(d){
return d.data.arc
})
.style("fill", function(d,i) { return i==0 ? "brown" : "green"; });