Bind new variables to __data__, keep existing variables - d3.js

I have (x,y) data bound to a couple of circles as follows:
var svg = d3.select("body")
.append("svg")
.attr("width", 640)
.attr("height", 400)
var data = [{x: 100, y: 100}, {x: 200, y: 200}]
var circles = svg.selectAll("circle").data(data)
.enter().append("circle")
.attr("r", 20)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
I now want to bind a new variable z to each circle, using a simple index join.
newdata = [{z: 10}, {z: 30}]
circles.data(newdata)
If I now do circles.data() I see that the z variables have been bound as expected, but that x and y are no longer there.
Is there a way to have x, y AND z bound to the circles' data?
Note that the title of this SO question makes it sound similar, but it's not asking the same thing!

This can be done fairly easily with the selection.each function:
// Add a new variable, 'z', equal 2 * x
circles.each(function(d) { d.z = d.x * 2; });
Or, for the specific case where the new data to be bound is stored in a list of objects called newdata (using simple indexing to join the data):
circles.each(function(d, i) { d.z = newdata[i].z; });

Related

d3 area chart filling below x asis

I am trying to draw area graph but it is filling below x axis. In below code I have used y0(yScale(0)) as I have seen in many examples and also I tried to give y0(height) it is not giving me correct output. I want area to be filled only above x axis if y axis values are +ve and if y axis values are -ve then area is going above max tick of y axis.
const D3Node = require('d3-node');
getGraphString: (data,yAxisTickFormat) => {
const d3n = new D3Node() // initializes D3 with container element
const d3 = d3n.d3;
let margin = { top: 20, right: 30, bottom: 20, left: 40 },
width = 275,
height = 200;
let svg = d3n.createSVG(width, height+margin.top+margin.bottom);
let xScale = d3.scaleTime().range([margin.left, width - margin.right])
.domain(d3.extent(data, function (d) { return d.date })),
yScale = d3.scaleLinear().range([height - margin.top, margin.bottom])
.domain(d3.extent(data, function (d) { return d.value }));
svg.append('g').attr("class", "xAxis")
.attr("transform", "translate(0," + (height - margin.bottom) + ")") //The transforms are SVG transforms
.call(d3.axisBottom(xScale).ticks(d3.timeYear).tickFormat(d3.timeFormat('%Y')))
.selectAll("text")
.style("text-anchor","end")
.attr("dx", "-.9em")
.attr("dy", ".50em")
.attr("transform","rotate(-45)")
svg.append("g") //We create an SVG Group Element to hold all the elements that the axis function produces.
.attr("transform", "translate(" + (margin.left) + ",0)")
.attr("class","yAxis")
.call(d3.axisLeft(yScale).ticks(4).tickFormat(d3.format(yAxisTickFormat)))
.selectAll("text")
.style("text-anchor","end")
.attr("dy", "0.32em")
.attr("dx", "-0.4em")
let lineFunc = d3.line().x(function (obj) { return xScale(obj.date) })
.y(function (obj) { return yScale(obj.value) })
svg.append("path")
.attr("d", lineFunc(data))
.attr("stroke", '#002046')
.attr("stroke-width", 3)
.attr("fill", "none");
let area = d3.area()
.curve(d3.curveLinear)
.x(function (d) { return xScale(d.date); })
.y0(yScale(0))
.y1(function (d) { return yScale(d.value); });
svg.append("path")
.style("fill", "#002046")
.attr("d", area(data));
return d3n.svgString();
}
let data =[ { date: 2019-01-01T00:00:00.000Z, value: 0.6330419130189774 },
{ date: 2018-01-01T00:00:00.000Z, value: 0.6266752649582236 },
{ date: 2017-01-01T00:00:00.000Z, value: 0.6403446517126394 },
{ date: 2016-01-01T00:00:00.000Z, value: 0.6432956408788177 } ];
getGraphString(data,'.0%');
Problem
If your y axis (scale domain) starts at 0, then yScale(0) is an appropriate baseline for the area. However, your scale's domain extent does not start at 0, it is dependent on the dataset:
yScale = d3.scaleLinear().range([height - margin.top, margin.bottom])
.domain(d3.extent(data, function (d) { return d.value }));
The lowest value in your dataset is 0.6266... not zero. In using yScale(0) D3 interpolates where y(0) would be, which in your case would require extending the axis quite a bit down the page and off the SVG.
Solution
We can manually set the baseline with something like : area.y0(yScale(0.6266...)). This places the baseline at the base of your y axis. But you don't need to set it manually as you can can set it with:
area.y0(yScale.range()[0]);
yScale.range() returns an array containing the scaled extent of the yScale (and therefore the y axis), we want to have the area's base be the same as the axis.
yScale.range()[0] is the equivilant of yScale(yScale.domain()[0]); - if 0 is the minimum value of the domain (0 == yScale.domain()[0]), it's a short jump to the often used area.y0(yScale(0))
Alternative
Alternatively, if you want the axis to include zero, you could keep yScale(0) as the baseline and set 0 to be the minimum value of the scale's domain:
.domain([0,d3.max(function(d) { return d.value; })])
Either way, the value provided as the minimum value for the scale's range and area.y0 should be the same if you want the bottom of the area to be aligned to the bottom of the axis. (This value should generally also be equal to the y translate value for the x axis).

D3.js: scaleTime not working (strange results)

I have found the following chart which uses V3 of d3:
http://mbostock.github.io/d3/talk/20111018/area-gradient.html
I have tried to port that sample to V5, but I got stuck with scaleTime(). The chart is not displayed correctly.
I have difficulties to debug var x as it is a function ... however if I look at the element svg:clipPath I see this (note the strange values):
<clipPath id="clip"><rect x="-8119106.125" y="0" width="0.000008575618267059326" height="361"></rect></clipPath>
These are the most relevant parts of my code:
var w=1280, h=800;
var svg = d3.select("#zoomable-area-chart").append("svg:svg");
(....)
// Scales
var x = d3.scaleTime().range([0, w]),
y = d3.scaleLinear().range([h, 0]),
svg.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("x", x(0))
.attr("y", y(1))
.attr("width", x(1) - x(0))
.attr("height", y(0) - y(1));
d3.csv("http://localhost/data.csv").then((data) => {
// Parse dates and numbers.
data.forEach(function(d) {
d.date = parse(d.date);
d.value = +d.value;
});
// Compute the maximum price.
x.domain([new Date(1999, 0, 1), new Date(2003, 0, 0)]);
y.domain([0, d3.max(data, function(d) {
return d.value;
})]);
draw();
});
Here you can find a playground for testing:
https://plnkr.co/edit/qWlLIg2avCe2Kt88r0T5?p=preview
the default domain of scaleTime is
Constructs a new time scale with the domain [2000-01-01, 2000-01-02], the unit range [0, 1]
Why use x(v) and y(v) to get there ranges when you set them as constants and you can get the range and use [0] and [1].
Make your zoom-rect fill none, it hides the graph

Conditionally fill/color of voronoi segments

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!").

Update D3 chart dynamically

The idea is to have a d3 vertical bar-chart that will be given live data.
I simulate the live data with a setInterval function that updates the the values of the elements in my dataset:
var updateData = function(){
a = parseInt(Math.random() * 100),
b = parseInt(Math.random() * 100),
c = parseInt(Math.random() * 100),
d = parseInt(Math.random() * 100);
dataset = [a, b, c, d];
console.log(dataset);
};
// simullate live data input
var update = setInterval(updateData, 1000);
I want to update the chart every 2 seconds.
For that I need a update function that gets the new dataset and then animates a transition to show the new results.
Like that:
var updateVis = function(){
..........
};
var updateLoop = setInterval(drawVis,2000);
I don't want to simply remove the chart and draw again. I want to animate the transition between the new and old bar height for each bar.
Checkout the fiddle
Since your not changing the number of bars, this can be as simple as:
var updateVis = function(){
svg.selectAll(".input")
.data(dataset)
.transition()
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
};
Updated fiddle.
But your next question becomes, what if I need a different number of bars? This is where you need to handle enter, update, exit a little better. You you can write one function for initial draw or updating.
function drawVis(){
// update selection
var uSel = svg.selectAll(".input")
.data(dataset);
// those exiting
uSel.exit().remove();
// new bars
uSel
.enter()
.append("rect")
.attr("class", "input")
.attr("fill", "rgb(250, 128, 114)");
// update all
uSel
.attr("x", function(d, i) {
return i * (w / dataset.length) + 2.5/100 * w;
})
.attr("width", w / dataset.length - barPadding)
.attr("height", y(0))
.transition().duration(750).ease("linear")
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
}
New fiddle.
That's the way to go.
Just think what you've done to get the initial chart:
1) Get data
2) Bind it to element (.enter())
3) Set element attributes to be function of the data.
Well, you do this again:
In the function updateData you get a new dataset that's the first step.
Then, rebind it:
d3.selectAll("rect").data(dataset);
And finally update the attributes:
d3.selectAll("rect").attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
(Want transitions? Go for it. It is easy to add in your code but you better read this tuto if you want to deeply understand it)
Check it on fiddle

Weird clipping with conrec.js contour in D3.js

I'm getting weird clipping behavior when using d3.js and conrec.js to plot contours:
This is a contour plot that is correctly depicted
This is a plot with clipping. The paths are filling incorrectly
Another example of a plot with unwanted behavior
Here is my code and approach. Data is a 31x31 value array. I make the contour and then data-bind the contour's contourList. I'm guessing something weird with the pathing is happening there!
var cliff = -1000;
data.push(d3.range(data[0].length).map(function() {
return cliff;
}));
data.unshift(d3.range(data[0].length).map(function() {
return cliff;
}));
data.forEach(function(d) {
d.push(cliff);
d.unshift(cliff);
});
var c = new Conrec,
xs = d3.range(0, data.length),
ys = d3.range(0, data[0].length),
zs = [0],
width = 300,
height = 300,
x = d3.scale.linear().range([0, width]).domain([0,
data.length
]),
y = d3.scale.linear().range([height, 0]).domain([0,
data[0].length
]);
c.contour(data, 0, xs.length - 1, 0, ys.length - 1, xs,
ys, zs.length, zs);
contourDict[key] = c;
d3.select("svg").remove();
var test = d3.select("#contour").append("svg")
.attr("width", width)
.attr("height", height)
.selectAll("path")
.data(c.contourList())
.enter().append("path")
.style("stroke", "black")
.attr("d", d3.svg.line()
.x(function(d) {
return x(d.x);
})
.y(function(d) {
return y(d.y);
}));
Thank you for your time!

Resources