Created
June 4, 2014 20:19
-
-
Save Veraticus/b5f3d119b51db8650ab9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var svg_label_font_size = "10px"; | |
var margin = {top: 20, right: 220, bottom: 20, left: 35}, | |
width = 960 - margin.left - margin.right, | |
height = 500 - margin.top - margin.bottom; | |
var bisectMonth = d3.bisector(function(d) { return d.month; }).left, | |
formatCurrencyValue = d3.format(",.2f"), | |
formatCurrency = function(d) { return "$" + formatCurrencyValue(d); }, | |
formatPercentValue = d3.format(",.0f"), | |
formatPercent = function(a,b) { return (b > 0) ? formatPercentValue(100 * a / b) + "%" : ""; }; | |
var x = d3.scale.linear() | |
.range([0, width]); | |
var y = d3.scale.linear() | |
.range([height, 0]); | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.ticks(36) | |
.tickSize(3) | |
.orient("bottom"); | |
var yAxis = d3.svg.axis() | |
.scale(y) | |
.tickSize(3) | |
.orient("left"); | |
var line = d3.svg.line() | |
.x(function(d) { return x(d.month); }) | |
.y(function(d) { return y(d.cost); }); | |
var svg = d3.select("#d3div").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 + ")"); | |
// Add the title. | |
//svg.append("g") | |
// .append("text") | |
// .attr("x", width/16) | |
// .attr("y", 20) | |
// .style("font-size", "20px") | |
// .style("font-weight", "bold") | |
// .text("AWS EC2 Price Comparison"); | |
// Add the x-axis. | |
svg.append("g") | |
.attr("class", "x axis") | |
.attr("transform", "translate(0," + height + ")") | |
.append("text") | |
.attr("x", width + 3) | |
.text("Month"); | |
// Add the y-axis. | |
svg.append("g") | |
.attr("class", "y axis") | |
.append("text") | |
.attr("transform", "rotate(-90)") | |
.attr("y", 6) | |
.attr("dy", ".71em") | |
.style("text-anchor", "end") | |
.text("Total Cost (USD)"); | |
var SECONDS_IN_YEAR = 31536000; | |
var SECONDS_IN_MONTH = SECONDS_IN_YEAR / 12;// 2628000 | |
var HOURS_IN_MONTH = 365 * 24 / 12; // 730 | |
function createUniqueName(value, durationInYears) { | |
if (durationInYears == null) | |
durationInYears = value.duration / SECONDS_IN_YEAR; | |
return ((durationInYears > 0) ? durationInYears + "-yr" : "") | |
+ " " + value.offeringType | |
+ " " + value.productDescription | |
+ " " + value.instanceType; | |
} | |
function splitUniqueNameToHTML(name) { | |
var i = name.lastIndexOf(" "); | |
return i == -1 | |
? name | |
: name.substr(0,i) + "<br>" + name.substr(i); | |
} | |
function calculateTotalCost(offering, monthlyPrice) { | |
var totalMonthlyCost = []; | |
var durationInMonths = offering.value.duration / SECONDS_IN_MONTH; | |
totalMonthlyCost[0] = { | |
month: 0, | |
cost: +offering.value.fixedPrice | |
}; | |
for (var month = 1; month <= 36; month++) { | |
var terms = offering.value.fixedPrice > 0 | |
? Math.floor(((month - 1) / durationInMonths) + 1) // 1 year term 1 = months 0-12, 1 year term 2 = months 13-24 | |
: 0; // On-Demand | |
totalMonthlyCost[month] = { | |
month: month, | |
cost: (terms * offering.value.fixedPrice) + (month * monthlyPrice) | |
} | |
} | |
return totalMonthlyCost; | |
} | |
var currentMonth = 36; | |
update(); | |
function update() { | |
// Construct REST URL | |
var resturl = "http://p1software-eb1.elasticbeanstalk.com/awsec2offering/api" | |
+ "/" + d3.select("#availabilityZone").node().value // us-east-1a | |
+ "/" + d3.select("#productDescription").node().value // linux | |
+ "/" + d3.select("#offeringType").node().value // heavy | |
+ "/" + d3.select("#instanceType1").node().value // t1.micro | |
+ "," + d3.select("#instanceType2").node().value; // m1.small | |
// resturl = "test.json" | |
// resturl = "http://localhost:8080/awsec2offering/awsec2offering/api/us-east-1a/linux/heavy/t1.micro,m1.small." | |
d3.json(resturl, function(error, json) { | |
data = json.ec2offerings; | |
// Assign colors to each unique name. | |
var color = d3.scale.category10(); | |
color.domain(data.map(function(p) { return createUniqueName(p); })); | |
// Coerce the data to numbers. | |
data.forEach(function(d) { | |
d.month = +d.month; | |
}); | |
// Extract the array in the json object into rates. | |
var rates = d3.entries(data).map(function(offering) { | |
var durationInYears = offering.value.duration / SECONDS_IN_YEAR; | |
var monthlyPrice = offering.value.hourlyPrice * HOURS_IN_MONTH; | |
var z = { | |
name: createUniqueName(offering.value, durationInYears), | |
durationInYears: durationInYears, | |
fixedPrice: offering.value.fixedPrice, | |
monthlyPrice: monthlyPrice, | |
values: calculateTotalCost(offering, monthlyPrice) | |
} | |
return z; | |
}); | |
x.domain([ | |
d3.min(rates, function(c) { return d3.min(c.values, function(v) { return v.month; }); }), | |
d3.max(rates, function(c) { return d3.max(c.values, function(v) { return v.month; }); }) | |
]); | |
y.domain([ | |
d3.min(rates, function(c) { return d3.min(c.values, function(v) { return v.cost; }); }), | |
d3.max(rates, function(c) { return d3.max(c.values, function(v) { return v.cost; }); }) | |
]); | |
// Update the x-axis. | |
d3.transition(svg).select('.x.axis') | |
.call(xAxis); | |
// Update y-axis. | |
d3.transition(svg).select('.y.axis') | |
.call(yAxis); | |
// DATA JOIN | |
var rate = svg.selectAll(".rate") | |
.data(rates); | |
// ENTER | |
var rateEnter = rate.enter().append("g") | |
.attr("class", "rate"); | |
// Add the lines. | |
rateEnter.append("path") | |
.attr("class", "line") | |
.attr("d", function(d) { return line(d.values); }) | |
.style("stroke", function(d) { return color(d.name); }); | |
// Add the line labels in the right margin. | |
rateEnter.append("text") | |
.attr("font-size", svg_label_font_size) | |
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; }) | |
.attr("transform", function(d) { return "translate(" + x(d.value.month) + "," + y(d.value.cost) + ")"; }) | |
.attr("x", 3) | |
.attr("dy", ".35em") | |
.text(function(d) { return d.name; }); | |
// http://bl.ocks.org/mbostock/3902569 | |
// http://bl.ocks.org/gniemetz/4618602 | |
// http://bl.ocks.org/benjchristensen/2657838 | |
// http://stackoverflow.com/questions/19003832/d3-x-value-mouseover-function-returns-nan | |
// http://jsfiddle.net/U4CGz/7/ | |
// http://code.shutterstock.com/rickshaw/examples/lines.html | |
var focus = svg.append("g") | |
.attr("class", "focus") | |
.style("display", "none"); | |
var circles = focus.selectAll('circle') | |
.data(rates) | |
.enter() | |
.append('circle') | |
.attr('class', 'circle') | |
.attr('r', 4) | |
.attr('fill', 'none') | |
.attr('stroke', function (d) { return color(d.name); }); | |
rate.append("rect") | |
.attr("class", "overlay") | |
.attr("width", width) | |
.attr("height", height) | |
.on("mouseover", function() { focus.style("display", null); }) | |
.on("mouseout", function() { focus.style("display", "none"); }) | |
.on("mousemove", mousemove); | |
// Force table paint | |
updateTable(currentMonth, rates); | |
function mousemove() { | |
var x0 = x.invert(d3.mouse(this)[0]); // 0.00 .. 36.00 | |
currentMonth = Math.round(x0); // 0 .. 36 | |
circles.attr('transform', function (d) { | |
return 'translate(' + x(currentMonth) + ',' + y(d.values[currentMonth].cost) + ')'; | |
}); | |
updateTable(currentMonth, rates); | |
} // mousemove | |
function updateTable(month, rates) { | |
// Load month's costs into sortedRows, then sort from high to low | |
var sortedRows = []; | |
for (i = 0; i < rates.length; i++) { | |
sortedRows.push({ | |
name: rates[i].name, | |
durationInYears: rates[i].durationInYears, | |
fixedPrice: rates[i].fixedPrice, | |
monthlyPrice: rates[i].monthlyPrice, | |
cost: rates[i].values[month].cost | |
}); | |
} | |
sortedRows.sort(function(a,b) { return b.cost - a.cost;}); | |
// https://groups.google.com/forum/#!topic/d3-js/LPVuNpPm0Wc | |
d3.select("#th-cost").text(month + " Month"); | |
for (i = 0; i < sortedRows.length; i++) { | |
// Fixed columns | |
d3.select("#td-fixed" + i).text(formatCurrency(sortedRows[i].fixedPrice)); | |
d3.select("#td-monthly" + i).text(formatCurrency(sortedRows[i].monthlyPrice)); | |
// Calculate total costs | |
var iCost = sortedRows[i].cost; | |
var iCostId = "#td-cost" + i; | |
// d3.select("#td-name" + i).text(sortedRows[i].name); | |
d3.select("#td-name" + i).html(splitUniqueNameToHTML(sortedRows[i].name)); | |
d3.select(iCostId).text(formatCurrency(iCost)) | |
.style('color', color(sortedRows[i].name)); | |
// Calculate the savings triangle | |
for (j = i + 1; j < sortedRows.length; j++) { | |
var jCost = sortedRows[j].cost; | |
var jCostId = "#td-cost" + i + "-" + j; | |
var savings = iCost - jCost; | |
d3.select(jCostId) | |
.html("+" + formatCurrency(savings) + "<br>+" + formatPercent(savings, jCost)) | |
.style('color', color(sortedRows[j].name)); | |
} | |
} | |
} // updateTable | |
var rateUpdate = d3.transition(rate); | |
rateUpdate.select("path") | |
.transition().duration(600) | |
.attr("d", function(d) { | |
return line(d.values); | |
}); | |
rateUpdate.select("text") | |
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; }) | |
.transition().duration(600) | |
.attr("transform", function(d,i) { return "translate(" + x(d.value.month) + "," + y(d.value.cost) + ")"; }) | |
.text(function(d) { return d.name; }); | |
// EXIT | |
rate.exit().remove(); | |
}); | |
} // update |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment