/* eslint-disable camelcase */
// external requirements:
// d3
//
// cPanel requirements:
// chart_utilities.js
// ----------------------------------------------------------------------
// NOTE: ABANDON ALL HOPE, YE WHO ENTER HERE WITHOUT KNOWING D3.js!
//
// Actually, it’s not that bad :), but seriously, before maintaining this
// code, please familiarize yourself with D3.js.
// ----------------------------------------------------------------------
( function(window) {
"use strict";
var d3 = window.d3;
var Chart_Utilities = window.Chart_Utilities;
var GRAPH_WIDTH = 620;
var GRAPH_HEIGHT = 200;
// order determines graph order
function sort_nums(a, b) {
return a - b;
}
var resolution_interval = {
"5min": 300000,
hourly: 3600000,
daily: 86400000
};
function draw_protocols_time_graph_key(opts) {
var protocols_order = opts.protocols_order;
var container_path = opts.container_path;
var D3_COLOR = d3.scale.category10()
.domain(protocols_order)
;
var key_sel = d3
.select(container_path)
.append("div")
.classed("graph-key", true)
;
// This is a simple example of a D3 “data join”.
// For more information, see: http://bost.ocks.org/mike/join/
//
var key_item = key_sel
.selectAll(".key-item")
.data(protocols_order)
.enter()
.append("span")
.classed("key-item", true)
;
// For each key item, create a color sample.
key_item
.append("span")
.classed("color-sample", true)
.style("background-color", function(d) {
return D3_COLOR(d);
})
;
// For each key item, add the text.
key_item
.append("span")
.classed("field-name", true)
.html( function(d) {
return " " + d.toUpperCase();
} )
;
}
// Returns whether it actually drew a chart, or just put in the
// text that says there’s no data.
//
// options (in a hash):
//
// container_path a selector that points to where to draw the graph
//
// api_protocol_data data from UAPI’s Bandwidth::query
// For example:
// {
// imap: {
// <unixtime>: 12345,
// ...
// },
// ...
// }
//
// min_date a Date object, earliest date on the X axis
//
// resolution text, how long of a time each data result
// represents: "5min", "hourly", "daily"
//
// time_format d3 axis tickFormat argument
//
// time_ticks OPTIONAL, d3 axis tickValues argument
//
function draw_protocols_time_graph(opts) {
var rear_opts = Object.create(opts);
rear_opts.data = opts.api_protocol_data;
rear_opts.protocols_order = opts.protocols_order;
return _draw_time_graph(rear_opts);
}
function _draw_time_graph(opts) {
var container_path = opts.container_path;
var data_in = opts.data;
var graph_protocols = opts.protocols_order;
var D3_COLOR = d3.scale.category10()
.domain(graph_protocols)
;
// TODO: Maybe better to let the caller handle putting in the text?
if ( !Object.keys(data_in).length ) {
d3.select(container_path)
.text(LOCALE.maketext("There is no data for this period."));
return false;
}
var MIN_TIME = opts.min_date;
var RESOLUTION = resolution_interval[opts.resolution];
var time_format = opts.time_format;
var time_ticks = opts.time_ticks; // optional
var MAX_TIME = opts.max_date ? new Date(opts.max_date) : new Date();
// Inspired by the example at: http://bl.ocks.org/mbostock/3885211
var margin = { top: 20, right: 20, bottom: 30, left: 120 },
width = GRAPH_WIDTH - margin.left - margin.right,
height = GRAPH_HEIGHT - margin.top - margin.bottom;
var svg = d3
.select(container_path)
.append("svg")
.attr("width", GRAPH_WIDTH)
.attr("height", GRAPH_HEIGHT)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
;
var x_scale = d3.time.scale().range( [0, width] )
.domain( [ MIN_TIME, MAX_TIME ] )
;
var x_axis = d3.svg.axis()
.scale(x_scale)
.orient("bottom")
.tickFormat(time_format)
;
if (time_ticks) {
x_axis.tickValues(time_ticks);
}
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(x_axis);
var protocol_data = {};
// If we could rely on D3 for the y-axis tick marks, we wouldn’t
// need this; however, since we’re converting 1,024 bytes to 1 KB,
// 1,048,576 btyes to 1 MB, and so on up, we can’t depend on D3 to
// give us nice, even-looking tick marks; if we did, we’d end up with
// stuff like 488.28 KB instead of “500,000 bytes”.
//
// We need to sum the totals for each sample so that we know the max
// value on the y domain to pass into our custom scaler method.
var total_data_per_time = {};
var protocol_max_unixtime = {};
var min_unixtime = Math.floor( Date.now() / 1000 );
graph_protocols.forEach( function(ptcl) {
protocol_data[ptcl] = [];
if (data_in[ptcl]) {
var unixtimes = Object.keys(data_in[ptcl]).sort(sort_nums);
protocol_max_unixtime[ptcl] = unixtimes[ unixtimes.length - 1 ];
if (unixtimes[0] < min_unixtime) {
min_unixtime = unixtimes[0];
}
}
} );
var bytes_per_minute;
var bytes_divisor = RESOLUTION / 60 / 1000;
// For a stacked graph it’s important that each time point
// have an entry for each dataset. So, go through and, if there’s
// data missing for some point, set it to 0.
var cur_time = new Date(min_unixtime * 1000);
// In this loop we munge the data from what cPanel UAPI gives us
// into a format that D3.js can use.
//
while (cur_time < MAX_TIME) {
var cur_date = new Date(cur_time);
cur_time.setMilliseconds( RESOLUTION + cur_time.getMilliseconds() );
// Ignore anything from before the graph’s time period.
if (cur_date < MIN_TIME) {
continue;
}
var unixtime = cur_date.getTime() / 1000;
if ( !(unixtime in total_data_per_time) ) {
total_data_per_time[unixtime] = 0;
}
// Iterate through the protocols in the passed-in order, not the
// data’s, since the data hash could put them into any order.
// This will make the graph “stack” be in the intended order.
for (var p = 0; p < graph_protocols.length; p++) {
var ptcl = graph_protocols[p];
// Ignore any protocol for which there is no data.
if ( !protocol_max_unixtime[ptcl] ) {
continue;
}
var p_data = data_in[ptcl];
// Convert the a sum for the RESOLUTION period into
// a rate.
//
bytes_per_minute = p_data[unixtime];
if (bytes_per_minute) {
bytes_per_minute /= bytes_divisor;
// Increment the total data count for this
// sample time.
total_data_per_time[unixtime] += bytes_per_minute;
}
// NB: If there’s any data at all for this protocol,
// then D3 needs a data figure for every time point.
//
protocol_data[ptcl].push( {
date: cur_date,
y: bytes_per_minute || 0
} );
}
}
// Finally, we package the protocol_data values into hashes,
// which lets us coordinate a dataset with the right protocol below.
// Might as well strip out any empty datasets while we’re in here.
var graph_data = graph_protocols.slice().reverse().map( function(ptcl) {
if ( protocol_data[ptcl].length ) {
return {
protocol: ptcl,
values: protocol_data[ptcl]
};
}
return false;
} ).filter(Boolean); // i.e., strip out anything that’s not truth-y
var stack = d3.layout.stack()
.values(function(d) {
return d.values;
});
// This will add y0 entries to the protocol_data arrays.
var stacked_data = stack(graph_data);
var tick_values = Chart_Utilities.get_tick_values_for_format_bytes(
d3.max( d3.values(total_data_per_time) )
);
// Now that we have the max Y value, we can build the axis and scale.
var y_scale = d3.scale.linear()
.range( [height, 0] )
.domain(
[ 0, tick_values[ tick_values.length - 1 ] ]
)
;
var y_axis = d3.svg.axis()
.scale(y_scale)
.orient("left")
.tickFormat( function(b) {
return b ? LOCALE.maketext("[format_bytes,_1]/min.", b) : "";
} )
.tickValues(tick_values)
;
svg.append("g")
.attr("class", "y axis")
.call(y_axis)
;
var area = d3.svg.area()
.x( function(d) {
return x_scale(d.date);
} )
.y0( function(d) {
return y_scale(d.y0);
} )
.y1( function(d) {
return y_scale(d.y0 + d.y);
} )
.interpolate("step-after");
// ----------------------------------------------------------------------
var protocol_svg = svg.selectAll(".protocol")
.data(stacked_data)
.enter().append("g")
.attr("class", "protocol")
;
protocol_svg.append("path")
.attr("class", "area")
.attr("d", function(d) {
return area(d.values);
})
.style("fill", function(d) {
return D3_COLOR(d.protocol);
})
;
return true;
}
window.Bandwidth_Graph = {
draw_protocols_time_graph: draw_protocols_time_graph,
draw_protocols_time_graph_key: draw_protocols_time_graph_key
};
}(window) );