I'm brand new to D3.js, so my apologies in advance if I'm overlooking something obvious, but I cannot for the life of me figure out what's not working here...
I've been inching my way through Scott Murray's Interactive Data Visualization for the Web and I've hit a block at Chapter 7. My goal is to load some simple data from a csv document, parse the first column as Date data, and then display. The head of the csv looks like this...
Date, Amount
9/15/17, 26
11/9/17, 31
11/30/17, 23
12/21/17, 26
2/7/18, 23
I run the code below (which comes directly from the book!) and...none of that seems to be happening. When I enter "dataset" into the console, it only seems to include the final row:
{Date: "01/31/17", Amount: "23"}
Amount: "23"
Date: "01/31/17"
I'm perplexed since, again, the code itself comes from the book. I've been working through previous chapters just fine until now. And yes, I'm running a local server. Any idea what I'm doing wrong here? Thanks very much in advance!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3: Time scale</title>
<script type="text/javascript" src="../d3.js"></script>
<style type="text/css">
/* No style rules here yet */
</style>
</head>
<body>
<script type="text/javascript">
//Width and height
var w = 500;
var h = 300;
var padding = 40;
var dataset, xScale, yScale;
//For converting strings to Dates
var parseTime = d3.timeParse("%m/%d/%y");
//Function for converting CSV values from strings to Dates and numbers
var rowConverter = function(d) {
return {
Date: parseTime(d.Date),
Amount: parseInt(d.Amount)
};
}
//Load in the data
d3.csv("time_scale_data.csv", rowConverter, function(data) {
//Copy data into global dataset
dataset = data;
//Create scale functions
xScale = d3.scaleTime()
.domain([
d3.min(dataset, function(d) { return d.Date; }),
d3.max(dataset, function(d) { return d.Date; })
])
.range([padding, w - padding]);
yScale = d3.scaleLinear()
.domain([
d3.min(dataset, function(d) { return d.Amount; }),
d3.max(dataset, function(d) { return d.Amount; })
])
.range([h - padding, padding]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Generate date labels first, so they are in back
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return formatTime(d.Date);
})
.attr("x", function(d) {
return xScale(d.Date) + 4;
})
.attr("y", function(d) {
return yScale(d.Amount) + 4;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "#bbb");
//Generate circles last, so they appear in front
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d.Date);
})
.attr("cy", function(d) {
return yScale(d.Amount);
})
.attr("r", 2);
});
</script>
</body>
</html>
Instead of :
d3.csv("time_scale_data.csv", rowConverter, function(data) {
--- your code ---
});
Try this :
d3.csv("time_scale_data.csv", rowConverter).then(data){
--- your code ---
});
Related
I've created a horizontal bar chart in d3 and I'm trying to implement a sort function to order both the bars and their corresponding labels. The chart so far looks like this:
Clicking on the "Sort" button sorts the bars properly, but doesn't move the labels.
The data for chart looks like this
const data = [
{
COL_DIV_CODE: 'Academic Affairs',
avg_base: 67778.9,
},
{
COL_DIV_CODE: 'Finance and Administration',
avg_base: 75000.1,
},
{
COL_DIV_CODE: 'Arts and Humanities, College of',
avg_base: 68109.0,
},
];
Here's a full demo of the code so far: bl.ocks.org
Specifically this part:
d3.select("#byValue").on("click", function() {
data.sort(function(a,b) {
return d3.descending(a.avg_base, b.avg_base);
});
yScale.domain(data.map(function(d) {
return d.avg_base;
}));
// Move bars
svg.selectAll(".bar")
.transition()
.duration(500)
.attr("y", function(d, i) {
console.log("bar: ", d.avg_base, " ", yScale(d.avg_base));
return yScale(d.avg_base);
});
// Move bar labels
svg.selectAll(".bar-label")
.transition()
.duration(500)
.attr("y", function(d) {
var obj = findObjectByCollegeName(d, data);
return yScale(obj.COL_DIV_CODE) + yScale.bandwidth() / 2 - 8;
});
});
Appreciate any help!
Firstly, try not to manually change nodes that you didn't create yourself -- namely axis labels. Usually, the proper approach is to select the container you applied axis to, and just use the call(axis) on it again.
Secondly, there isn't a real reason for you to change the domain field for the scale; you do need to reapply it because the order changed, but you can reuse COL_DIV_CODE. Particularly important, because it seems axis uses the field to identify labels before and after the change (call(axis)).
Some minor things -- use alternatives to attr where available, since it always overwrites everything, when sometimes you might want to only change parts. I'm talking about assigning classes (use classed), and styles (styled).
Here is your bl.ocks code with minimal changes
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<button id="byValue">Sort by Value</button>
<script>
const data = [
{
COL_DIV_CODE: 'Academic Affairs',
avg_base: 67778.9,
},
{
COL_DIV_CODE: 'Finance and Administration',
avg_base: 75000.1,
},
{
COL_DIV_CODE: 'Arts and Humanities, College of',
avg_base: 68109.0,
},
];
const maxObj = data.reduce(function(max, obj) {
return obj.avg_base > max.avg_base? obj : max;
});
function findObjectByCollegeName(name, data) {
for (var i=0;i<data.length;i++) {
if (data[i].COL_DIV_CODE == name) {
return data[i]
}
}
}
const heightMargin = 120;
const widthMargin = 300;
const width = 1200 - 2 * widthMargin;
const height = 400 - 2 * heightMargin;
var svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500)
const chart = svg.append('g')
.attr('transform', `translate(${widthMargin}, ${heightMargin})`);
// Draw X axis
const xScale = d3.scaleLinear()
.range([width, 0])
.domain([maxObj.avg_base, 0]);
chart.append('g')
.attr('transform', `translate(0, 0)`)
.call(d3.axisTop(xScale));
// Draw Y axis
const yScale = d3.scaleBand()
.range([0, height])
.domain(data.map((s) => s.COL_DIV_CODE))
.padding(0.2);
var axisLeft = d3.axisLeft(yScale).tickFormat(function(d) {return d; });
chart.append('g')
.attr('transform', `translate(0, 0)`)
.attr('class', 'y-axis')
.call(axisLeft);
d3.selectAll(".y-axis .tick text")
.attr("class", "bar-label"); // Add a class to the bar labels
// Draw gridlines - vertical
chart.append('g')
.attr('class', 'grid')
.call(d3.axisTop()
.scale(xScale)
.tickSize(-height, 0, 0)
.tickFormat(''));
// // Draw bars
chart.selectAll()
.data(data)
.enter()
.append('rect')
.attr("class","bar")
.attr('style', 'fill: steelblue')
.attr('y', (s) => yScale(s.COL_DIV_CODE))
.attr('x', 0)
.attr('width', (s) => xScale(s.avg_base))
.attr('height', yScale.bandwidth());
d3.select("#byValue").on("click", function() {
data.sort(function(a,b) {
return d3.descending(a.avg_base, b.avg_base);
});
yScale.domain(data.map(function(d) {
return d.COL_DIV_CODE;
}));
// Move bars
svg.selectAll(".bar")
.transition()
.duration(500)
.attr("y", function(d, i) {
console.log("bar: ", d.avg_base, " ", yScale(d.avg_base));
return yScale(d.COL_DIV_CODE);
});
// Move bar labels
/*
svg.selectAll(".bar-label")
.transition()
.duration(500)
.attr("y", function(d) {
var obj = findObjectByCollegeName(d, data);
return yScale(obj.COL_DIV_CODE) + yScale.bandwidth() / 2 - 8;
});
*/
chart.select('g.y-axis')
.transition()
.duration(500)
.call(axisLeft);
});
</script>
</body>
I'm learning D3 JS, and I'm trying to replicate the D3 bar chart tutorial here. It works as is using d3.v3, but the moment I change the src to d3.d5 by changing:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
//Irrelevant CSS...
</style>
<svg class="chart"></svg>
<script src="https://d3js.org/d3.v5.min.js"></script> //Changed to d3.v5
<script>
var width = 420,
barHeight = 20;
var x = d3.scale.linear()
.range([0, width]);
var chart = d3.select(".chart")
.attr("width", width);
d3.tsv("data.tsv", type, function(error, data) {
x.domain([0, d3.max(data, function(d) { return d.value; })]);
chart.attr("height", barHeight * data.length);
var bar = chart.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
bar.append("rect")
.attr("width", function(d) { return x(d.value); })
.attr("height", barHeight - 1);
bar.append("text")
.attr("x", function(d) { return x(d.value) - 3; })
.attr("y", barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d.value; });
});
function type(d) {
d.value = +d.value; // coerce to number
return d;
}
</script>
I get an error:
Uncaught (in promise) TypeError: chart.selectAll(...).data(...).enter is not a function
I'd rather learn the latest and greatest, which I assume is D3.v5, instead of D3.v3, unless the latter really is the latest stable build. Was there a syntax change in this method chain? because I thought .selectAll(...).data(...).enter would be a pretty "standard" method chain. This has been surprisingly resistant to googling, unfortunately; similar problems are much more complex use-cases with many more degrees of freedom, whereas my question simply involves changing the D3 version.
I see 2 problems with your solution:
Change d3.scale.linear() to d3.scaleLinear()
Change d3.tsv("data.tsv", type, function(error, data) { to d3.tsv("data.tsv").then(function(data) {
Explanation for 2: d3.tsv function returns a promise which needs to be resolved before you can use the data
How to get lat, lon, date?
I have used nested but it's not working. lat and lon are showing data in HTML console but unable to use lat and lon to display location as circles in the map.
the issue is in the var nested part. I have tried multiple ways to solve but unable to.
here is the data:
[{
"session":{"id":"1"},
"location":{"lat":36.958992,"lon":-122.045301},
"number":{"created":{"date":"2018-05-19 00:00:51.000000"}},
"session":{"id":"2"},
"location":{"lat":40.958992,"lon":-100.045301},
"number":{"created":{"date":"2018-05-29 00:00:11.000000"}}
}]
here is the code I have used:
it is html file with everything;
<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
stroke: white;
stroke-width: 0.25px;
fill: grey;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v0.min.js"></script>
<script>
var width = 960,
height = 500;
var projection = d3.geo.mercator()
.center([0, 5 ])
.scale(200)
.rotate([-180,0]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var path = d3.geo.path()
.projection(projection);
var g = svg.append("g");
function process(d){
var processed = {
lat: parseFloat(d.location.lat),
lon: parseFloat(d.location.lon),
}
return processed;
}
// load and display the World
d3.json("world-110m2.json", function(error, topology) {
// load and display the cities
d3.json("data.json", function(error, data) {
console.log("data", data);
var nest_data = data.map(process);
console.log("nest_data", nest_data);
var nested = d3.nest()
.key(function(d) { return d.location; })
.rollup(function(leaves) { return leaves.length; })
.entries(data);
console.log("nested", nested);
g.selectAll("circle")
.data(data)
.enter()
.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)
});
// zoom and pan
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll("circle")
.attr("d", path.projection(projection));
g.selectAll("path")
.attr("d", path.projection(projection));
});
svg.call(zoom)
</script>
</body>
</html>
I am a beginner with D3 and JS in general.
I am trying to do a simple rectangle visualisation with a small csv file as a source.
price, units
80.67, 100
80.87, 99
79.34, 47
File, csv are in the same folder.
I am using Python's SimpleHTTPServer to serve locally in this folder.
This is my code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Test Data</title>
<script type="text/javascript" src="../d3/d3.v3.js"></script>
</head>
<body>
<script type="text/javascript">
// load csv from the same directory
d3.csv("test.csv", function (data){
return {
price: +data.price, // convert to number with +
units: +data.units, // convert to number with +
};
var canvas = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500)
canvas.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("width", function (d) { return d.price; })
.attr("height", 48)
.attr("y", function (d) { return d.units; })
.attr("fill", "blue");
canvas.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("fill", "white")
.attr("y", function (d) { return d.units + 24; })
.text( function (d) { return d.units;})
});
</script>
</body>
I am getting no errors, just a blank page.
What is wrong with this code?
The first thing you do in your callback is to return. None of the code after that is being executed. I'm referring to
return {
price: +data.price, // convert to number with +
units: +data.units, // convert to number with +
};
which should probably be
data.forEach(function(d) {
d.price = +d.price;
d.units = +d.units;
});
The signature of the callback should also be function(error, data) instead of function(data).
I have simple code here from Interactive Data Visualization by Scott Murray with minor changes. What I changed is the initial data's length 5 is different from the dataset1's length 25 in the click function.
However, every time I click and update, it does generate random new numbers but the length only shows 5 bars.
What is the reason for this? And how could I modify to change it?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script type="text/javascript" src="../d3/d3.v3.js"></script>
<title>D3 index01 </title>
<style type="text/css">
div.bar{
display: inline-block;
width: 20px;
height: 75px;
background-color: teal;
margin-right: 2px;
}
</style>
</head>
<body>
<p> click it </p>
<script type = "text/javascript">
var w=600;
var h = 250;
var dataset = [5,10,13,14,15];
var xScale = d3.scale.ordinal()
.domain(d3.range(dataset.length)).rangeRoundBands([0,w],0.05);
var yScale = d3.scale.linear()
.domain([0,d3.max(dataset)]).range([0,h]);
var svg = d3.select("body")
.append("svg")
.attr("width",w)
.attr("height",h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d,i){
return xScale(i);
})
.attr("y",function(d){return (h-yScale(d));})
.attr("width",xScale.rangeBand())
.attr("height",function(d){return yScale(d);});
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(i)+xScale.rangeBand() / 2;
})
.attr("y", function(d) {
return h - yScale(d) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
d3.select("p")
.on("click",function(){
var dataset1 = [];
for(var i=0; i<25; i++){
var newNumber = Math.floor(Math.random()*25);
dataset1.push(newNumber);
}
svg.selectAll("rect").data(dataset1)
.transition()
.delay(function(d,i){
return i*100;
})
.duration(500)
.attr("x",function(d,i){
return xScale(i);
})
.attr("y",function(d){
return h-yScale(d);
})
.attr("x", function(d,i){
return xScale(i);
})
.attr("height",function(d){
return yScale(d);
})
.attr("fill",function(d){
return "rgb(0,0, " + (d*10) + ")";
});
svg.selectAll("text").data(dataset1).
text(function(d) {
return d;
})
.attr("x", function(d, i) {
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
return h - yScale(d) + 14;
});
});
</script>
</body>
</html>
The issue, as I see it, is that when you update the rects with the new numbers using this statement
svg.selectAll("rect").data(dataset1)
the selectAll only selects the original 5 rects that were created based on dataset
var dataset = [5,10,13,14,15];
You could either create them all from the start with an equally sized starting dataset or append new rect svgs at this point.
Since you are doing a transition on the original rects I think it makes the most sense to just start with all 25. You could write a function to automatically generate this and put it where dataset is defined:
var dataset = [];
var numBars = 25; // This is the number of bars you want
var maxHeight = 25; // Seems like this is independent and needed as a ceiling
for(var i =0;i < N;i++){
var newNumber = Math.floor(Math.random()* maxHeight);
dataset.push(newNumber);
}
You could then replace 25 with numBars within your onclick function as well.