Skip to content

Instantly share code, notes, and snippets.

@thomasdullien
Last active April 29, 2019 16:17
Show Gist options
  • Save thomasdullien/b75fb8f3aaedcef4fc0d5ea2f8d5d00e to your computer and use it in GitHub Desktop.
Save thomasdullien/b75fb8f3aaedcef4fc0d5ea2f8d5d00e to your computer and use it in GitHub Desktop.
D3 Diagram to illustrate savings.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Minimal D3 Example</title>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style>
:root {
--savings-color: #A9C3D0F0;
--main-color: #888;
--main-color-darker: #333;
--main-color-lighter: #AAA;
}
.service_bar_pre {
fill: var(--main-color);
stroke-width: 2;
stroke: var(--main-color-darker);
}
.service_bar_post_remainder {
fill: var(--main-color);
stroke-width: 2;
stroke: var(--main-color-darker);
}
.service_bar_post_savings {
fill: var(--savings-color);
stroke-width: 2;
stroke: #fff;
}
.service_bar_third_remainder {
fill: var(--main-color);
stroke-width: 2;
stroke: var(--main-color-darker);
}
.service_bar_third_savings {
fill: var(--savings-color);
stroke-width: 2;
stroke: #fff;
}
.links_pre_post {
stroke-width: 1;
stroke: #fff;
fill: rgba(0,0,0,0.1);
}
.links_post_third {
stroke-width: 0;
fill: lightgrey;
opacity: 0.4;
}
.curly_bracket {
stroke-width: 1;
fill: var(--main-color-darker);
stroke: var(--main-color-darker);
shape-rendering: geometricPrecision;
}
div.tooltip {
position: absolute;
text-align: justify;
width: 80px;
height: 30px;
padding: 5px;
font: 12px sans-serif;
background: var(--main-color-lighter);
border-radius: 4px;
}
div.savings {
position: absolute;
text-align: center;
width: 80px;
height: 30px;
padding: 5px;
font: 12px sans-serif;
background: var(--main-color-lighter);
border-radius: 4px;
}
.bar:hover {
fill: orange;
}
</style>
</head>
<body>
<script type="text/javascript">
var data2 = {
"services": [
{
"name" : "Cassandra",
"description" : "A large Cassandra cluster.",
"percentage" : 32,
"savings" : 29,
"optimizations" : [ "More sophisticated use of compression" ]
},
{
"name" : "ElasticSearch",
"description" : "A large ElasticSearch cluster.",
"percentage" : 31,
"savings" : 31,
"optimizations" : [ "Different use of compression" ]
},
{
"name" : "Service A",
"description" : "An internet-facing data-ingestion service in Python.",
"percentage" : 11,
"savings" : 90,
"optimizations" : [ "Better CPU utilization", "Vectorized loop",
"JIT'ed Regular expressions" ]
},
{
"name" : "Service B",
"description" : "An internet-facing data-ingestion service in Python.",
"percentage" : 11,
"savings" : 67,
"optimizations" : [ "More efficient lock-handling in core datastructure",
"Ensuring JIT'ed execution of the entire code" ]
},
{
"name" : "Service C",
"description" : "An internet-facing data-ingestion service in a mixture of Python and C.",
"percentage" : 11,
"savings" : 35,
"optimizations" : [ "Improved de-duplication and caching of data",
"Reduction of initialization costs by implementing a forkserver" ]
},
{
"name" : "Misc. services",
"description" : "All other services lumped together.",
"percentage" : 4,
"savings" : 0,
"optimizations" : []
}
]
};
var total_width = 300;
var total_height = 500;
var bar_width = 40 / (500.0 / total_width);
var big_font_size = total_width / 30;
var small_font_size = total_width / 40;
// Use the margin convention.
var margin = {top: 20, right: 40, bottom: 20, left: 10};
var width = total_width - margin.left - margin.right,
height = total_height - margin.top - margin.bottom;
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 + ")");
// Div for the tooltips.
var div = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0.2);
// Div for the savings.
var div2 = d3.select("body")
.append("div")
.attr("class", "savings")
.style("opacity", 1.0);
data = data2.services;
pre_bar_x_position = 0.25*(width / 5) - (bar_width / 2);
post_bar_x_position = 1.25*(width / 5) - (bar_width / 2);
third_bar_x_position = 2.25*(width / 5) - (bar_width / 2);
legend_bar_x_position = 3.25*(width / 5) - (bar_width / 2);
legend_bar_width = total_width - legend_bar_x_position;
// For each service, we wish to create 5 rectangles: One on the very left
// that represents the service prior to our optimization, two in the middle
// that show how our optimization split savings off of the original service,
// and two in the far right that are just re-arrangements of the blocks in the
// middle to group all the "savings" and all the "leftover" bits.
// Helper functions for calculating positions.
var pre_bar_height = function(data_item) {
fractional_height = data_item.percentage / 100.0;
return height * fractional_height;
}
var post_bar_height_savings = function(data_item) {
normal_height = pre_bar_height(data_item);
savings = data_item.savings / 100.0;
return normal_height * savings;
}
var post_bar_height_remainder = function(data_item) {
normal_height = pre_bar_height(data_item);
remain = 1.0 - (data_item.savings / 100.0);
return normal_height * remain;
}
// The code is cleanest if we annotate all the data elements with their x and
// y positions before we start. I know that this is not the "d3 way", but a
// different attempt yielded rather messy code.
pre_y_position = 0;
total_savings_height = 0;
total_remainder_height = 0;
for (var i = 0; i < data.length; ++i) {
data[i].pre_y_position = pre_y_position;
data[i].post_savings_y_position = pre_y_position;
data[i].post_remainder_y_position = pre_y_position + post_bar_height_savings(data[i]);
pre_y_position += pre_bar_height(data[i]);
total_savings_height += post_bar_height_savings(data[i]);
total_remainder_height += post_bar_height_remainder(data[i]);
}
third_savings_y_position = 0;
third_remainder_y_position = total_savings_height;
for (var i = 0; i < data.length; ++i) {
data[i].third_savings_y_position = third_savings_y_position;
data[i].third_remainder_y_position = third_remainder_y_position;
third_savings_y_position += post_bar_height_savings(data[i]);
third_remainder_y_position += post_bar_height_remainder(data[i]);
}
total_savings_percentage = Math.round(total_savings_height / (total_savings_height +
total_remainder_height) * 100.0);
// All position data is calculated. Draw the diagram.
// Begin by setting the legend div to be the right size.
div.style("left", legend_bar_x_position + margin.left + "px" )
.style("top", (total_savings_height + margin.top) + "px")
.style("height", (total_remainder_height) + "px")
.style("width", legend_bar_width + "px")
.style("font-size", small_font_size + "pt");
// The percentage div.
percentage_div_height = big_font_size + 10;
div2.style("left", legend_bar_x_position + margin.left + "px" )
.style("top", (((total_savings_height) / 2) + margin.top - percentage_div_height/2 ) + "px")
.style("height", percentage_div_height + "px")
.style("width", legend_bar_width + "px")
.style("font-size", big_font_size + "pt")
.html("40% savings!");
// Draw sankey-style curves between the first and second bar chart.
// SVG unfortunately does not natively support splines that smoothly interpolate
// through control points, only Bezier curves (which are more difficult to
// handle). To simplify life, the following code takes a series of points,
// interpolates a Catmull-Rom spline through them, then converts the results
// to arrays of three points for the path to plot.
// Code that interpolates a Catmull-Rom spline through a few points, then emits
// the control points for Bezier Curves (SVG Path drawing) to draw them.
function catmullRom2bezier(points) {
var result = [];
for (var i = 0; i < points.length - 1; i++) {
var p = [];
p.push({
x: points[Math.max(i - 1, 0)].x,
y: points[Math.max(i - 1, 0)].y
});
p.push({
x: points[i].x,
y: points[i].y
});
p.push({
x: points[i + 1].x,
y: points[i + 1].y
});
p.push({
x: points[Math.min(i + 2, points.length - 1)].x,
y: points[Math.min(i + 2, points.length - 1)].y
});
// Catmull-Rom to Cubic Bezier conversion matrix
// 0 1 0 0
// -1/6 1 1/6 0
// 0 1/6 1 -1/6
// 0 0 1 0
var bp = [];
bp.push({
x: ((-p[0].x + 6 * p[1].x + p[2].x) / 6),
y: ((-p[0].y + 6 * p[1].y + p[2].y) / 6)
});
bp.push({
x: ((p[1].x + 6 * p[2].x - p[3].x) / 6),
y: ((p[1].y + 6 * p[2].y - p[3].y) / 6)
});
bp.push({
x: p[2].x,
y: p[2].y
});
result.push(bp);
}
return result;
}
// Given a series of points, generate a path through them.
function makePath(points, initialmove) {
var result = "";
if (initialmove) {
var result = "M" + points[0].x + "," + points[0].y + " ";
}
var catmull = catmullRom2bezier(points);
for (var i = 0; i < catmull.length; i++) {
result += "C" + catmull[i][0].x + "," + catmull[i][0].y + " " + catmull[i][1].x + "," + catmull[i][1].y + " " + catmull[i][2].x + "," + catmull[i][2].y + " ";
}
return result;
}
function makeLines(points) {
//var result = "M" + points[0].x + "," + points[0].y + " ";
result = "";
for (var i = 0; i < points.length-1; ++i) {
result += "L" + points[i].x + "," + points[i].y + " " + points[i+1].x + "," +
points[i+1].y;
}
return result;
}
// Constructs a path between two rectangles in a bar chart, showing flow between
// them, Sankey-diagram style.
function getSanKeyPath(start, end, start2, end2) {
var xcurvature = 0.3;
var ycurvature = 0.2;
// Interpolate two intermediate points: One close to start on the same y as
// the start, and one close to the end with the same y as close to the end.
intermediate_point_upper_A = {
x : d3.interpolateNumber(start.x, end.x)(xcurvature),
y : d3.interpolateNumber(start.y, end.y)(ycurvature) };
intermediate_point_upper_B = {
x : d3.interpolateNumber(start.x, end.x)(1-xcurvature),
y : d3.interpolateNumber(start.y, end.y)(1-ycurvature) };
intermediate_point_lower_A = {
x : d3.interpolateNumber(start2.x, end2.x)(xcurvature),
y : d3.interpolateNumber(start2.y, end2.y)(ycurvature) };
intermediate_point_lower_B = {
x : d3.interpolateNumber(start2.x, end2.x)(1-xcurvature),
y : d3.interpolateNumber(start2.y, end2.y)(1-ycurvature) };
upper_path = [ start, intermediate_point_upper_A, intermediate_point_upper_B,
end ];
lower_path = [ start2, intermediate_point_lower_A, intermediate_point_lower_B,
end2 ];
// Spline from left upper side to right upper side.
path = makePath( upper_path, true )
// Linear segment downward.
path += "L" + end.x + "," + end.y + " " + start2.x + "," + start2.y;
// Spline from lower right side to lower left side.
path += makePath( lower_path, false )
return path;
}
// Only draw the links on a large-enough screen:
if (total_width > 250) {
svg.selectAll(".links_pre_post")
.data(data)
.enter()
.append("path")
.attr({
class: "links_pre_post",
d: function(d) {
startpoint = { x : pre_bar_x_position + bar_width,
y: d.pre_y_position };
endpoint = { x : post_bar_x_position,
y : d.post_remainder_y_position };
startpoint2 = { x : post_bar_x_position,
y : d.post_remainder_y_position + post_bar_height_remainder(d) };
endpoint2 = { x : pre_bar_x_position + bar_width,
y : d.pre_y_position + pre_bar_height(d) };
return getSanKeyPath(startpoint, endpoint, startpoint2, endpoint2);
}
});
svg.selectAll(".links_post_third")
.data(data)
.enter()
.append("path")
.attr({
class: "links_post_third",
d: function(d) {
startpoint = { x : post_bar_x_position + bar_width,
y: d.post_remainder_y_position };
endpoint = { x : third_bar_x_position,
y : d.third_remainder_y_position };
startpoint2 = { x : third_bar_x_position,
y : d.third_remainder_y_position + post_bar_height_remainder(d) };
endpoint2 = { x : post_bar_x_position + bar_width,
y : d.post_remainder_y_position + post_bar_height_remainder(d) };
return getSanKeyPath(startpoint, endpoint, startpoint2, endpoint2);
}
});
}
// The leftmost bar.
legend_for_initial_service = function(d) {
result = "<h3 style=\"text-align: center\">" + d.name + "</h3>" +
"<h4 style=\"text-align: center\">" + d.description + "</h4>" +
d.percentage + "% of the total cloud spending " +
"was used for this service before the project started.";
return result;
}
legend_for_shrunk_service = function(d) {
result = "<h3 style=\"text-align: center\">" + d.name + "</h3>" +
"<h4 style=\"text-align: center\">" + d.description + "</h4>" +
"The cost of this service was shrunk by " + d.savings + "% through our " +
"optimizations.<br>";
if (d.optimizations.length > 0) {
result += "The following optimizations were performed: <ul>";
for (i = 0; i < d.optimizations.length; ++i) {
result += "<li>" + d.optimizations[i] + "</li>";
}
result += "</ul>";
}
return result;
}
legend_for_third_column = function(d) {
result = "<h3 style=\"text-align: center\">" + d.name + "</h3>" +
"<h4 style=\"text-align: center\">" + d.description + "</h4>" +
"After our optimizations, the service used " + (100.0-d.savings) + "% of " +
"the original cost.";
return result;
}
legend_for_third_column_savings = function(d) {
result = "<h3 style=\"text-align: center\">" + d.name + "</h3>" +
"<h4 style=\"text-align: center\">" + d.description + "</h4>" +
"Our optimizations reduced the cost of the service by " + d.savings + "%."
return result;
}
svg.selectAll(".service_bar_pre")
.data(data)
.enter()
.append("rect")
.attr({
class : "service_bar_pre",
width : bar_width,
height: pre_bar_height,
y : function(d) { return d.pre_y_position; },
x : pre_bar_x_position
})
.on("mouseover", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
div.transition()
.duration(200)
.style("opacity", 1);
div.html(
legend_for_initial_service(d));
})
.on("mouseout", function(d) {
div.transition()
.duration(200)
.style("opacity", 0.2);
div.html("")
});
// The middle bar. Draw the savings first.
svg.selectAll(".service_bar_post_savings")
.data(data)
.enter()
.append("rect")
.attr({
class : "service_bar_post_savings",
width : bar_width,
height: post_bar_height_savings,
y : function(d) { return d.post_savings_y_position; },
x : post_bar_x_position
})
.on("mouseover", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
div.transition()
.duration(200)
.style("opacity", 1);
div.html(
legend_for_shrunk_service(d));
})
.on("mouseout", function(d) {
div.transition()
.duration(200)
.style("opacity", 0.2);
div.html("");
});
// Now draw the remaining costs.
svg.selectAll(".service_bar_post_remainder")
.data(data)
.enter()
.append("rect")
.attr({
class : "service_bar_post_remainder",
width : bar_width,
height: post_bar_height_remainder,
y : function(d) { return d.post_remainder_y_position; },
x : post_bar_x_position
})
.on("mouseover", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
div.transition()
.duration(200)
.style("opacity", 1);
div.html(
legend_for_shrunk_service(d));
})
.on("mouseout", function(d) {
console.log("mouseout");
div.transition()
.duration(200)
.style("opacity", 0.2);
});
// The rightmost bar.
// Draw the savings first.
svg.selectAll(".service_bar_third_savings")
.data(data2.services)
.enter()
.append("rect")
.attr({
class : "service_bar_third_savings",
width : bar_width,
height: post_bar_height_savings,
y : function(d) { return d.third_savings_y_position; },
x : third_bar_x_position
})
.on("mouseover", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
div.transition()
.duration(200)
.style("opacity", 1);
div.html(
legend_for_third_column_savings(d));
})
.on("mouseout", function(d) {
div.transition()
.duration(200)
.style("opacity", 0.2);
div.html("");
});
// Draw the remainders.
svg.selectAll(".service_bar_third_remainder")
.data(data2.services)
.enter()
.append("rect")
.attr({
class : "service_bar_third_remainder",
width : bar_width,
height: post_bar_height_remainder,
y : function(d) { return d.third_remainder_y_position; },
x : third_bar_x_position
})
.on("mouseover", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
div.transition()
.duration(200)
.style("opacity", 1);
div.html(
legend_for_third_column(d));
})
.on("mouseout", function(d) {
div.transition()
.duration(200)
.style("opacity", 0.2);
div.html("")
});
// Code to draw legends, popups, and so forth.
// A function to draw a curly bracket path. Returns a path for
// a curly brace between x1,y1 and x2,y2, w pixels wide
// and q factor, .5 is normal, higher q = more expressive bracket.
function makeCurlyBraceHalf(x1, y1, x2, y2, thickness, curviness) {
// The midpoint between the target and the start point is where the linear
// segment will live. This will be the "midpoint" of the line (in terms of
// thickness.
linear_segment_x = d3.interpolateNumber(x1, x2)(0.5);
linear_segment_x_right = linear_segment_x + thickness / 2;
linear_segment_x_left = linear_segment_x - thickness / 2;
// The y position of the start of the linear segment is dictated by the
// curviness parameter.
linear_segment_start_y = d3.interpolateNumber(y1, y2)(curviness);
linear_segment_end_y = d3.interpolateNumber(y1, y2)(1-curviness);
linear_segment_right_start = { x : linear_segment_x_right,
y : linear_segment_start_y };
linear_segment_right_end = { x : linear_segment_x_right,
y : linear_segment_end_y };
linear_segment_left_start = { x : linear_segment_x_left,
y: linear_segment_end_y };
linear_segment_left_end = { x : linear_segment_x_left,
y: linear_segment_start_y };
// Now interpolating spline points for the angled segments are needed.
interp_right = {
x : d3.interpolateNumber(x1, linear_segment_x_right)(1-curviness),
y : d3.interpolateNumber(y1, linear_segment_start_y)(curviness) }
interp_left = {
x : d3.interpolateNumber(x1, linear_segment_x_left)(1-curviness),
y : d3.interpolateNumber(y1, linear_segment_start_y)(curviness) }
start_spline_right = { x : x1, y : y1 }
end_spline_right = { x : linear_segment_x_right, y : linear_segment_start_y }
start_spline_left = { x : linear_segment_x_left, y : linear_segment_start_y }
end_spline_left = start_spline_right;
spline_right = makePath( [start_spline_right, interp_right, end_spline_right ], true);
spline_left = makePath( [start_spline_left, interp_left, end_spline_left ], false)
interp_target_right = {
x : d3.interpolateNumber( linear_segment_x_right, x2 )(curviness),
y : d3.interpolateNumber( linear_segment_end_y, y2 )(1-curviness),
}
interp_target_left = {
x : d3.interpolateNumber( linear_segment_x_left, x2 )(curviness),
y : d3.interpolateNumber( linear_segment_end_y, y2) (1-curviness)
}
lower_spline_right = makePath(
[ linear_segment_right_end , interp_target_right, { x : x2, y: y2 }], false);
lower_spline_left = makePath(
[{ x: x2, y: y2 }, interp_target_left, linear_segment_left_start ])
linear_segment_right = makeLines(
[ linear_segment_right_start, linear_segment_right_end ]);
linear_segment_left = makeLines(
[ linear_segment_left_start, linear_segment_left_end ] );
result = spline_right + linear_segment_right + lower_spline_right +
lower_spline_left + linear_segment_left + spline_left;
return result; // spline_right + linear_segment_right + spline_left;
}
function makeCurlyBrace(x1,y1,x2,y2,target_x, target_y, thickness, curviness) {
upper_brace = makeCurlyBraceHalf(x1, y1, target_x, target_y, thickness, curviness);
lower_brace = makeCurlyBraceHalf(x2, y2, target_x, target_y, thickness, curviness);
return upper_brace + lower_brace;
}
svg.append("g")
.append("path")
.attr({
class: "curly_bracket",
d: makeCurlyBrace(third_bar_x_position + bar_width + 5, 0, third_bar_x_position
+ bar_width + 5, total_savings_height,
third_bar_x_position + bar_width + (20 / (500.0/width)), d3.interpolateNumber(
0, total_savings_height)(0.5), 3, 0.3)
});
/*
svg.append("g")
.append("text")
.attr({
class: "percentage_savings",
x : third_bar_x_position + bar_width + (35 / (500.0/width)),
y : d3.interpolateNumber(0, total_savings_height)(0.5) + (big_font_size/2)} )
.style("font-size", big_font_size + "pt")
.style("font-family", "sans-serif")
.text( function(d) { return total_savings_percentage + "% savings!"} );
*/
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment