Related
I am trying to draw a scatter plot chart with D3 from a csv file. The csv looks something like this:
location smokeSomeDays allFamPovPct
AL 5% 15%
AK 5% 8%
AZ 4% 13%
AR 6% 14%
CA 4% 12%
CO 4% 8%
CT 4% 8%
DE 4% 9%
DC 5% 14%
...
...
...
I want the Y-Axis to be 'smokeSomeDays' and the X-Axis to be 'allFamPovPct'
Here is my js that evokes the "Cannot read property 'getAttribute' of null" error, even though 'getAttribute' is not mentioned anywhere in the code:
var svgW = 960;
var svgH = 500;
var margin = { top: 20, right: 40, bottom: 60, left: 50 };
var chartWidth = svgW - margin.left - margin.right;
var chartHeight = svgH - margin.top - margin.bottom;
var svg = d3.select("body")
.append("svg")
.attr("width", svgW)
.attr("height", svgH)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv('data.csv', function(data) {
data.forEach(function () {
// Scales
var xScale = d3.scaleLinear()
.domain([
d3.min([0,d3.min(data,function (d) { return d.allFamPovPct })]),
d3.max([0,d3.max(data,function (d) { return d.allFamPovPct })])
])
.range([0,chartWidth])
var yScale = d3.scaleLinear()
.domain([
d3.min([0,d3.min(data,function (d) { return d.smokeSomeDays })]),
d3.max([0,d3.max(data,function (d) { return d.smokeSomeDays })])
])
.range([chartHeight,0])
// X-axis
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5)
// Y-axis
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5)
// Circles
var circles = svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx',function (d) { return xScale(d.allFamPovPct) })
.attr('cy',function (d) { return yScale(d.smokeSomeDays) })
.attr('r','10')
.attr('stroke','black')
.attr('stroke-width',1)
.attr('fill')
svg.append('g')
.attr('class','axis')
.attr('transform', 'translate(0,' + chartHeight + ')')
.call(xAxis)
.append('text') // X-axis Label
.attr('class','label')
.attr('y',-10)
.attr('x',chartWidth)
svg.append('g')
.attr('class', 'axis')
.call(yAxis)
.append('text') // y-axis Label
.attr('class','label')
.attr('transform','rotate(-90)')
.attr('x',0)
.attr('y',5)
});
});
The full error msg looks like this:
Uncaught TypeError: Cannot read property 'getAttribute' of null d3.min.js:4
at dt.ul [as attr] (d3.min.js:4)
at app.js:49
at Array.forEach (<anonymous>)
at app.js:17
at d3.min.js:3
at Object.<anonymous> (d3.min.js:7)
at v.call (d3.min.js:4)
at XMLHttpRequest.e (d3.min.js:7)
But all the lines mentioned in the error looks good to me, maybe I'm missing something.
The code also doesn't produce a scatter plot that I want. It gives me something like this:
|
|
|
|
|
|
|
|
|
O ---------------------------------------
Here is my html, if it helps:
<!DOCTYPE html>
<html>
<head>
<title>Data Journalism</title>
<style>
.chart {
}
.main text {
font: 10px sans-serif;
}
.axis circle, .axis path {
shape-rendering: crispEdges;
stroke: black;
fill: none;
}
circle {
fill: steelblue;
}
</style>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.7.3/d3.min.js">
</script>
</head>
<body>
<div class='content'></div>
<script type="text/javascript" src="app.js"></script>
</body>
</html>
Thanks for your help in advance!
The error you get right now is due to the fact that...
.attr("fill")
... is a getter, not a setter. Have in mind that this error is thrown by D3 library, but your console shows the line of your code that is the actual culprit.
However, this is the least of your problems here.
The first big problem is that forEach inside the callback. That is completely unnecessary in a D3 code. Get rid of it.
The second problem is that smokeSomeDays and allFamPovPct are strings, not numbers. You have to convert them to numbers to use your scale. Here, in this example, I'm using a row function:
function(d){
d.smokeSomeDays = +d.smokeSomeDays.split("%")[0];
d.allFamPovPct = +d.allFamPovPct.split("%")[0];
return d;
}
Here is your code with those changes (and just a couple of rows of your CSV):
var csv = `location,smokeSomeDays,allFamPovPct
AL,5%,15%
AK,5%,8%
AZ,4%,13%
AR,6%,14%`;
var svgW = 400;
var svgH = 300;
var margin = {
top: 20,
right: 40,
bottom: 60,
left: 50
};
var chartWidth = svgW - margin.left - margin.right;
var chartHeight = svgH - margin.top - margin.bottom;
var svg = d3.select("body")
.append("svg")
.attr("width", svgW)
.attr("height", svgH)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var data = d3.csvParse(csv, function(d) {
d.smokeSomeDays = +d.smokeSomeDays.split("%")[0];
d.allFamPovPct = +d.allFamPovPct.split("%")[0];
return d;
})
var xScale = d3.scaleLinear()
.domain([
d3.min([0, d3.min(data, function(d) {
return d.allFamPovPct
})]),
d3.max([0, d3.max(data, function(d) {
return d.allFamPovPct
})])
])
.range([0, chartWidth])
var yScale = d3.scaleLinear()
.domain([
d3.min([0, d3.min(data, function(d) {
return d.smokeSomeDays
})]),
d3.max([0, d3.max(data, function(d) {
return d.smokeSomeDays
})])
])
.range([chartHeight, 0])
// X-axis
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5)
// Y-axis
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5)
// Circles
var circles = svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', function(d) {
return xScale(d.allFamPovPct)
})
.attr('cy', function(d) {
return yScale(d.smokeSomeDays)
})
.attr('r', '10')
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr("fill", "teal");
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0,' + chartHeight + ')')
.call(xAxis)
.append('text') // X-axis Label
.attr('class', 'label')
.attr('y', -10)
.attr('x', chartWidth)
svg.append('g')
.attr('class', 'axis')
.call(yAxis)
.append('text') // y-axis Label
.attr('class', 'label')
.attr('transform', 'rotate(-90)')
.attr('x', 0)
.attr('y', 5)
<script src="https://d3js.org/d3.v4.min.js"></script>
It might be issue with loading library multiple times/initialising same id multiple times
I am trying to create a scatter plot with d3.js (v4). I am a newbie in d3, and thanks to the limited documentation of examples with v4, am struggling to create the plot (already have asked here once before). My code is given below:
const margin = { top: 100, right: 50, left: 50, bottom: 50};
const width = 1300 - margin.right - margin.left;
const height = 1250 - margin.top - margin.bottom;
d3.csv("http://localhost:9000/data.csv", (error, data) => {
if (error) throw error;
const x = (d) => d["Category"];
const xScale = d3.scalePoint()
.domain(data.map((d) => d["Category"]))
.range([0, width]);
const xMap = (d) => xScale(x(d));
const xAxis = d3.axisBottom().scale(xScale);
// Plotting Score 1 for now
const y = (d) => d["Score 1"];
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, y)])
.range([height, 0]);
const yMap = (d) => yScale(y(d))
const yAxis = d3.axisLeft().scale(yScale);
const svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
svg.append('g')
.attr('class', 'axis axis--x')
.call(xAxis)
.attr('transform', 'translate(0, 800)')
.append('text')
.attr('class', 'label')
.attr('x', width)
.attr('y', -6)
.style('text-anchor', 'middle')
.text('Category');
svg.append('g')
.attr('class', 'axis axis--y')
.call(yAxis)
.attr('transform', 'rotate(-90)')
.attr('y', 0 - margin.left)
.attr('x', 0 - (height/2))
.attr("dy", "1em")
.style('text-anchor', 'middle')
.text('Score');
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('class', 'dot')
.attr('cx', xMap)
.attr('cy', yMap)
.attr('r', 3.5)
.attr('fill', 'red');
});
A few lines of data.csv are given:
Name,Category,Score 1,Score 2,Score 3,Average score
A1_P1,A01,2.3,2.4,4.1,2.4
A2_P1,A02,1.4,1.5,2.4,1.5
A3_P1,A03,0.9,0.9,0.9,0.9
A4_P1,B01,1.5,1.5,1,1.5
A5_P1,B02,1.2,1.2,1.4,1.2
A6_P1,B03,1,1,1.1,1
A7_P1,C01,1.6,1.2,1,1.2
A8_P1,C02,1.2,1.2,0.9,1.2
A9_P1,C03,1.1,1.1,1,1.1
A10_P1,D01,1.5,1.6,1.1,1.5
The x-axis is showing (but not the 'Category' label), and even more importantly, the y-axis is not showing at all. The points themselves are being shown correctly, though. Does anyone know what is wrong with my y-axis setting and axis labels? Thanks in advance!
When I start a new chart with d3, I find it best to start with a known simple set-up example to place my drawing g, axis, etc... This is my usual starting point.
That said, here's list of issues I see in your chart:
Not placing x-axis dynamically.
svg container is sized wrong and ends up on top of axis.
Attempting to append a text axis label to y axis but you never actually append the text (and then apply attributes and styling to the axis itself instead of that missing text element).
Cleaning this all up looks like this:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
const margin = {
top: 100,
right: 50,
left: 50,
bottom: 50
};
const width = 500 - margin.right - margin.left;
const height = 500 - margin.top - margin.bottom;
//d3.csv("data.csv", (error, data) => {
// if (error) throw error;
var data = [{"Name":"A1_P1","Category":"A01","Score 1":"2.3","Score 2":"2.4","Score 3":"4.1","Average score":"2.4"},{"Name":"A2_P1","Category":"A02","Score 1":"1.4","Score 2":"1.5","Score 3":"2.4","Average score":"1.5"},{"Name":"A3_P1","Category":"A03","Score 1":"0.9","Score 2":"0.9","Score 3":"0.9","Average score":"0.9"},{"Name":"A4_P1","Category":"B01","Score 1":"1.5","Score 2":"1.5","Score 3":"1","Average score":"1.5"},{"Name":"A5_P1","Category":"B02","Score 1":"1.2","Score 2":"1.2","Score 3":"1.4","Average score":"1.2"},{"Name":"A6_P1","Category":"B03","Score 1":"1","Score 2":"1","Score 3":"1.1","Average score":"1"},{"Name":"A7_P1","Category":"C01","Score 1":"1.6","Score 2":"1.2","Score 3":"1","Average score":"1.2"},{"Name":"A8_P1","Category":"C02","Score 1":"1.2","Score 2":"1.2","Score 3":"0.9","Average score":"1.2"},{"Name":"A9_P1","Category":"C03","Score 1":"1.1","Score 2":"1.1","Score 3":"1","Average score":"1.1"},{"Name":"A10_P1","Category":"D01","Score 1":"1.5","Score 2":"1.6","Score 3":"1.1","Average score":"1.5"}];
const x = (d) => d["Category"];
const xScale = d3.scalePoint()
.domain(data.map((d) => d["Category"]))
.range([0, width]);
const xMap = (d) => xScale(x(d));
const xAxis = d3.axisBottom().scale(xScale);
// Plotting Score 1 for now
const y = (d) => d["Score 1"];
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, y)])
.range([height, 0]);
const yMap = (d) => yScale(y(d))
const yAxis = d3.axisLeft().scale(yScale);
const svg = d3.select('body')
.append('svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
svg.append('g')
.attr('class', 'axis axis--x')
.call(xAxis)
.attr('transform', 'translate(0,' + height + ')')
.append('text')
.attr('class', 'label')
.attr('x', width)
.attr('y', -6)
.style('text-anchor', 'middle')
.text('Category');
svg.append('g')
.attr('class', 'axis axis--y')
.call(yAxis)
.append("text")
.attr('transform', 'rotate(-90)')
.attr('y', 0 - margin.left)
.attr('x', 0 - (height / 2))
.attr("dy", "1em")
.style('text-anchor', 'middle')
.text('Score')
.style('fill', 'black')
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('class', 'dot')
.attr('cx', xMap)
.attr('cy', yMap)
.attr('r', 3.5)
.attr('fill', 'red');
//});
</script>
</body>
</html>
I create a scatterplot which is defined on the following data (note that only first two fields are currently using for plotting):
var data = [[5,3,"{'text':'word1',size:4},{'text':'word2','size':1}"],
[3,5,"{'text':'word3',size:5},{'text':'word4','size':4}"],
[1,4,"{'text':'word1',size:3},{'text':'word2','size':5},{'text':'word3','size':2}"],
[2,3,"{'text':'word2',size:1},{'text':'word3','size':5}"]];
Next, when we click on each particular point in the scatterplot the application should attach a wordcloud which is defined from words stored in the 3rd field of the data variable. I use Jason Davies's implementation of wordcloud. Currently (for demo purposes), the wordcloud is generating onlyfrom the static data stored in variable frequency_list. The current code is also stored on JSFiddle.
Any idea how to proceed?
var data = [[5,3,"{'text':'word1',size:4},{'text':'word2','size':1}"],
[3,5,"{'text':'word3',size:5},{'text':'word4','size':4}"],
[1,4,"{'text':'word1',size:3},{'text':'word2','size':5},{'text':'word3','size':2}"],
[2,3,"{'text':'word2',size:1},{'text':'word3','size':5}"]];
var margin = {top: 20, right: 15, bottom: 60, left: 60},
width = 500 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d[0]; })])
.range([ 0, width ]);
var y = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d[1]; })])
.range([ height, 0 ]);
var chart = d3.select('body')
.append('svg:svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart')
var main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main')
// Draw the x axis
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'main axis date')
.call(xAxis);
// draw the y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'main axis date')
.call(yAxis);
var g = main.append("svg:g");
g.selectAll("scatter-dots")
.data(data)
.enter().append("svg:circle")
.attr("cx", function (d,i) { return x(d[0]); } )
.attr("cy", function (d) { return y(d[1]); } )
.attr("r", 5)
.on("mouseover", function(){d3.select(this).style("fill", "red")})
.on("mouseout", function(){d3.select(this).style("fill", "black")});
// FUNCTION TO DISPLAY CIRCLE
g.on('mouseover', function(){
div.style("display", "block")
d3.select("krog").style("fill", "orange");
generate();
});
g.on('mouseout', function(){
//div.style("display", "none")
div.select("svg").remove();
});
var div = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("display", "none");
// Functions to draw wordcloud
var frequency_list = [{"text":"study","size":40},{"text":"motion","size":15},{"text":"forces","size":10},{"text":"electricity","size":15},{"text":"movement","size":10},{"text":"relation","size":5},{"text":"things","size":10},{"text":"force","size":5},{"text":"ad","size":5}];
var color = d3.scale.linear()
.domain([0,1,2,3,4,5,6,10,15,20,100])
.range(["#ddd", "#ccc", "#bbb", "#aaa", "#999", "#888", "#777", "#666", "#555", "#444", "#333", "#222"]);
// Generates wordcloud
function generate(){
d3.layout.cloud().size([800, 300])
.words(frequency_list)
.rotate(0)
.fontSize(function(d) { return d.size; })
.on("end", draw)
.start();
}
function draw(words) {
d3.select("div").append("svg")
.attr("width", 850)
.attr("height", 350)
.attr("class", "wordcloud")
.append("g")
// without the transform, words words would get cutoff to the left and top, they would
// appear outside of the SVG area
.attr("transform", "translate(320,200)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("font-size", function(d) { return d.size + "px"; })
.style("fill", function(d, i) { return color(i); })
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
})
.text(function(d) { return d.text; });
}
You have a couple of problems here.
First, your data has strings for the words. I changed that for an array of objects:
var data = [[5,3,[{'text':'word1',size:4},{'text':'word2','size':1}]],
[3,5,[{'text':'word3',size:5},{'text':'word4','size':4}]],
[1,4,[{'text':'word1',size:3},{'text':'word2','size':5},{'text':'word3','size':2}]],
[2,3,[{'text':'word2',size:1},{'text':'word3','size':5}]]];
After that, I changed the function draw: instead of appending a new div every time you hover a circle, it just change the div content:
div.append("svg")
.attr("width", 300)
.attr("height", 300)
.attr("class", "wordcloud")
.append("g")
But now comes the most important change:
You are displaying the wordcloud every time the user hover a circle, but you're calling the mouseover for the group element. That way, we cannot access the data bound to each specific circle.
Instead of that, we'll set a variable for the circles:
var circle = g.selectAll("scatter-dots")
.data(data)
.enter()
.append("svg:circle");
Thus, we can get the data for each hovered circle, which is the third element in the array:
circle.on('mouseover', function(d){
div.style("display", "block")
d3.select("krog").style("fill", "orange");
generate(d[2]);//here, d[2] is the third element in the data array
});
And we pass this third element (d[2]) to the function generate as a parameter named thisWords:
function generate(thisWords){
d3.layout.cloud().size([800, 300])
.words(thisWords)
.rotate(0)
.fontSize(function(d) { return d.size; })
.on("end", draw)
.start();
}
here is your fiddle: https://jsfiddle.net/jwrbps4j/
PS: you'll have to improve the translate for that words.
I have a simple script (also on JSFiddle) which draws scatterplot from given data. When I go over the data point on the scatter the script should display red circle below my chart; and vice versa, when the event "mouseout" is called the circle should disappear.
Now the red circle is shown when the "mouseover" event is called, but the circle appends to other circles. I wonder how to properly implement show/hide functionality in this case.
The code is pasted below.
var data = [[4,3], [3,3], [1,4], [2,3]];
var margin = {top: 20, right: 15, bottom: 60, left: 60},
width = 500 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d[0]; })])
.range([ 0, width ]);
var y = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d[1]; })])
.range([ height, 0 ]);
var chart = d3.select('body')
.append('svg:svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart')
var main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main')
// Draw the x axis
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'main axis date')
.call(xAxis);
// draw the y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'main axis date')
.call(yAxis);
var g = main.append("svg:g");
g.selectAll("scatter-dots")
.data(data)
.enter().append("svg:circle")
.attr("cx", function (d,i) { return x(d[0]); } )
.attr("cy", function (d) { return y(d[1]); } )
.attr("r", 5);
// FUNCTION TO DISPLAY CIRCLE BELO CHART
g.on('mouseover', function(){
div.style("display", "block")
div.append("svg")
.attr("width", 50)
.attr("height", 50)
.append("circle")
.attr("cx", 25)
.attr("cy", 25)
.attr("r", 25)
.style("fill", "red");
});
g.on('mouseout', function(){
div.style("display", "none")
});
var div = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("display", "none");
You're appending a new SVG every time you hover the circle.
An easy and lazy solution is removing the SVG on "mouseout":
g.on('mouseout', function(){
div.select("svg").remove();
});
Here is your fiddle: https://jsfiddle.net/39pmwzzh/
I currently have a working scatter plot that I make using this
var data = (an array of arrays with two integers in each array)
var margin = {top: 20, right: 15, bottom: 60, left: 60}
, width = 300 - margin.left - margin.right
, height = 250 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d[0]; })])
.range([ 0, width ]);
var y = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d[1]; })])
.range([ height, 0 ]);
var chart = d3.select('.scatterGraph')
.append('svg:svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart')
var main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main')
// draw the x axis
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom')
.ticks(5);
main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'main axis date')
.call(xAxis);
// draw the y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'main axis date')
.call(yAxis);
var g = main.append("svg:g");
g.selectAll("scatter-dots")
.data(data)
.enter().append("svg:circle")
.attr("cx", function (d,i) { return x(d[0]); } )
.attr("cy", function (d) { return y(d[1]); } )
.attr("r", 2);
I was wondering how I could add a line graph (or alternatively another scatter plot) to this graph. I'm very new to d3 so I'm currently lost on how to do it. For example I would just want to add a line described by a function y = 2t where t is the x axis of the scatterplot. Thanks!
If it is as simple as a line described by a function y=2t you can just append a line element to your chart in this case main like this, assuming that your width is at least greater than twice your height
main.append("line").attr("x1", 0).attr("x2", height/2)
.attr("y1", height).attr("y2", 0);
But if you have a line that connected through multiple points, you will need to add a path element to your svg, and use d3.svg.line() function to generate its d attribute. So something like this,
var lineFunction = d3.svg.line().x(function (d) { x(d[0])}; )
.y(function (d) { y(d[1])}; );
var gLine = main.append("g");
gLine.append("path").attr("d", lineFunction(data));
For another scatter plot, you can reuse
var g = main.append("svg:g");
g.selectAll("scatter-dots")
.data(data2)
.enter().append("svg:circle")
.attr("cx", function (d,i) { return x(d[0]); } )
.attr("cy", function (d) { return y(d[1]); } )
.attr("r", 2);
but with a different set of data, and different accessor functions or scales if needed.