D3 .rangeBands() returning NaN - javascript

In the code below Chrome's debugger I get the following error
Error: Invalid value for <rect> attribute y="NaN"
when this is exectued
this.options.barDemo.selectAll("rect")
.data(data)
.enter()
.append("svg:rect")
.attr("x", function (d, i) {
//console.log(x(i));
return x(i);
})
.attr("y", function (d) {
console.log(height - y(d.attributes.contract));
return height - y(d.attributes.contract);
})
.attr("height", function (d) {
console.log(y(d.attributes.contract));
return y(d.attributes.contract);
})
.attr("width", barWidth)
.attr("fill", "#2d578b");
I've isolated the problem to this function for the y coordinate value
y(d.attributes.contract)
My question is this: Given a value in between 0 and 200000, why would the above statement evaluate to NaN given var y is equal to this:
var y = d3.scale.ordinal().domain([0, 200000]).rangeBands([height, 0]);
It's my understanding that the D3 .ordinal() function will return a coordinate based on where that value falls within a given range. This is my first time using D3, however, so any suggestions/hints/tips greatly appreciated.

I think an ordinal scale is the wrong choice here. An ordinal scale maps values the discrete set of values in the domain directly to values in the range, so it will only return a value if you pass it one of the values in the domain. In your case it will return the height when you pass 0, and 0 when you pass 200000. Anything else will return NaN.
Probably what you want is a linear scale. Linear scales take in a continuous range, and then return a value within the range. Given this scale:
var y = d3.scale.linear().domain([0, 200000]).range([height, 0]);
you can pass it any value between 0 and 200000 and it will return a value between height and 0.

I was getting NaN in a different scenario because I was passing undefined as my padding when configuring via .rangeBands([0,1], padding). Something to watch out for.

Related

Bubble Map with leaflet and D3.js [problem] : bubbles overlapping

I have a basic map here, with dummy data. Basically a bubble map.
The problem is I have multiple dots (ex:20) with exact same GPS coordinates.
The following image is my csv with dummy data, color blue highlight overlapping dots in this basic example. Thats because many compagny have the same city gps coordinates.
Here is a fiddle with the code I'm working on :
https://jsfiddle.net/MathiasLauber/bckg8es4/45/
Many research later, I found that d3.js add this force simulation fonction, that avoid dots from colliding.
// Avoiding bubbles overlapping
var simulationforce = d3.forceSimulation(data)
.force('x', d3.forceX().x(d => xScale(d.longitude)))
.force('y', d3.forceY().y(d => yScale(d.latitude)))
.force('collide', d3.forceCollide().radius(function(d) {
return d.radius + 10
}))
simulationforce
.nodes(cities)
.on("tick", function(d){
node
.attr("cx", function(d) { return projection.latLngToLayerPoint([d.latitude, d.longitude]).x; })
.attr("cy", function(d) {return projection.latLngToLayerPoint([d.latitude, d.longitude]).y; })
});
The problem is I can't make force layout work and my dots are still on top of each other. (lines: 188-200 in the fiddle).
If you have any tips, suggestions, or if you notice basic errors in my code, just let me know =D
Bunch of code close to what i'm trying to achieve
https://d3-graph-gallery.com/graph/circularpacking_group.html
https://jsbin.com/taqewaw/edit?html,output
There are 3 problems:
For positioning the circles near their original position, the x and y initial positions need to be specified in the data passed to simulation.nodes() call.
When doing a force simulation, you need to provide the selection to be simulated in the on tick callback (see node in the on('tick') callback function).
The simulation needs to use the previous d.x and d.y values as calculated by the simulation
Relevant code snippets below
// 1. Add x and y (cx, cy) to each row (circle) in data
const citiesWithCenter = cities.map(c => ({
...c,
x: projection.latLngToLayerPoint([c.latitude, c.longitude]).x,
y: projection.latLngToLayerPoint([c.latitude, c.longitude]).y,
}))
// citiesWithCenter will be passed to selectAll('circle').data()
// 2. node selection you forgot
const node = selection
.selectAll('circle')
.data(citiesWithcenter)
.enter()
.append('circle')
...
// let used in simulation
simulationforce.nodes(citiesWithcenter).on('tick', function (d) {
node
.attr('cx', function (d) {
// 3. use previously computed x value
// on the first tick run, the values in citiesWithCenter is used
return d.x
})
.attr('cy', function (d) {
// 3. use previously computed y value
// on the first tick run, the values in citiesWithCenter is used
return d.y
})
})
Full working demo here: https://jsfiddle.net/b2Lhfuw5/

Questions when building bar charts by D3.js

Freshboy got several questions when creating my first bar chart by D3.js:
line 30 -- console.log(yScale); The console shows "function.....", what's this? ; Last line-- Why I can get correct answers of each column whenI give
"height" the value "yScale". What's happening there?
.attr("x", function(data, i){return xScale(i)}) xScale is a variable not a function. Why can I use xScale like a function--xScale(i)?
var data = [4,8,15,16,23,42];
const svg = d3.select("svg");
const margin = {top:25, right:25, bottom:25, left:25};
const xDomain = [0,5];
const xRange = [0,200];
const yDomain = [0,42];
const yRange = [0,200];
const rectWidth = 200 / 6;
const xScale = d3.scaleLinear()
.domain(xDomain)
.range(xRange);
var yScale = d3.scaleLinear()
.domain(yDomain)
.range(yRange);
const g = svg.append("g")
.attr("transform", "translate(50,50)");
createRect = g.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", function(data, i){return xScale(i)})
.attr("y", function(data,i){
console.log(yScale(data));
console.log(yScale);
return 200-yScale(data)})
.attr("width", rectWidth)
.attr("height", yScale);
<!DOCTYPE html>
<html>
<script src='https://d3js.org/d3.v5.min.js'></script>
<style> rect {fill: lightblue; stroke: black; }</style>
<body>
<svg width=300 height=300>
</svg>
<script src="w8-1-4 Axes.js">
</script>
</body>
</html>
Both xScale and yScale are functions.
This is what you see when you log yScale and why you can use xScale "like a function". d3.scaleLinear() returns a function to scale values. In javascript, functions are objects, object properties can also be functions (methods in this case to set values such as domain and range).
Most simply:
d3.scaleLinear() returns a function.
With d3 scales (and most d3 functions), you create a new scale function with d3.scaleLinear() and then use the scale's methods to set values such as the scale's domain or range. The method returns the scale itself, hence why you can chain together several methods, modifying the scale function as you go.
Why don't you need to pass any parameters to yScale when using .attr("height",yScale)?
D3 uses Function.prototype.apply() to pass paramaeters to any function provided to .attr() (or many other methods in D3, such as selection.style() or selection.each()). By using .apply D3 "calls a function with a given this value, and arguments provided as an array". The this value in D3 is generally an individual element, the first parameter, canonnically d, is the datum associated with that element, the 2nd paremeter, cannonically i, is the index of that element, and the last parameter is the group of elements in the selection.
The function passed to .attr() is called for each element in the selection.
The scale function takes one parameter, a value to be scaled. As your datum is a number, we can pass the scale function directly to .attr(): the first parameter passed to it will be the datum.
Carrying forward the above and for reference,
.attr("height",xScale)
produces the same result as:
.attr("height", function(d) {
return xScale(d);
})
as d is an element's datum (an item from the data array), we can use either approach above.

D3 projection not setting cx property

I have a map of the USA that I'm trying to display lat/lon points over. I've mashed together a few examples to get this far, but I've hit a wall. My points are in a csv file, which I'm not sure how to upload here, but it's just 65,000 rows of number pairs. For instance 31.4671154,-84.9486771.
I'm mostly following the example from Scott Murray's book here.
I'm using the Albers USA projection.
var projection = d3.geo.albersUsa()
.scale(1200)
.translate([w / 2, h / 2]);
And setting up the landmarks as an svg group appended to the map container.
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.on("click", stopped, true);
svg.append("rect")
.attr("class", "background")
.attr("width", w)
.attr("height", h)
.on("click", reset);
var g = svg.append("g");
var landmarks = svg.append("g")
I read the data and try to set circles at each lat/lon point.
d3.csv("./public/assets/data/landmark_latlon_edited.csv", function(error, latlon){
console.log(latlon);
landmarks.selectAll("circle")
.data(latlon)
.enter()
.append("circle")
.attr({
'fill': '#F00',
'r': 3
})
.attr('cx', function(d){
return projection([d.lon, d.lat][0]);
})
.attr('cy', function(d){
return projection([d.lon, d.lat])[1];
})
.style({
'opacity': .75
});
});
Now, the problem is that the cx property is not receiving a value. When viewed in the inspector the circles don't show a cx, and, indeed, appear in the svg at the appropriate y values, but in a stacked column at x=0.
<circle fill="#F00" r="3" cy="520.8602676002965" style="opacity: 0.75;"></circle>
I found an old issue I thought might be related here which states that the projection method will return null if you try to feed it values outside of its normal bounding box. I opened the csv in Tableau and saw a couple values that were in Canada or some U.S. territory in the middle of the Pacific (not Hawaii), and I removed those, but that didn't solve the problem.
I'm decidedly novice here, and I'm sure I'm missing something obvious, but if anyone can help me figure out where to look I would greatly appreciate it. Lots of positive vibes for you. If I can add anything to clarify the problem please let me know.
Thanks,
Brian
I had the same problem when I updated to d3 v3.5.6. Here is what I did to check for null values, so that you don't try to access the [0] position of null:
.attr("cx", function(d) {
var coords = projection([d.lon, d.lat]);
if (coords) {
return coords[0];
}
})
I'm sure there is a cleaner way to do this, but it worked for me.
You have a little error in your function generating cx values which messes it all up. It's just one parenthesis in the wrong place:
.attr('cx', function(d){
return projection([d.lon, d.lat][0]);
})
By coding [d.lon, d.lat][0] you are just passing the first value of the array, which is d.lon, to the projection and are returning the result of projection() which is an array. Instead, you have to place the [0] outside the call of projection() because you want to access the value it returned. Check your function for cy where you got things right. Adjusting it as follows should yield the correct values for cx:
.attr('cx', function(d){
return projection([d.lon, d.lat])[0];
})

d3js rescale redraw barchart without new data

I have a simple (o.k. not so simple) barchart which shows the electric power consumption of one consumer (C1). I add the consumption of another consumer (C2) as line. The max consumption of C2 if higher then the max consumption of C1 so I have to rescale. I have solved this problem but not as beautiful I wanted to.
I calculate the new yMax, set the domain, rescale the axis (beautiful) remove all 'rect' and redraw (not beautiful). Is there a possibility to say: hey bars, I have a new scale, go down with a beautiful animation :)
Here the rescale method:
var rescale = function () {
//in this function the new _maxYValue is set
renderLineView();
var data = _data;
y.domain([_minYValue, _maxYValue]);
_svg.select(".y.axis")
.transition().duration(1500).ease("sin-in-out")
.call(yAxis());
_svg.selectAll("rect").remove();
var barWidth = getBarWidth(data.length);
var bars = d3.select("#layer_1").selectAll(".bar").data(data, function (d) {
return d.xValue;
});
bars.enter().append("rect")
.attr("class", "daybarincomplete")
.attr("x", function (d, i) {
return x(d.xValue) + 4;
})
.attr("width", barWidth)
.attr("y", function (d) {
return Math.min(y(0), y(d.value));
})
.attr("height", function (d) {
return Math.abs(y(d.value) - y(0));
});
}
Here is the jsfiddle: http://jsfiddle.net/axman/v4qc7/5/
thx in advance
©a-x-i
Use the .transition() call on bars, to determine the behaviour you want when the data changes (e.g. Bar heights change). You'd chain the .attr() function after it to set bar height etc.
To deal with data points that disappear between refreshes (e.g. You had 10 bars originally but now only have 9), chain the .exit().remove() functions to bars.
With both of the above, you can additionally chain something like .duration(200).ease('linear') to make it look all pretty.
pretty much what #ninjaPixel said. There's a easy to follow example here
http://examples.oreilly.com/0636920026938/chapter_09/05_transition.html

d3 - xScale not updating?

I'm trying to make a d3 scatterplot with two drop-down menus. The drop-down menus are used to select which datasets to plot against each other. I use two global variables to keep track of which datasets are currently used. "currentX" is the name of the first dataset (on the x-axis) and "currentY" is the name of the second dataset.
My scale functions depend on the values of "currentX" and "currentY". Here is an example of my xScale function:
var xScale = d3.scale.linear()
.domain([d3.min(dataset, function(d){return d.data[currentX]}), d3.max(dataset, function(d){return d.data[currentX]})
.range([padding, w - padding])
.nice();
My yScale function is the same, but uses currentY instead of currentX. My problem is that when I try to change views of the data, my scale doesn't update. Here is the code for changing between views of the data:
d3.selectAll('select')
.on('change', function() {
// Update currentX and currentY with the currently selected datasets
key = Object.keys(dataset[0].data)[this.selectedIndex];
if (this.getAttribute('id') == 'xSelect') {currentX = key}
if (this.getAttribute('id') == 'ySelect') {currentY = key}
// Change data used in the scatterplot
svg.selectAll('circle')
.data(dataset)
.transition()
.duration(1000)
.attr('cx', function(d) { return xScale(d.data[currentX]) })
.attr('cy', function(d) { return yScale(d.data[currentY]) })
.attr('r', 2)
};
I want the xScale and yScale functions to update, to reflect the new values of currentX and currentY. But for some reason, these functions are not updating. If anyone could help me fix this, I would really appreciate it! Thanks!
UPDATE: Just to clarify, my problem is that my xScale and yScale functions do not change, even though xCurrent and yCurrent (and their minimum and maximum values) have changed. For example, "console.log(xScale(-5))" always produces the same value. This value should change as xCurrent and yCurrent change! Thanks again.
UPDATE 2: The global variables "xCurrent" and "yCurrent" ARE being updated. Furthermore, if I define NEW xScale and yScale functions in the .on('change') function, then my scales are updated. This actually fixes my problem, but I would still like to know why I can't do it the other way. Still trying to learn D3!
You need to update the x scale domain inside your change function. Also, you can use d3.extent instead of d3.min and d3.max. For example:
.on('change', function () {
// Update currentX and currentY with the currently selected datasets
key = Object.keys(dataset[0].data)[this.selectedIndex];
if (this.getAttribute('id') == 'xSelect') {currentX = key}
if (this.getAttribute('id') == 'ySelect') {currentY = key}
xScale.domain(d3.extent(dataset, function(d){return d.data[currentX];}));
// Change data used in the scatterplot
svg.selectAll('circle')
.data(dataset)
.transition()
.duration(1000)
.attr('cx', function(d) { return xScale(d.data[currentX]) })
.attr('cy', function(d) { return yScale(d.data[currentY]) })
.attr('r', 2)
})
This is because the scale generator isn't aware of changes to your data. It's domain method isn't going to be called every time the data changes. Thus, when the data does change, you have to explicitly set the scale's domain before re-drawing any data that depends on it.

Categories