forked from d3noob‘s block: Leanpub Book Download Scatterplot
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body { font: 12px sans-serif; }
.axis path,
.axis line {
fill: none;
stroke: grey;
stroke-width: 1;
shape-rendering: crispEdges;
}
.dot { stroke: none; }
.grid .tick { stroke: lightgrey; opacity: 0.7; }
.grid path { stroke-width: 0;}
path {
stroke: #1f78b4;
stroke-width: 1.5;
fill: none;
}
text.shadow {
stroke: white;
stroke-width: 2.5px;
opacity: 0.9;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
// functions to parse the date / time formats
parseDate = d3.time.format("%Y-%m-%d").parse;
parseTime = d3.time.format("%H:%M:%S").parse;
formatDate = d3.time.format("%d-%b"),
formatTime = d3.time.format("%H:%M"),
bisectDate = d3.bisector(function(d) { return d.date; }).left;
// Load the raw data
d3.json("downloads.json", function(error, events) {
// parse and format all the event data
events.forEach(function(d) {
d.dtg = d.dtg.slice(0,-4)+'0:00'; // get the 10 minute block
dtgSplit = d.dtg.split(" "); // split on the space
d.date = dtgSplit[0]; // get the date seperatly
d.time = dtgSplit[1]; // format the time
d.number_downloaded = 1; // Number of downloads
});
// get the scatterplot data and nest the data by date/time
var data = d3.nest()
.key(function(d) { return d.dtg;})
.rollup(function(d) {
return d3.sum(d,function(g) {return g.number_downloaded; });
})
.entries(events);
// format the date/time data
data.forEach(function(d) {
d.dtg = d.key; // get the 10 minute block
dtgSplit = d.dtg.split(" "); // split on the space
d.date = parseDate(dtgSplit[0]); // get the date seperatly
d.time = parseTime(dtgSplit[1]); // format the time
d.number_downloaded = d.values; // Number of downloads
});
// nest the data by date for the daily graph
var dataDate = d3.nest()
.key(function(d) { return d.date;})
.rollup(function(d) {
return d3.sum(d,function(g) {return g.number_downloaded; });
})
.entries(events);
// format the date data
dataDate.forEach(function(d) {
d.date = parseDate(d.key); // format the date
d.close = d.values; // Number of downloads
});
// nest the data by 10 minute intervals for the time graph
var dataTime = d3.nest()
.key(function(d) { return d.time;})
.sortKeys(d3.ascending)
.rollup(function(d) {
return d3.sum(d,function(g) {return g.number_downloaded; });
})
.entries(events);
// format the time data
dataTime.forEach(function(d) {
d.time = d.key; // get the 10 minute block
d.time = parseTime(d.time); // get the date seperatly
d.close = d.values; // Number of downloads
});
// Get number of days in date range to calculate scatterplotWidth
var oneDay = 24*60*60*1000; // hours*minutes*seconds*milliseconds
var dateStart = d3.min(data, function(d) { return d.date; });
var dateFinish = d3.max(data, function(d) { return d.date; });
var numberDays = Math.round(Math.abs((dateStart.getTime() -
dateFinish.getTime())/(oneDay)));
var margin = {top: 20, right: 20, bottom: 20, left: 50},
scatterplotHeight = 520,
scatterplotWidth = numberDays * 1.5,
dateGraphHeight = 220,
timeGraphWidth = 220;
// Set the dimensions of the canvas / graph
var height = scatterplotHeight + dateGraphHeight,
width = scatterplotWidth + timeGraphWidth;
// ************* draw the scatterplot ****************
var formatDay_Time = d3.time.format("%H:%M"); // tooltip time
var formatWeek_Year = d3.time.format("%d-%m-%Y"); // tooltip date
var x = d3.time.scale().range([0, scatterplotWidth]);
var y = d3.time.scale().range([0, scatterplotHeight]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(7);
var yAxis = d3.svg.axis()
.scale(y)
.orient("right")
.ticks(12,0,0)
.tickFormat(d3.time.format("%H:%M"));
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 + ")");
// State the functions for the grid
function make_x_axis() {
return d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(12)
}
// State the functions for the grid
function make_y_axis() {
return d3.svg.axis()
.scale(y)
.orient("right")
.ticks(8)
}
// Set the domains
y.domain([new Date(1899, 12, 02, 0, 0, 0),
new Date(1899, 12, 01, 0, 0, 1)]);
x.domain(d3.extent(data, function(d) { return d.date; }));
// tickSize: Get or set the size of major, minor and end ticks
svg.append("g").classed("grid x_grid", true)
.attr("transform", "translate(0,"
+ (scatterplotHeight + dateGraphHeight) + ")")
.style("stroke-dasharray", ("3, 3, 3"))
.call(make_x_axis()
.tickSize(-scatterplotHeight - dateGraphHeight, 0, 0)
.tickFormat(""))
// tickSize: Get or set the size of major, minor and end ticks
svg.append("g").classed("grid y_grid", true)
.attr("transform", "translate(0,0)")
.style("stroke-dasharray", ("3, 3, 3"))
.call(make_y_axis()
.tickSize(+scatterplotWidth+timeGraphWidth, 0, 0)
.tickFormat(""))
// Draw the Axes and the tick labels
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + scatterplotHeight + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "middle");
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + scatterplotWidth + ",0)")
.call(yAxis)
.selectAll("text");
// draw the plotted circles
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", function(d) { return d.number_downloaded*1.5; })
.style("opacity", 0.3)
.style("fill", "#e31a1c" )
.attr("cx", function(d) { return x(d.date); })
.attr("cy", function(d) { return y(d.time); })
;
// ************* draw the date graph *****************
// Set the ranges
var xDate = d3.time.scale().range([0, scatterplotWidth]);
var yDate = d3.scale.linear().range([dateGraphHeight, 0]);
var yAxisDate = d3.svg.axis().scale(yDate)
.orient("right").ticks(5);
// Define the line
var valueline = d3.svg.line()
.interpolate("basis")
.x(function(d) { return xDate(d.date); })
.y(function(d) { return yDate(d.close); } );
// Scale the range of the data
xDate.domain(d3.extent(dataDate, function(d) { return d.date; }));
yDate.domain([0,d3.max(dataDate, function(d) { return d.close; })]);
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("transform", "translate(0," + scatterplotHeight + ")")
.attr("d", valueline(dataDate));
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate("
+ scatterplotWidth + "," + scatterplotHeight + ")")
.call(yAxisDate);
// ************* draw the time graph **************
// Set the ranges
var yTime = d3.time.scale().range([0, scatterplotHeight]);
var xTime = d3.scale.linear().range([0, timeGraphWidth]);
// Define the axes
var xAxisTime = d3.svg.axis().scale(xTime)
.orient("bottom").ticks(5);
// Define the line
var valuelineTime = d3.svg.line()
.x(function(d) { return xTime(d.close); })
.y(function(d) { return yTime(d.time); });
// Scale the range of the data
yTime.domain([new Date(1899, 12, 02, 0, 0, 0),
new Date(1899, 12, 01, 0, 0, 1)]);
xTime.domain([1e-6, d3.max(dataTime,
function(d) { return d.close; }
)]);
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("transform", "translate(" + scatterplotWidth + ",0)")
.attr("d", valuelineTime(dataTime));
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate("
+ scatterplotWidth + "," + scatterplotHeight + ")")
.call(xAxisTime);
// *********** place the mouse movement information **************
var focus = svg.append("g")
.style("display", "none");
// append the x line
focus.append("line")
.attr("class", "x")
.style("stroke", "#33a02c")
.style("stroke-dasharray", "3,3")
.style("opacity", 1)
.style("shape-rendering", "crispEdges");
// append the y line
focus.append("line")
.attr("class", "y")
.style("stroke", "#33a02c")
.style("stroke-dasharray", "3,3")
.style("opacity", 1)
.style("shape-rendering", "crispEdges");
// place the value at the intersection
focus.append("text")
.attr("class", "y1")
.style("stroke", "white")
.style("stroke-width", "3.5px")
.style("opacity", 0.8)
.attr("dx", 8)
.attr("dy", "-.3em");
focus.append("text")
.attr("class", "y2")
.attr("dx", 8)
.attr("dy", "-.3em");
// place the date at the intersection
focus.append("text")
.attr("class", "y3")
.style("stroke", "white")
.style("stroke-width", "3.5px")
.style("opacity", 0.8)
.attr("dx", 8)
.attr("dy", "1em");
focus.append("text")
.attr("class", "y4")
.attr("dx", 8)
.attr("dy", "1em");
// place the Daily Downloads dynamic text
focus.append("text")
.attr("class", "y5")
.style("stroke", "white")
.style("stroke-width", "3.5px")
.style("opacity", 0.8)
.attr("dy", "1em");
focus.append("text")
.attr("class", "y6")
.attr("dy", "1em");
// place the time Downloads dynamic text
focus.append("text")
.attr("class", "y7")
.style("stroke", "white")
.style("stroke-width", "3.5px")
.style("opacity", 0.8)
.attr("dy", ".35em");
focus.append("text")
.attr("class", "y8")
.attr("dy", ".35em");
// append the rectangle to capture mouse
svg.append("rect")
.attr("width", scatterplotWidth)
.attr("height", scatterplotHeight)
.style("fill", "none")
.style("pointer-events", "all")
.on("mouseover", function() { focus.style("display", null); })
.on("mouseout", function() { focus.style("display", "none"); })
.on("mousemove", mousemove);
// The conversion ratio to change x position of cursor to
// the index of the date array
var convertDate = dataDate.length/scatterplotWidth;
var convertTime = dataTime.length/scatterplotHeight;
// interactive mouse function
function mousemove() {
var xpos = d3.mouse(this)[0],
x0 = x.invert(xpos),
y0 = d3.mouse(this)[1],
y1 = y.invert(y0),
date1 = d3.mouse(this)[0];
// Place the intersection date text
focus.select("text.y1")
.attr("transform",
"translate(" + (date1 - 50) + "," + (y0+20) + ")")
.text(formatDate(x0));
focus.select("text.y2")
.attr("transform",
"translate(" + (date1 - 50) + "," + (y0+20) + ")")
.text(formatDate(x0));
// Place the intersection time text
focus.select("text.y3")
.attr("transform",
"translate(" + (date1) + "," + (y0-15) + ")")
.text(formatTime(y1).substring(0,4)+'0');
focus.select("text.y4")
.attr("transform",
"translate(" + (date1) + "," + (y0-15) + ")")
.text(formatTime(y1).substring(0,4)+'0');
// Place the dynamic daily downloads text
focus.select("text.y5")
.attr("transform",
"translate("
+ (date1) + ","
+ (scatterplotHeight+dateGraphHeight) + ")")
.attr("text-anchor", "middle")
.text(dataDate[parseInt(xpos*convertDate)].close);
focus.select("text.y6")
.attr("transform",
"translate("
+ (date1) + ","
+ (scatterplotHeight+dateGraphHeight) + ")")
.attr("text-anchor", "middle")
.text(dataDate[parseInt(xpos*convertDate)].close);
// Place the dynamic time downloads text
focus.select("text.y7")
.attr("transform",
"translate("
+ (scatterplotWidth+timeGraphWidth) + ","
+ (y0) + ")")
.attr("text-anchor", "start")
.text(dataTime[144-parseInt(y0*convertTime)].close);
focus.select("text.y8")
.attr("transform",
"translate("
+ (scatterplotWidth+timeGraphWidth) + ","
+ (y0) + ")")
.attr("text-anchor", "start")
.text(dataTime[144-parseInt(y0*convertTime)].close);
focus.select(".x")
.attr("transform",
"translate(" + date1 + "," + (0) + ")")
.attr("y2", height );
focus.select(".y")
.attr("transform",
"translate(0," + y0 + ")")
.attr("x2", width );
}
// axis labels
svg.append("text")
.attr("transform", "translate("
+ scatterplotWidth + ","
+ scatterplotHeight + "), rotate(-90)")
.attr("y", 25)
.attr("x",-dateGraphHeight )
.attr("dy", "1em")
.style("text-anchor", "start")
.attr("class", "shadow")
.text("Daily Downloads");
svg.append("text")
.attr("transform", "translate("
+ scatterplotWidth + ","
+ scatterplotHeight + "), rotate(-90)")
.attr("y", 25)
.attr("x",-dateGraphHeight )
.attr("dy", "1em")
.style("text-anchor", "start")
.text("Daily Downloads");
svg.append("text")
.attr("transform", "translate("
+ scatterplotWidth + ","
+ scatterplotHeight + ")")
.attr("y", 20)
.attr("x",timeGraphWidth )
.attr("dy", "1em")
.style("text-anchor", "end")
.attr("class", "shadow")
.text("Downloads in 10 Minute Blocks");
svg.append("text")
.attr("transform", "translate("
+ scatterplotWidth + ","
+ scatterplotHeight + ")")
.attr("y", 20)
.attr("x",timeGraphWidth )
.attr("dy", "1em")
.style("text-anchor", "end")
.text("Downloads in 10 Minute Blocks");
});
</script>
</body>
The graph above shows each instance that the book [D3 Tips and Tricks](https://leanpub.com/D3-Tips-and-Tricks) was download from January 2013 to March 2015. It is best viewed in full screen by [opening in a new window](http://bl.ocks.org/d3noob/raw/a0cbcddc6bf0eb9569fe/).
The main scatterplot has the day that the download occured on the X axis and the time (UTC) that the download occured on the Y axis. The time of the download is grouped into 10 minute blocks and where more than one download occured, the radius of the corresponding point is increased.
The line graph at the bottom shows the variation of numbers of downloads by day and the line graph on the right shows the variation of downloads by time of day (in 10 minute blocks).
Moving the mouse over the scatterplot will show the time and date of the point and the data is extrapolated onto the line graphs to show the corresponding number of downloads for the day and time.
There is an explanation of the code available in [D3 Tips and Tricks](https://leanpub.com/D3-Tips-and-Tricks) or in the blog post [here](http://www.d3noob.org/2015/04/exploring-event-data-by-combination.html).