|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>SpaceX launches by time</title> |
|
<style> |
|
body { |
|
font-family: Helvetica,Arial,sans-serif; |
|
} |
|
|
|
.orbit-label { |
|
font-size: 20px; |
|
} |
|
|
|
.value-text { |
|
font-size: 10px; |
|
} |
|
|
|
.bar { |
|
fill: #849199; /* color picker from SpaceX logo */ |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<svg width="960" height="500"></svg> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script> |
|
var svg = d3.select("svg"), |
|
margin = {top: 10, right: 20, bottom: 20, left: 10}, |
|
innerDistances = {orbitLabelWidth: 95, horizontal: 30, valueTextHeight: 10}, |
|
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 + ")"); |
|
|
|
d3.json("https://api.spacexdata.com/v2/launches", function(error, data) { |
|
if (error) { |
|
throw error; |
|
} |
|
//console.log(data); |
|
|
|
// Collect launches by orbit because we'll have graphs for each orbit. |
|
var orbitData = data |
|
.filter(function(launch) { |
|
return (launch["rocket"]["rocket_id"] != "falcon1"); |
|
}) |
|
.reduce(function(orbitDict, launch) { |
|
var orbit = launch["rocket"]["second_stage"]["payloads"][0]["orbit"]; |
|
var orbitLaunches = orbitDict[orbit]; |
|
if (!orbitLaunches) { |
|
orbitLaunches = orbitDict[orbit] = []; |
|
} |
|
orbitLaunches.push(launch); |
|
return orbitDict; |
|
}, {}); |
|
//console.log(orbitData); |
|
|
|
// Convert to array of dictionaries with all the required data. |
|
var summarizedData = Object.keys(orbitData) |
|
.map(function(orbit) { |
|
var launches = orbitData[orbit]; |
|
var launchesByWeekDay = new Array(7).fill(0); |
|
var launchesByHour = new Array(24).fill(0); |
|
launches.forEach(function(launch) { |
|
var launchDate = new Date(launch["launch_date_unix"] * 1000); |
|
launchesByWeekDay[launchDate.getUTCDay()] += 1; |
|
launchesByHour[launchDate.getUTCHours()] += 1; |
|
}); |
|
|
|
return { |
|
orbit: orbit, |
|
firstFlightNumber: launches[0]["flight_number"], // for sorting |
|
launches: launches, |
|
launchesByWeekDay: launchesByWeekDay, |
|
launchesByHour: launchesByHour, |
|
}; |
|
}); |
|
// Order orbits in order of the first flight to such orbit. |
|
summarizedData.sort(function(lhs, rhs) { |
|
return lhs.firstFlightNumber - rhs.firstFlightNumber; |
|
}) |
|
//console.log(summarizedData); |
|
|
|
var maxLaunchesCount = d3.max(summarizedData, function(orbitData) { |
|
return d3.max([ |
|
d3.max(orbitData.launchesByWeekDay), |
|
d3.max(orbitData.launchesByHour) |
|
]); |
|
}); |
|
|
|
var plotWidth = (width - innerDistances.horizontal - innerDistances.orbitLabelWidth) / 2; |
|
var xScaleWeekDays = d3.scaleBand() |
|
.domain([1, 2, 3, 4, 5, 6, 0]) // put Sunday in the end |
|
.rangeRound([0, plotWidth]) |
|
.paddingInner(0.1); |
|
var xScaleHours = xScaleWeekDays.copy() |
|
.domain(d3.range(24)); |
|
|
|
var yScaleOrbits = d3.scaleBand() |
|
.domain(summarizedData.map(function(d) { return d.orbit; })) |
|
.rangeRound([height, 0]) |
|
.paddingInner(0.1); |
|
var orbitPlotHeight = yScaleOrbits.bandwidth(), |
|
orbitPlotInnerHeight = orbitPlotHeight - innerDistances.valueTextHeight; |
|
var yScaleValues = d3.scaleLinear() |
|
.domain([0, maxLaunchesCount]) |
|
.rangeRound([orbitPlotInnerHeight, 0]); |
|
|
|
g.selectAll("g") |
|
.data(summarizedData) |
|
.enter().append("g") |
|
.attr("transform", function(orbitData) { |
|
var yOffset = height - orbitPlotHeight - yScaleOrbits(orbitData.orbit); |
|
return "translate(0," + yOffset + ")"; |
|
}) |
|
.each(buildOrbitPlots); |
|
|
|
function buildOrbitPlots(orbitData) { |
|
// Orbit name. |
|
var labelText = d3.select(this).append("g") |
|
.append("text") |
|
.attr("x", innerDistances.orbitLabelWidth / 2) |
|
.attr("y", orbitPlotHeight / 2) |
|
.attr("text-anchor", "middle") |
|
.attr("class", "orbit-label"); |
|
var words = orbitData.orbit.split(" ") |
|
.filter(function(w) { return w.length > 0; }); |
|
if (words.length > 1) { |
|
labelText.selectAll("tspan") |
|
.data(words) |
|
.enter().append("tspan") |
|
.attr("x", innerDistances.orbitLabelWidth / 2) |
|
.attr("dy", function(d, i) { return i > 0 ? "1.2em" : 0; }) |
|
.text(function(d) { return d; }); |
|
} else { |
|
labelText.text(orbitData.orbit); |
|
} |
|
|
|
var weekDayG = d3.select(this).append("g") |
|
.attr("transform", "translate(" + innerDistances.orbitLabelWidth + ",0)"); |
|
var weekDayBars = weekDayG.selectAll("g") |
|
.data(orbitData.launchesByWeekDay) |
|
.enter().append("g") |
|
.attr("transform", function(d, i) { |
|
var x = xScaleWeekDays(i), |
|
y = yScaleValues(d) + innerDistances.valueTextHeight; |
|
return "translate(" + x + "," + y + ")"; |
|
}); |
|
weekDayBars.append("rect") |
|
.attr("class", "bar") |
|
.attr("x", 0) |
|
.attr("y", 0) |
|
.attr("width", xScaleWeekDays.bandwidth()) |
|
.attr("height", function(d) { return orbitPlotInnerHeight - yScaleValues(d); }) |
|
weekDayBars.append("text") |
|
.attr("x", xScaleWeekDays.bandwidth() / 2) |
|
.attr("y", -2) |
|
.attr("text-anchor", "middle") |
|
.attr("class", "value-text") |
|
.text(function(d) { return d > 0 ? d : ""; }); |
|
|
|
var hoursG = d3.select(this).append("g") |
|
.attr("transform", "translate(" + (width-plotWidth) + ",0)"); |
|
var hoursBars = hoursG.selectAll("g") |
|
.data(orbitData.launchesByHour) |
|
.enter().append("g") |
|
.attr("transform", function(d, i) { |
|
var x = xScaleHours(i), |
|
y = yScaleValues(d) + innerDistances.valueTextHeight; |
|
return "translate(" + x + "," + y + ")"; |
|
}); |
|
hoursBars.append("rect") |
|
.attr("class", "bar") |
|
.attr("x", 0) |
|
.attr("y", 0) |
|
.attr("width", xScaleHours.bandwidth()) |
|
.attr("height", function(d) { return orbitPlotInnerHeight - yScaleValues(d); }) |
|
hoursBars.append("text") |
|
.attr("x", xScaleHours.bandwidth() / 2) |
|
.attr("y", -2) |
|
.attr("text-anchor", "middle") |
|
.attr("class", "value-text") |
|
.text(function(d) { return d > 0 ? d : ""; }); |
|
|
|
// Add axes. |
|
var isLastOrbit = (orbitData === summarizedData[summarizedData.length - 1]); |
|
var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; |
|
weekDayG.append("g") |
|
.attr("transform", "translate(0," + orbitPlotHeight + ")") |
|
.call(d3.axisBottom(xScaleWeekDays) |
|
.tickSize(0) |
|
.tickPadding(5) |
|
.tickFormat(function(d) { |
|
// Don't repeat day names over and over, show only at the very bottom. |
|
return isLastOrbit ? WEEKDAYS[d] : ""; |
|
})); |
|
hoursG.append("g") |
|
.attr("transform", "translate(0," + orbitPlotHeight + ")") |
|
.call(d3.axisBottom(xScaleHours) |
|
.tickSize(0) |
|
.tickPadding(5) |
|
// But for hours it is more usable (though uglier) to have |
|
// ticks for each plot. Otherwise it is hard to match bar with |
|
// hour value. |
|
.tickFormat(d3.format("02d"))); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |