D3 Scatterplot returning xy = NaN - d3.js

I am getting the following console error:
Error: <circle> attribute cy: Expected length, "NaN".
This is resulting in my circle elements being assigned a value of 0 and being rendered at the top of the screen.
My circles are being passed a Date value as the cy rather than a number, and I have tried using both scaleTime() and scaleLinear() but not had the desired effect.
<circle cx="41.9047619047619" cy="NaN" r="6" class="dot" data-xvalue="1995" data-yvalue="Mon Jan 01 1900 00:36:50 GMT+0000 (Greenwich Mean Time)"></circle>
Code
let width = 500;
let height = 500;
let margin = {left: 20, right: 20, top: 20, bottom: 20};
let timeFormat = d3.timeParse("%M:%S");
// select container and render svg canvas, set height and width.
const chart = d3.select('#container')
.append('svg')
.attr('height', height)
.attr('width', width);
// Fetch data
d3.json('https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/cyclist-data.json').then(data => {
// Find highest and lowest years in dataset
const maxYear = d3.max(data, (d) => d.Year);
const minYear = d3.min(data, (d) => d.Year);
// Parse time data
data.forEach(d => {
let timeParser = d3.timeParse('%M:%S')
d.Time = timeParser(d.Time);
});
// Define Scales
let xScale = d3.scaleLinear() // try scaleTime afterwards to check
.domain([minYear, maxYear])
.range([margin.left, width - margin.right]);
let yScale = d3.scaleTime()
.domain([d3.extent(data, (d) => d.Time)])
.range([0, height - margin.top])
// Render circles for each data point
chart.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', (d) => xScale(d.Year))
.attr('cy', (d) => yScale(d.Time))
.attr('r', 6)
.attr('class', 'dot')
.attr('data-xvalue', (d) => d.Year)
.attr('data-yvalue', (d) => d.Time)
})
I have tried the following alternatives when I am parsing the time data but still getting the same error:
// Parse time data
data.forEach(d => {
let timeParser = d3.timeParse('%M:%S')
d["Time"] = timeParser(d.Time);
});
// Parse time data
data.forEach(d => {
let timeParser = d3.timeParse('%M:%S')
d.Time = new Date(timeParser(d.Time));
});
JSON data for reference:
{
"Time": "36:50",
"Place": 1,
"Seconds": 2210,
"Name": "Marco Pantani",
"Year": 1995,
"Nationality": "ITA",
"Doping": "Alleged drug use during 1995 due to high hematocrit levels",
"URL": "https://en.wikipedia.org/wiki/Marco_Pantani#Alleged_drug_use"
},
{
"Time": "36:55",
"Place": 2,
"Seconds": 2215,
"Name": "Marco Pantani",
"Year": 1997,
"Nationality": "ITA",
"Doping": "Alleged drug use during 1997 due to high hermatocrit levels",
"URL": "https://en.wikipedia.org/wiki/Marco_Pantani#Alleged_drug_use"
},

Related

d3 selection.exit().remove() not working, _exit is always array(0)

I have gone through quite a few tutorial but still couldn't work out how to correctly update data, any help would be highly appreciated, thank you!
For some reason, the code doesn't run properly in the snippet, I am quite new to use it,
what I want to achieve is by clicking the button, the data got updated, I tried to use selection.exit().remove, but the exit array is always empty, I don't really understand how that works
const arr = [{
name: "A",
dataset_1: 5,
dataset_2: 6,
},
{
name: "B",
dataset_1: 7,
dataset_2: 22,
},
{
name: "C",
dataset_1: 33,
dataset_2: 23,
},
{
name: "D",
dataset_1: 20,
dataset_2: 12,
},
{
name: "E",
dataset_1: 21,
dataset_2: 15,
},
{
name: "F",
dataset_1: 15,
dataset_2: 18,
},
];
//function for adding dataset
let counter = 2;
const add_set = (arr) => {
const ran = () => Math.floor(Math.random() * 20 + 1);
const add = (arr) => {
counter++;
arr.map((i) => (i[`dataset_${counter}`] = ran()));
};
add(arr);
};
//function for removing dataset
const remove_set = (arr) => {
arr.map((i) => delete i[`dataset_${counter}`]);
counter >= 1 ? counter-- : counter;
};
//draw area chart
const draw = () => {
//No.1 define place to draw
let svg = d3.select("svg"),
margin = {
left: 50,
right: 50,
top: 15,
bottom: 30
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg
.append("g")
.attr(
"transform",
"translate(" + margin.left + "," + margin.top + ")"
);
//No.2 set axes
let categoriesNames = arr.map((d) => d.name);
let xScale = d3.scalePoint().domain(categoriesNames).range([0, width]); // scalepoint make the axis starts with value compared with scaleBand
let copy = JSON.parse(JSON.stringify(arr));
copy.map((i) => delete i.name);
var yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, d3.max(copy, (i) => Math.max(...Object.values(i)))]);
let colorScale = d3.scaleOrdinal().range(d3.schemeSet3);
let dataSets = Object.keys(arr[0]).filter((i) => i !== "name");
colorScale.domain(dataSets);
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale));
g.append("g").attr("class", "y axis").call(d3.axisLeft(yScale));
g.exit().remove();
//No.3 draw chart
let areaData = dataSets.map((i) => {
return {
id: i,
values: arr.map((d) => {
return {
name: d.name,
value: d[i]
};
}),
};
});
let generator = d3
.area()
.curve(d3.curveMonotoneX)
.y0(yScale(0))
.x((d) => xScale(d.name))
.y1((d) => yScale(d.value));
let area = g
.selectAll(".area")
.data(areaData)
.enter()
.append("g")
.attr("class", (d) => `area${d.id}`);
area.exit().remove();
area
.append("path")
.attr("d", (d) => {
return generator(d.values);
})
.attr("opacity", 0.6)
.style("fill", (d) => colorScale(d.id));
};
//buttons
draw();
const update = () => {
add_set(arr);
draw();
};
const remove = () => {
remove_set(arr);
draw();
};
d3.select("body")
.append("button")
.text("Remove dataset")
.on("click", remove);
d3.select("body")
.append("button")
.text("Add dataset")
.on("click", update);
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<html>
<body>
<svg width="700" height="500"></svg>
</body>
</html>
selection.exit() is empty, because selection is a subset of nodes from g, and g is newly added every time you call draw. You need to separate one-time logic (draw axes, append container elements, set height/width/etc) from logic you want to run each time.
You should also see in your code that the colour of one of the areas changed. That was because another area was drawn in front of it. If something was really wrong, it would have repurposed the existing areas and failed to remove the one you didn't need. This is a pointer that you append something the second time that you shouldn't have appended!
But there was more. You use g.selectAll(".area"), append a g element, but don't give it class area. Instead, you give it class areadataset_1 and areadataset_2. This way, on subsequent calls, you can't find the area elements!
You also need to learn about .merge(). I recommend looking up some tutorials, like this one. You only updated the new paths, never the old ones. Since you didn't need g, I removed it for now, making the rest of the logic easier.
const arr = [{
name: "A",
dataset_1: 5,
dataset_2: 6,
},
{
name: "B",
dataset_1: 7,
dataset_2: 22,
},
{
name: "C",
dataset_1: 33,
dataset_2: 23,
},
{
name: "D",
dataset_1: 20,
dataset_2: 12,
},
{
name: "E",
dataset_1: 21,
dataset_2: 15,
},
{
name: "F",
dataset_1: 15,
dataset_2: 18,
},
];
//function for adding dataset
let counter = 2;
const add_set = (arr) => {
const ran = () => Math.floor(Math.random() * 20 + 1);
const add = (arr) => {
counter++;
arr.map((i) => (i[`dataset_${counter}`] = ran()));
};
add(arr);
};
//function for removing dataset
const remove_set = (arr) => {
arr.map((i) => delete i[`dataset_${counter}`]);
counter >= 1 ? counter-- : counter;
};
const margin = {
left: 50,
right: 50,
top: 15,
bottom: 30
};
let svg,
width,
height,
g;
const setup = () => {
//No.1 define place to draw
svg = d3.select("svg");
width = +svg.attr("width") - margin.left - margin.right;
height = +svg.attr("height") - margin.top - margin.bottom;
g = svg
.append("g")
.attr(
"transform",
"translate(" + margin.left + "," + margin.top + ")"
);
}
//draw area chart
const draw = () => {
//No.2 set axes
let categoriesNames = arr.map((d) => d.name);
let xScale = d3.scalePoint().domain(categoriesNames).range([0, width]); // scalepoint make the axis starts with value compared with scaleBand
let copy = JSON.parse(JSON.stringify(arr));
copy.map((i) => delete i.name);
var yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, d3.max(copy, (i) => Math.max(...Object.values(i)))]);
let colorScale = d3.scaleOrdinal().range(d3.schemeSet3);
let dataSets = Object.keys(arr[0]).filter((i) => i !== "name");
colorScale.domain(dataSets);
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale));
g.append("g").attr("class", "y axis").call(d3.axisLeft(yScale));
g.exit().remove();
//No.3 draw chart
let areaData = dataSets.map((i) => {
return {
id: i,
values: arr.map((d) => {
return {
name: d.name,
value: d[i]
};
}),
};
});
let generator = d3
.area()
.curve(d3.curveMonotoneX)
.y0(yScale(0))
.x((d) => xScale(d.name))
.y1((d) => yScale(d.value));
let area = g
.selectAll(".area")
.data(areaData);
area.exit().remove();
area
.enter()
.append("path")
.attr("class", (d) => `area area${d.id}`)
.attr("opacity", 0.6)
.merge(area)
.attr("d", (d) => {
return generator(d.values);
})
.style("fill", (d) => colorScale(d.id));
};
//buttons
setup();
draw();
const update = () => {
add_set(arr);
draw();
};
const remove = () => {
remove_set(arr);
draw();
};
d3.select("body")
.append("button")
.text("Remove dataset")
.on("click", remove);
d3.select("body")
.append("button")
.text("Add dataset")
.on("click", update);
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<html>
<body>
<svg width="700" height="500"></svg>
</body>
</html>
In my case the selectAll and append were using different types:
groups.selectAll('rect.bar-item')
///...
enter.append('path')
Making them the same rect or only path solved it for me.
Hope this helps someone.

Barchart "dates" are displaying strange / bars are pressed

I want to create a barchart displaying C02 emission.
The Problem (see picture below):
Why are the bars "pushed" to the right? Why are the years in the x-axis displayed without the first integer?
I am using Version 3 of d3.
Given some JSON data like this:
[
{
"Cement": 0.0,
"Gas Flaring": 0.0,
"Gas Fuel": 0.0,
"Liquid Fuel": 0.0,
"Per Capita": null,
"Solid Fuel": 3.0,
"Total": 3.0,
"Year": 1751
},
and so on…
]
To prepare for scaling I did:
var minDate = dataset[0].Year;
var maxDate = dataset[dataset.length - 1].Year;
var maxValue = d3.max(dataset, function(d) {
return d["Per Capita"];
});
I append the svg
var svg = d3
.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
I sacled the xAxis and the yAxis:
var xAxisScale = d3.time
.scale()
.domain([minDate, maxDate])
.range([0, w]);
var yAxisScale = d3.scale
.linear()
.domain([0, maxValue])
.range([h, 0]);
The I finally builded these axisses…
var xAxis = d3.svg
.axis()
.scale(xAxisScale)
.orient("bottom");
var yAxis = d3.svg
.axis()
.scale(yAxisScale)
.orient("left");
svg
.append("g")
.attr("class", "axis")
.attr("transform", "translate(92," + (h - padding) + ")")
.call(xAxis);
svg
.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",-90)")
.call(yAxis);
I also than addeded the rects…
svg
.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.style("fill", "teal")
.attr({
x: function(d, i) {
return i * (w / dataset.length);
},
y: function(d) {
return yAxisScale(d["Per Capita"]);
},
width: w / dataset.length,
height: function(d) {
return h - yAxisScale(d["Per Capita"]);
}
});
The result is not the intended one.
Could you please elaborate what went wrong?
Why are the bars "pushed" to the right?
Why are the years in the x-axis displayed without the first integer?
I am using Version 3 of d3.
Thank you very much!
The main problem here is that this...
"Year": 1751
... is not a date object. That's just a number. If you look at your axis you'll realise that.
So, you have to parse it. For instance:
const format = d3.time.format("%Y");
dataset.forEach(function(d){
d.Year = format.parse(d.Year);
});
Also, when you do this...
var minDate = dataset[0].Year;
var maxDate = dataset[dataset.length - 1].Year;
... you're blindly trusting that the array is sorted. Don't do that. Instead, do:
var minDate = d3.max(dataset, function(d){
return d.Year
});
var maxDate = d3.min(dataset, function(d){
return d.Year
});
Or, if you want to use destructuring:
var [minDate, maxDate] = d3.extent(dataset, d => d.Year);
Finally, now that you have a proper scale, don't use the indices for the x position. Use the scale:
x: function(d) {
return xAxisScale(d.Year);
},
This covers the problem regarding the x position. For fixing the y position, just set a proper margin.

D3 y(number) returns NaN

I've been banging my head against a wall trying to figure this one out. My normally reliable Google-Fu has failed me. Nothing on I've found on SO has pointed me in the right direction. Would love some help on this.
I'm building a grouped bar chart. For some reason, y(number) will only give me NaN even though I am passing it a number.
Code below:
const data = [
{
'provider': 'twitter',
'likes': 2362,
'comments': 107,
'shares': 1129
},
{
'provider': 'facebook',
'likes': 2783,
'comments': 148,
'shares': 1069
},
{
'provider': 'instagram',
'likes': 1878,
'comments': 101,
'shares': 1032
},
{
'provider': 'tumblr',
'likes': 2965,
'comments': 147,
'shares': 1393
}
]
const margin = {top: 10, right: 10, bottom: 10, left: 10},
width = 628 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom
const x0 = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(0.1)
const x1 = d3.scaleBand()
.padding(0.1)
const y = d3.scaleLinear()
.rangeRound([height, 0])
const keys = d3.keys(data[0]).filter((key) => (key !== 'provider'))
console.log('keys ', keys) // keys ["likes","comments","shares"]
data.forEach((d) => {
d.values = keys.map((name) => ({name: name, value: +d[name]}))
})
x0.domain(data.map(d => d.provider))
x1.domain(keys).range([0, x0.bandwidth()])
y.domain(0, d3.max(data, (d) => (d3.max(d.values, (d) => (d.value)))))
console.log('max', d3.max(data, (d) => (d3.max(d.values, (d) => (d.value))))) // max 2965
const svg = d3.select('body').append('svg')
.attr('width', 628)
.attr('height', 300)
const provider = svg.selectAll('.provider')
.data(data)
.enter().append('g')
.attr('class', d => 'provider-' + d.provider)
.attr('transform', d => `translate(${x0(d.provider)}, 0)`)
provider.selectAll('rect')
.data(d => d.values)
.enter().append('rect')
.attr('width', x1.bandwidth())
.attr('x', d => x1(d.name))
.attr('y', d => {
console.log('d.value ', d.value) // d.value 2362
console.log('y(d.value) ', y(d.value)) // NaN
})
The domainmethod expects an array:
y.domain([0,
d3.max(data, (d) => (d3.max(d.values, (d) => (d.value))))
])

Why wont my d3 arcs transition?

I'm trying to make arcs (on a geographic projection) which start at one point and grow to another. However, the transition never seems to occur - the arcs just appear fully formed! Any ideas why this is?
jsfiddle here
leeroyjenkins = function(){
var data = [{"lat": 0, "lon": 0},
{"lat": 10, "lon": 10},
{"lat": 20, "lon": 20},
{"lat": 30, "lon": 30},
{"lat": 40, "lon": 40},]
main(data);
};
var main = function(points) {
var width = 960,
height = 500;
var startpt = {"lat": 15, "lon": 15};
var projection = d3.geo.orthographic()
.scale(250)
.translate([width / 2, height / 2])
.clipAngle(90);
var arcpath = function (startpt, endpt) {
var p = d3.geo.path()
.projection(projection);
return p({type: "LineString", coordinates: [[startpt.lon, startpt.lat],
[endpt.lon, endpt.lat]]});
}
var λ = d3.scale.linear()
.domain([0, width])
.range([-180, 180]);
var φ = d3.scale.linear()
.domain([0, height])
.range([90, -90]);
var drag = d3.behavior.drag()
.origin(function() { var r = projection.rotate();
return {x: λ.invert(r[0]), y: φ.invert(r[1])}; })
.on("drag", function() {
projection.rotate([λ(d3.event.x), φ(d3.event.y)]);
svg.selectAll("path.arc").attr("d", function(d) {return arcpath(d, startpt)});
});
var tooltip = d3.select("body")
.append("div")
.attr("id", "tooltip");
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(drag);
svg.append("path")
.attr("id", "outline")
.datum({type: "Sphere"})
.attr("d", d3.geo.path().projection(projection));
svg.selectAll("path.arc")
.data(points)
.enter()
.append("path")
.attr("class", "arc")
.attr("d", function (d) {return arcpath(d, d)})
.transition()
.duration(1000)
.attr("d", function (d) {return arcpath(d, startpt)});
};
leeroyjenkins();

How do you allow d3js plot to run off screen

Using Mike Bostocks example of Path Transitions I have created a line graph to show data with an accompanying plot of icons underneath to call attention to elements in the graph. Here is a JSFiddle showing the result and the relevant code is:
[note: for demonstration I'm appending an image with every data point here, this may not be the case in production]
var data = [
{ "id": 0, "elevation": 90 },
{ "id": 1, "elevation": 73 },
{ "id": 2, "elevation": 70 },
{ "id": 3, "elevation": 59 },
{ "id": 4, "elevation": 63 },
{ "id": 5, "elevation": 65 },
{ "id": 6, "elevation": 61 },
{ "id": 7, "elevation": 59 },
{ "id": 8, "elevation": 60 },
{ "id": 9, "elevation": 62 },
{ "id": 10, "elevation": 64 }
];
var key = function (d) {
return d.id;
};
var xScale = d3.scale.linear()
.domain([0, data.length - 1])
.range([0, settings.containerWidth - 16]);
var yScale = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d.elevation; })])
.range([settings.containerHeight, 0]);
var svg = d3.select("#animation-container")
.append("svg")
.attr("width", settings.containerWidth)
.attr("height", settings.containerHeight);
var line = d3.svg.line()
.x(function(d, i) { return xScale(i); })
.y(function(d) { return yScale(d.elevation); })
.interpolate("basis");
svg.append("g")
.append("path")
.datum(data, key)
.attr("class", "line")
.attr("d", line);
svg.append("g")
.selectAll(".data-points")
.data(data, key)
.enter()
.append('image')
.attr("x", function (d, i) { return xScale(i); })
.attr("y", settings.iconLine)
.attr("xlink:href", "https://github.com/favicon.ico")
.attr("width", settings.iconWidth)
.attr("height", settings.iconHeight);
My thinking is that the solutions is be related to getting the horizontal scaling correct but I have tried many variations of linear and ordinal scales and transitions vs updating base data to make this work without success.
My question is:
How do I allow both the graph and icons to run off screen?
How do I keep the icons touching each other rather than gaps between them
Where my code differs from Mikes excellent example is that I don't want to
remove the offscreen data as my user will scroll back and forth.
Thanks.
EDIT:
As per #LarsKotthoff suggestion below I amended the range() of the xScale to the following:
var xScale = d3.scale.linear()
.domain([0, data.length - 1])
.range([0, (data.length - 1) * settings.iconWidth]);
This has a double effect:
Don't set the upper bound of the range to the screen as it will scale to the screen. Set it to the length of data. The result is if you have more data than svg width it will run off the edge of the svg boundary naturally.
Have all your icons joined together by creating a range that is a multiple of their sizes i.e. multiplying the range by the icon size.
Here's is the updated JSFiddle
To make a plot extend beyond the border of the SVG, all you need to do is adjust the range of the scales you're using. For example, your x scale is set up as
var xScale = d3.scale.linear()
.domain([0, data.length - 1])
.range([0, settings.containerWidth - 16]);
The range determines where things are drawn on the SVG and is currently restricted to the width of the SVG. To make it extend beyond that, change it to e.g.
var xScale = d3.scale.linear()
.domain([0, data.length - 1])
.range([0, 2 * settings.containerWidth - 16]);
which would make the x scale cover twice the width of the actual SVG -- that is, half of the graph would be visible, while the other half would be "off screen".
Complete demo here.
Note d3.scale.linear is from v3. It changed to d3.scaleLinear in v4:
'scale' and 'svg' does not exist in "node_modules/#types/d3/index"

Resources