I am trying to create a scatter plot based on two timestamps in d3, but i'm not sure the proper way to use the d3-time-format methods to properly parse the values in the timestamp format and build a range based on a 24-hour period. So far I built a function to loop through my array and use the d3 methods to convert the string to a d3-readable format, but I can seem to figure out how to format the output so it converts from date format to time format. My questions are does d3 accept time format? And how can I go about converting the date object to a time object? At the moment I have a y-axis that presents ticks for years.
Provided is my full code:
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<style>
div.tooltip {
position: absolute;
text-align: center;
width: 100px;
height: 30px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
</style>
<script>
var data = [
{
"x": "23:19:30",
"y": "08:07:00"
},
{
"x": "22:55:30",
"y": "06:08:00"
},
{
"x": "21:14:30",
"y": "06:13:30"
},
{
"x": "21:24:30",
"y": "06:04:30"
},
{
"x": "21:24:00",
"y": "06:04:00"
},
{
"x": "21:28:00",
"y": "06:09:00"
},
{
"x": "23:23:30",
"y": "08:20:30"
},
{
"x": "23:47:00",
"y": "08:01:30"
},
{
"x": "00:03:00",
"y": "06:49:30"
},
{
"x": "21:46:00",
"y": "06:21:00"
},
{
"x": "21:58:00",
"y": "06:02:30"
},
{
"x": "21:33:00",
"y": "05:56:00"
},
{
"x": "22:33:00",
"y": "06:15:30"
},
{
"x": "23:49:00",
"y": "07:10:30"
},
{
"x": "23:46:30",
"y": "08:35:30"
},
{
"x": "23:15:30",
"y": "05:59:30"
},
{
"x": "21:26:00",
"y": "06:05:00"
},
{
"x": "21:26:30",
"y": "05:54:00"
},
{
"x": "21:06:00",
"y": "05:53:00"
},
{
"x": "21:25:00",
"y": "05:47:30"
},
{
"x": "00:29:30",
"y": "08:59:30"
},
{
"x": "01:14:00",
"y": "08:09:30"
},
{
"x": "23:12:30",
"y": "06:06:30"
},
{
"x": "21:26:00",
"y": "05:52:30"
},
{
"x": "21:18:30",
"y": "05:47:00"
},
{
"x": "20:54:30",
"y": "07:07:30"
},
{
"x": "21:36:00",
"y": "05:53:30"
},
{
"x": "00:28:00",
"y": "08:00:00"
},
{
"x": "23:21:30",
"y": "07:58:30"
},
{
"x": "21:34:00",
"y": "05:51:00"
},
{
"x": "21:23:30",
"y": "05:58:00"
},
{
"x": "21:05:30",
"y": "05:53:00"
},
{
"x": "21:33:30",
"y": "05:39:30"
},
{
"x": "23:49:30",
"y": "06:50:00"
},
{
"x": "01:11:00",
"y": "08:37:30"
},
{
"x": "22:34:30",
"y": "05:15:00"
},
{
"x": "22:49:30",
"y": "05:55:00"
},
{
"x": "22:06:30",
"y": "06:03:00"
},
{
"x": "21:32:30",
"y": "06:01:00"
},
{
"x": "21:49:00",
"y": "05:39:30"
},
{
"x": "22:47:30",
"y": "08:27:30"
},
{
"x": "21:26:30",
"y": "05:51:00"
},
{
"x": "21:47:30",
"y": "05:51:00"
},
{
"x": "21:28:00",
"y": "05:47:30"
},
{
"x": "21:32:00",
"y": "05:47:30"
},
{
"x": "21:13:30",
"y": "05:46:00"
},
{
"x": "23:42:30",
"y": "06:45:00"
},
{
"x": "21:33:00",
"y": "05:48:00"
},
{
"x": "21:45:00",
"y": "05:51:00"
},
{
"x": "21:29:30",
"y": "06:06:00"
},
{
"x": "21:16:00",
"y": "05:43:00"
},
{
"x": "21:14:00",
"y": "05:46:30"
},
{
"x": "00:01:30",
"y": "07:25:30"
},
{
"x": "02:24:00",
"y": "10:35:30"
},
{
"x": "22:29:30",
"y": "07:04:00"
},
{
"x": "21:43:30",
"y": "05:51:00"
},
{
"x": "21:31:30",
"y": "05:45:00"
},
{
"x": "22:16:30",
"y": "05:50:30"
},
{
"x": "21:59:00",
"y": "05:47:00"
},
{
"x": "02:55:30",
"y": "11:15:30"
},
{
"x": "02:57:00",
"y": "07:23:00"
},
{
"x": "21:49:30",
"y": "06:48:30"
},
{
"x": "21:31:30",
"y": "05:26:30"
}
]
// D3 date parser
for (var i=0; i < data.length; i++){
var parser = d3.timeParse("%I:%M:%S")
data[i].x = parser(data[i].x);
data[i].y = parser(data[i].y);
}
console.log(data)
var margin = { top: 10, right: 30, bottom: 30, left: 60 }
var width = 800 - margin.left - margin.right;
var height = 800 - margin.top - margin.bottom;
// Define the div for the tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); // translate(margin left, margin top)
var x = d3.scaleTime()
.domain([d3.min(data, function(d) { return d.date }), d3.max(data, function(d) { return d.date })])
.range([0, width]);
svg.append("g")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(d3.axisBottom(x))
// .tickFormat(d3.time.format);
// text label for the x axis
svg.append("text")
.attr("transform",
"translate(" + (width/2) + " ," + (height + margin.top + 20) + ")")
.style("text-anchor", "middle")
.text("Date");
var y = d3.scaleTime()
.domain([0, d3.max(data, function(d){ return +d.y })])
.range([height, 0]);
svg.append("g")
.call(d3.axisLeft(y));
// text label for the y axis
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x",0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Time Asleep (Minutes)");
// Add line path
svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(function(d) { return x(d.date) })
.y(function(d) { return y(d.y) })
);
// Add the scatterplot (data points)
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("r", 3)
.attr("cx", function(d){ return x(d.date) })
.attr("cy", function(d){ return y(d.y) })
// Add tooltip on hover
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.x + "<br/>" + d.label)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 30) + "px")
})
// Remove tooltip after hover
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});
</script>
To format the date, use d3.timeParse. This will parse any date string to a Date object. D3 can understand and work with time objects with timeScale.
I've renamed your original data to rawData. We also need to sort the data by the X axis:
var toDate = d3.timeParse("%H:%M:%S")
var data = rawData.map(d => ({
x: toDate(d.x),
y: toDate(d.y),
})).sort((a, b) => d3.descending(a.x, b.x)
To calculate the domain, use d3.extent which will compute the domain automatically.
var x = d3.scaleTime()
.domain(d3.extent(data, d => d.x))
.range([0, width]);
<head></head>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<style>
div.tooltip {
position: absolute;
text-align: center;
width: 100px;
height: 30px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
</style>
<script>
var rawData = [
{
"x": "23:19:30",
"y": "08:07:00"
},
{
"x": "22:55:30",
"y": "06:08:00"
},
{
"x": "21:14:30",
"y": "06:13:30"
},
{
"x": "21:24:30",
"y": "06:04:30"
},
{
"x": "21:24:00",
"y": "06:04:00"
},
{
"x": "21:28:00",
"y": "06:09:00"
},
{
"x": "23:23:30",
"y": "08:20:30"
},
{
"x": "23:47:00",
"y": "08:01:30"
},
{
"x": "00:03:00",
"y": "06:49:30"
},
{
"x": "21:46:00",
"y": "06:21:00"
},
{
"x": "21:58:00",
"y": "06:02:30"
},
{
"x": "21:33:00",
"y": "05:56:00"
},
{
"x": "22:33:00",
"y": "06:15:30"
},
{
"x": "23:49:00",
"y": "07:10:30"
},
{
"x": "23:46:30",
"y": "08:35:30"
},
{
"x": "23:15:30",
"y": "05:59:30"
},
{
"x": "21:26:00",
"y": "06:05:00"
},
{
"x": "21:26:30",
"y": "05:54:00"
},
{
"x": "21:06:00",
"y": "05:53:00"
},
{
"x": "21:25:00",
"y": "05:47:30"
},
{
"x": "00:29:30",
"y": "08:59:30"
},
{
"x": "01:14:00",
"y": "08:09:30"
},
{
"x": "23:12:30",
"y": "06:06:30"
},
{
"x": "21:26:00",
"y": "05:52:30"
},
{
"x": "21:18:30",
"y": "05:47:00"
},
{
"x": "20:54:30",
"y": "07:07:30"
},
{
"x": "21:36:00",
"y": "05:53:30"
},
{
"x": "00:28:00",
"y": "08:00:00"
},
{
"x": "23:21:30",
"y": "07:58:30"
},
{
"x": "21:34:00",
"y": "05:51:00"
},
{
"x": "21:23:30",
"y": "05:58:00"
},
{
"x": "21:05:30",
"y": "05:53:00"
},
{
"x": "21:33:30",
"y": "05:39:30"
},
{
"x": "23:49:30",
"y": "06:50:00"
},
{
"x": "01:11:00",
"y": "08:37:30"
},
{
"x": "22:34:30",
"y": "05:15:00"
},
{
"x": "22:49:30",
"y": "05:55:00"
},
{
"x": "22:06:30",
"y": "06:03:00"
},
{
"x": "21:32:30",
"y": "06:01:00"
},
{
"x": "21:49:00",
"y": "05:39:30"
},
{
"x": "22:47:30",
"y": "08:27:30"
},
{
"x": "21:26:30",
"y": "05:51:00"
},
{
"x": "21:47:30",
"y": "05:51:00"
},
{
"x": "21:28:00",
"y": "05:47:30"
},
{
"x": "21:32:00",
"y": "05:47:30"
},
{
"x": "21:13:30",
"y": "05:46:00"
},
{
"x": "23:42:30",
"y": "06:45:00"
},
{
"x": "21:33:00",
"y": "05:48:00"
},
{
"x": "21:45:00",
"y": "05:51:00"
},
{
"x": "21:29:30",
"y": "06:06:00"
},
{
"x": "21:16:00",
"y": "05:43:00"
},
{
"x": "21:14:00",
"y": "05:46:30"
},
{
"x": "00:01:30",
"y": "07:25:30"
},
{
"x": "02:24:00",
"y": "10:35:30"
},
{
"x": "22:29:30",
"y": "07:04:00"
},
{
"x": "21:43:30",
"y": "05:51:00"
},
{
"x": "21:31:30",
"y": "05:45:00"
},
{
"x": "22:16:30",
"y": "05:50:30"
},
{
"x": "21:59:00",
"y": "05:47:00"
},
{
"x": "02:55:30",
"y": "11:15:30"
},
{
"x": "02:57:00",
"y": "07:23:00"
},
{
"x": "21:49:30",
"y": "06:48:30"
},
{
"x": "21:31:30",
"y": "05:26:30"
}
]
// D3 date parser
var toDate = d3.timeParse("%H:%M:%S")
var data = rawData.map(d => ({
x: toDate(d.x),
y: toDate(d.y),
})).sort((a, b) => d3.descending(a.x, b.x))
console.log(data[0])
var margin = { top: 10, right: 30, bottom: 30, left: 60 }
var width = 800 - margin.left - margin.right;
var height = 800 - margin.top - margin.bottom;
// Define the div for the tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); // translate(margin left, margin top)
var x = d3.scaleTime()
.domain(d3.extent(data, d => d.x))
.range([0, width]);
svg.append("g")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(d3.axisBottom(x))
// .tickFormat(d3.time.format);
// text label for the x axis
svg.append("text")
.attr("transform",
"translate(" + (width/2) + " ," + (height + margin.top + 20) + ")")
.style("text-anchor", "middle")
.text("Date");
var y = d3.scaleTime()
.domain(d3.extent(data, d => d.y))
.range([height, 0]);
svg.append("g")
.call(d3.axisLeft(y));
// text label for the y axis
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x",0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Time Asleep (Minutes)");
// Add line path
svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(function(d) { return x(d.x) })
.y(function(d) { return y(d.y) })
);
// Add the scatterplot (data points)
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("r", 3)
.attr("cx", function(d){ return x(d.x) })
.attr("cy", function(d){ return y(d.y) })
// Add tooltip on hover
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.x + "<br/>" + d.label)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 30) + "px")
})
// Remove tooltip after hover
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});
</script>
I've recently begun learning D3.js and I am struggling to create a transition in a scatter plot with the following data:
var data = [
{"year" : "2004", "x":100, "y":300, "size": 2, "type": "A"},
{"year" : "2005", "x":200, "y":200, "size": 2, "type": "A"},
{"year" : "2006", "x":300, "y":100, "size": 2, "type": "A"},
{"year" : "2004", "x":150, "y":250, "size": 2.382450, "type": "B"},
{"year" : "2005", "x":150, "y":250, "size": 3.078548, "type": "B"},
{"year" : "2006", "x":150, "y":250, "size": 4.265410, "type": "B"}];
Where in the scatter plot there are 2 points (type A&B) and they change location (x&y) and size by year. I've created a fiddle where I try to nest the data and plot the points, but making the next step of using transition() function is confusing. More specifically, I am still declaring the whole data, but to make transitions work I only need part of the data.
The key to understand what you want lies here:
There are 2 points and they change location (x&y) and size by year
Therefore, this is clearly a XY problem. Your problem is not "how to transition with nested data". Your problem is "how to transition by year".
My proposed solution involves, first of all, dropping that nested array. You don't need that.
Instead, get all the years in the data...
var years = [...new Set(data.map(function(d) {
return d.year
}))];
..., filter the data by year...
var dataStart = data.filter(function(d) {
return d.year === years[0]
});
... and loop trough the years. Here, I'm using d3.interval():
var counter = 1;
var timer = d3.interval(transition, 1500);
function transition() {
var newData = data.filter(function(d) {
return d.year === years[counter]
});
svg.selectAll("circle").data(newData)
.transition()
.duration(1000)
.attr("cx", function(d) {
console.log(d)
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.size * 10;
});
counter += 1;
if (counter === 3) {
timer.stop()
}
}
Here is the demo:
var data = [{
"year": "2004",
"x": 100,
"y": 100,
"size": 2,
"type": "A"
}, {
"year": "2005",
"x": 200,
"y": 180,
"size": 2,
"type": "A"
}, {
"year": "2006",
"x": 300,
"y": 50,
"size": 2,
"type": "A"
}, {
"year": "2004",
"x": 150,
"y": 150,
"size": 2.382450,
"type": "B"
}, {
"year": "2005",
"x": 150,
"y": 50,
"size": 3.078548,
"type": "B"
}, {
"year": "2006",
"x": 150,
"y": 100,
"size": 4.265410,
"type": "B"
}];
var width = 400,
height = 200;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var years = [...new Set(data.map(function(d) {
return d.year
}))];
var dataStart = data.filter(function(d) {
return d.year === years[0]
});
var cell = svg.selectAll("circle")
.data(dataStart);
cell.enter()
.append("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.size * 10;
});
var counter = 1;
var timer = d3.interval(transition, 1500);
function transition() {
var newData = data.filter(function(d) {
return d.year === years[counter]
});
svg.selectAll("circle").data(newData)
.transition()
.duration(1000)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.size * 10;
});
counter += 1;
if (counter === 3) {
timer.stop()
}
}
<script src="https://d3js.org/d3.v4.min.js"></script>
I have data in a multidimensional array in the form of:
data = [
[
{
"x": 329, "y": 484.8333333333333
},
{
"x": 439, "y": 484.8333333333333
},
{
"x": 439, "y": 484.8333333333333
},
{
"x": 549, "y": 484.8333333333333
}
], [
{
"x": 559, "y": 484.8333333333333
},
{
"x": 669, "y": 484.8333333333333
},
{
"x": 669, "y": 484.8333333333333
},
{
"x": 779, "y": 484.8333333333333
}
], [
{
"x": 329, "y": 313.8333333333333
},
{
"x": 439, "y": 313.8333333333333
},
{
"x": 439, "y": 253.83333333333331
},
{
"x": 549, "y": 253.83333333333331
}
], [
{
"x": 559, "y": 313.8333333333333
},
{
"x": 669, "y": 313.8333333333333
},
{
"x": 669, "y": 253.83333333333331
},
{
"x": 779, "y": 253.83333333333331
}
], etc.
]
Each array is the coordinates for one step-before path connecting two svg elements. I've defined a function for generating a path:
stepFuction = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("step-before");
I am attempting to instantiate the paths like so:
step = svg.selectAll("path")
.data(_.each(stepData, (d) => { return d; }))
.enter()
.append("path")
.attr("class", "line")
.attr("d", stepFuction);
Rather than a step-before path, I'm getting a bowtie looking shape (see attachment). Obviously, I am doing something wrong, I'm assuming it has something to do with using _.each inside the data method.
1) What is the correct approach to creating one path per array in my data array?
I would like to be able to drag the elements around and have these paths update. I am using d3.on("tick") for nodes, labels and groups with something like:
node.attr("x", function (d) { return d.x - d.width / 2 + pad; })
.attr("y", function (d) { return d.y - d.height / 2 + pad; });
and it is working correctly but I'm not sure how to update paths as there are multiple values that need to be recalculated on every tick.
2) What is the correct approach to updating these step-before paths on each tick?
I created a fiddle if anything wasn't clear in my description:
d3 step-before fiddle
I think this is actually to do with stylings.
Fiddle
Added this css code:
path {
stroke-width: 1px;
fill: none;
stroke: black;
}
There must be something obvious I'm missing here.
I'm trying to draw a simple line and this is my javascript:
// CRASH DATA
var lineData = [
{ "x": 0, "y": 0.5},
{ "x": 2, "y": 0.1},
{ "x": 4, "y": -0.5},
{ "x": 6, "y": -0.8},
{ "x": 8, "y": -0.9},
{ "x": 10, "y": -0.10},
{ "x": 12, "y": -0.10},
{ "x": 14, "y": -0.11},
{ "x": 16, "y": -0.10},
{ "x": 18, "y": -0.9},
{ "x": 20, "y": -0.7},
{ "x": 22, "y": -0.6},
{ "x": 24, "y": -0.5},
{ "x": 26, "y": -0.3},
{ "x": 28, "y": -0.1},
{ "x": 30, "y": 0.2},
{ "x": 32, "y": 0.4},
{ "x": 34, "y": 0.8},
{ "x": 36, "y": 0.8},
{ "x": 38, "y": 0.7},
{ "x": 40, "y": 0.4},
{ "x": 42, "y": 0.4},
{ "x": 44, "y": 0.4},
{ "x": 46, "y": 0.2},
{ "x": 48, "y": 0.1},
{ "x": 50, "y": 0}
];
//DRAW TRAJECTORY
function draw(data){
var margin = {top: 30, right: 20, bottom: 30, left: 50},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
var x = d3.scale.linear().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);
//This is the accessor function we talked about above
var lineFunction = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("linear");
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.x; }));
y.domain(d3.extent(data, function(d) { return d.y; }));
svg.append("g") // Add the X Axis
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g") // Add the Y Axis
.attr("class", "y axis")
.call(yAxis);
svg.append("path") // Add the lineFunction path.
.attr("class", "line")
.attr("d", lineFunction(data));
};
//PUT EVERYTHING ON SCREEN
$( document ).ready(function() {
draw(lineData);
});
And here is the outcome:
You aren't actually using the scales you define. Your line function should be
var lineFunction = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); })
.interpolate("linear");
Complete example here.