blob: db1d222919d5bcf2df8da4dc5d7f8daae5ffedd0 [file] [log] [blame]
// TODO:
// 1. auto-resizing graph, change the axis range
// 2. set axis range, set title
// 4. show coordinate
// 6. fix the offline plotter
// Error msg:
var maxPlotsAllowed = 20;
errMsg = {ENUMPLOTS: 'More than'+maxPlotsAllowed+' will be generated.\n'
+'Please consider adding filters or geometric mean / legend to '
+'reduce the number of plots!\n'
+'Current plotting aborted' };
// Some const
// if x axis is an element of timeParam, then the default behavior is to gmean everything
var timeParam = ['run', 'parsed_date'];
var unixEpoch = ['parsed_date'];
var defaultGmean = 'circuit';
// the value for invalid y. these will be eliminated by the plotter
var invalidY = -1;
// create the actual plot in the display_canvas
//
var overlay_list = [];
var gmean_list = [];
// raw data means it is not processed by gmean, and its param name has not been manipulated.
// NOTE: we don't store the data of default plot (cuz they are not manipulated)
var raw_data = null;
// store data after gmean
// each task has a folder, each plot has a subfolder, each line has a file (containing only x and y)
// {task: {plot: {line: [[x, y]]}}}
var manipulatedData = null;
var gmean_data = null;
var range = [];
// xNameMap is only set when value in x axis is string
var xNameMap = null;
var formGmean;
var formOverlay;
var first_plot = false;
// all margins are the margins between canvsvg and canvsvg -> rect
// not between the canvsvg and canvsvg -> g
var ratio = {canvToWholeWid: 1060/(1060+260),
legToWholeWid: 240/(1060+260),
wholeHeiToWid: 750/(1060+260),
// margin.top / wholeHeight
marginTopToWhole: 100/750,
marginBotToWhole: 100/750,
// margin.left / wholeWidth
marginLefToWhole: 150/(1060+260),
marginRigToWhole: 10/(1060+260)};
// display option
var numPlotsPerRow = 1;
// attach event listeners when ready
$(document).ready(function() {
var custom_panel = document.getElementById("custom_panel");
var customize_plot = document.getElementById("customizePlot");
customize_plot.addEventListener('click', function () {
generate_overlay_selector();
}, false);
var save_plot = document.getElementById("savePlot");
save_plot.addEventListener('click', savePlot);
var save_plot_data = document.getElementById("savePlotData");
save_plot_data.addEventListener('click', savePlotData);
formGmean = document.getElementById("gmean_select").getElementsByTagName("fieldset")[0];
formOverlay = document.getElementById("overlay_select").getElementsByTagName("fieldset")[0];
var get_custom_plot = document.getElementById("get_custom_plot");
get_custom_plot.addEventListener('click', function () {
gmean_list = [];
//var checkbox = formGmean.getElementsByTagName('input');
var checkbox = d3.select('#gmean_select').selectAll('input');
checkbox.each(function () {
if (d3.select(this).property('checked')) {
gmean_list.push(d3.select(this).attr('index'));
}
});
overlay_list = [];
//var checkbox = formOverlay.getElementsByTagName('input');
checkbox = d3.select('#overlay_select').selectAll('input');
checkbox.each(function () {
if (d3.select(this).property('checked')
&& gmean_list.indexOf(d3.select(this).attr('index')) == -1) {
overlay_list.push(d3.select(this).attr('index'));
}
});
plot_generator();}
);
});
/*
* high level function after data is obtained from database interface
* -- generate the default plots
*/
function plotter_setup(data) {
if (data.status !== 'OK') {
report_error(data.status);
return;
}
console.log('--- data ---');
console.log(data);
// initialize
raw_data = data;
raw_data.tasks = _.map(raw_data.tasks, function(d){return d.split('|')[0]; });
gmean_data = null;
overlay_list = [];
gmean_list = [];
range = [];
xNameMap = [];
document.getElementById('customizePlot').style.visibility = 'visible';
document.getElementById('savePlot').style.visibility = 'visible';
document.getElementById('savePlotData').style.visibility = 'visible';
d3.select('#generate_plot').html('');
d3.select('#generate_plot').text('Regenerate Plots');
d3.select('#chart').html('');
// clear up
remove_inputs(formGmean);
remove_inputs(formOverlay);
document.getElementById('custom_panel').style.visibility = 'hidden';
// convert x value to numerical
indexXString();
// check is x is run / parsed_date
// if yes, then geomean on everything
// if not, then supress on params that will generate the least subplots
range = [];
manipulatedData = {};
for (var i = 0; i < raw_data.data.length; i++) {
range.push({x: [], y: []});
range[i]['x'] = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
range[i]['y'] = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
manipulatedData[raw_data.tasks[i]] = {};
}
if (timeParam.indexOf(raw_data.params[0]) !== -1) {
// geomean on everything.
defaultToGmeanTimePlot();
// possibly show the generate_overlay_selector as well
} else {
// choose the axis with the most distinct values and overlay on it
defaultToGmeanSubPlot();
}
}
/*
* generate the default plot if x axis is time
* methodology: gmean over everything
*/
function defaultToGmeanTimePlot() {
first_plot = true;
d3.select('#chart').html('');
// flattenedData = _.flatten(raw_data, true);
for (var i = 0; i < raw_data.data.length; i++) {
groupedX = _.groupBy(raw_data.data[i], function (dotSeries) {return dotSeries[0]} );
seriesXY = [];
for (var k in groupedX) {
groupedX[k] = reduceToGmean(groupedX[k]);
// now {x1: y1, x2: y2, ...}
// should convert it [[x1,y1],[x2,y2],...]
seriesXY.push([Number(k), groupedX[k], raw_data.tasks[i]]);
}
findAxisScale(seriesXY, xNameMap[i], i);
d3.select('#chart').append('div').attr('class', 'task_container').attr('id', 'task_'+i);
simple_plot(raw_data.params, seriesXY, [], xNameMap[i], i, 'taskTitle');
sizingTaskContainer(1, 'task_'+i);
}
}
/*
* generate the default plot when x is not time
* methodology: gmean over circuits, overlay on the filter which has the most distinct values
*/
function defaultToGmeanSubPlot() {
first_plot = true;
// check if circuit is in the filter:
// it should be, cuz it is the primary key
var gmeanIndex = raw_data.params.indexOf(defaultGmean);
if (gmeanIndex == -1) {
alert('circuit (primary key) is not in the param list\nskip default plot');
generate_overlay_selector();
} else if (gmeanIndex == 0 || gmeanIndex == 1) {
alert('circuit is in x y axis: \nskip default plot');
generate_overlay_selector();
} else {
for (var k in raw_data.data) {
var data = [];
gmeanPrepData = data_transform(raw_data.data[k], [(gmeanIndex-2)+''], 'non-overlay');
for (var p in gmeanPrepData) {
// now gmeanPrepData[p] is a subplot overlaying on circuit
// next: gruop on x and gmean on y
var groupedX = data_transform(gmeanPrepData[p], [(0-2)+''], 'xy group');
var seriesXY = [];
for (var l in groupedX) {
groupedX[l] = reduceToGmean(groupedX[l]);
seriesXY.push([l, groupedX[l], p]);
}
findAxisScale(seriesXY, xNameMap[k], k);
data.push(seriesXY);
}
var newRawData = [];
var newParams = [];
for (var i in raw_data.params) {
if (raw_data.params[i] != defaultGmean ) {
newParams.push(raw_data.params[i]);
}
}
for (var i in data) {
//simple_plot(raw_data.params, data[i], []);
// convert data again to support overlay again
for (var j in data[i]) {
var paramSplit = data[i][j][2].split(' ');
var temp = [];
for (var s in paramSplit) {
if (s != 0) {
temp.push(paramSplit[s]);
}
}
paramSplit = temp;
//console.log(paramSplit);
data[i][j] = [data[i][j][0], data[i][j][1]].concat(paramSplit);
newRawData.push(data[i][j]);
}
}
newOverlayList = [findMostExpensiveAxis(newRawData, newParams) + ''];
var newData = data_transform(newRawData, newOverlayList, 'non-overlay');
var c = d3.select('#chart').append('div').attr('class', 'task_container').attr('id', 'task_'+k);
var t = c.append('h3').attr('class', 'task_title');
t.append('span').attr('class', 'h_grey').append('text').text('Task Name: ');
t.append('span').attr('class', 'h_dark').append('text').text(raw_data.tasks[k]);
var s = c.append('h4').attr('class', 'task_title');
s.append('span').attr('class', 'h_grey').append('text').text('Geo Mean Axis: ');
s.append('span').attr('class', 'h_dark').append('text').text(defaultGmean);
var count = 0;
for (var j in newData) {
count += 1;
}
if (count <= maxPlotsAllowed) {
for (var j in newData) {
simple_plot(newParams, newData[j], newOverlayList, xNameMap[k], k, 'normalTitle');
}
} else {
count = 0;
alert(errMsg['ENUMPLOTS']);
}
sizingTaskContainer(count, 'task_'+k);
}
}
}
/*
* reduce a list to a single gmean value
*/
function reduceToGmean(groupedX) {
// groupedX: eliminate all other irrelevant values
groupedX = _.map(groupedX, function (list) {return list[1];} );
// count the number of tuples with the valid y value
var validCount = 0;
groupedX = _.reduce( groupedX,
function (memo, num) {
if (num > 0) {
memo *= num;
validCount += 1;
}
return memo;
},
1);
// setup the gmean entry for groupedX
return (validCount > 0) ? Math.pow(groupedX, 1.0/validCount) : -1;
}
/*
* to generate the overlay selector panel
* the plot will be generated after button is clicked.
* type is to used to decide whether to generate axes of raw data or gmean data
*/
function remove_inputs(parent) {
var inputs = parent.getElementsByTagName('input');
var labels = parent.getElementsByTagName('label');
var br = parent.getElementsByTagName('br');
while (inputs.length) {
inputs[0].parentNode.removeChild(inputs[0]);
labels[0].parentNode.removeChild(labels[0]);
br[0].parentNode.removeChild(br[0]);
}
}
function generate_overlay_selector() {
// clear up
remove_inputs(formGmean);
remove_inputs(formOverlay);
d3.select('#chart').html('');
var choice = raw_data.params;
// choose gmean axis
for (var i = 2; i < choice.length; i ++ ) {
var input = document.createElement('input');
var label = document.createElement('label');
input.type = 'checkbox';
label.textContent = input.value = choice[i];
label.htmlFor = input.id = 'gip-' + choice[i];
input.setAttribute('index', i - 2);
input.addEventListener('change', function(){updateLegendCheckBox(this.id);});
//var label = form.append('label').attr('class', 'param_label');
formGmean.appendChild(input);
formGmean.appendChild(label);
formGmean.appendChild(document.createElement('br'));
if (raw_data.params[0] == 'parsed_date' && choice[i] == 'run') {
$('#gip-'+choice[i]).prop('checked', true);
}
}
function updateLegendCheckBox(gid) {
var oid = gid.substring(4, gid.length);
if (d3.select('#'+gid).property('checked')) {
$('#oip-'+oid).prop('checked', false);
d3.select('#oip-'+oid).attr('disabled', 'on');
d3.select('#olabel-'+oid).attr('style', 'color: rgb(188,188,188)');
} else {
d3.select('#oip-'+oid).attr('disabled', null);
d3.select('#olabel-'+oid).attr('style', 'color: rgb(0,0,0)');
}
}
// choose overlay axis
for (var i = 2; i < choice.length; i ++ ) {
//var label = form.append('label').attr('class', 'param_label');
var input = document.createElement('input');
var label = document.createElement('label');
input.type = 'checkbox';
label.textContent = input.value = choice[i];
label.htmlFor = input.id = 'oip-' + choice[i];
input.setAttribute('index', i - 2);
label.id = 'olabel-'+choice[i];
//var label = form.append('label').attr('class', 'param_label');
formOverlay.appendChild(input);
formOverlay.appendChild(label);
formOverlay.appendChild(document.createElement('br'));
}
// make selection pannel visible
custom_panel.style.visibility = 'visible';
}
/*
* if x is originally a int or float, this function does nothing
* if x is originally a string, this function replaces the x value with index,
* and setup the xNameMap
*/
function indexXString () {
xNameMap = [];
for (var t = 0; t < raw_data.tasks.length; t ++) {
var xValue = new Set();
for (j in raw_data.data[t]) {
xValue.add(raw_data.data[t][j][0]);
}
// check if xValue contains NaN
var needConvert = 0;
for (v of xValue) {
if (isNaN(Number(v))) {
needConvert = 1;
break;
}
}
// do conversion
if (needConvert == 1) {
// setup xNameMap
var index = 0;
var xNM = {index:[], values:[]};
for (v of xValue) {
xNM.index.push(index);
xNM.values.push(v);
index += 1;
}
xNameMap.push(xNM);
} else {
xNameMap.push({index: [], values: []});
}
}
}
/*
* expensive in terms of having most distinct values
* the input rawData is NOT necessarily the same as the global var raw_data
*/
function findMostExpensiveAxis(rawData, params) {
var maxValue = 0;
var maxIndex = -1;
for (var i = 0; i < params.length - 2; i++) {
var temp = new Set();
for (var j = 0; j < rawData.length; j++) {
temp.add(rawData[j][i+2]);
}
var filt_name = [];
// convert set to list
for (v of temp) {
filt_name.push(v);
}
if (maxValue < filt_name.length) {
maxValue = filt_name.length;
maxIndex = i;
}
}
return maxIndex;
}
/*
* generate the plot with user specified legend
* this function is called after user is in the 'customer plot' mode
*/
function plot_generator() {
first_plot = true;
// clear up
d3.select('#chart').html('');
var series = raw_data.data;
// filter is an array of array: filter[i][*] stores all possible values of axis i
// NOTE: I am actually not using the 'filter' variable.
var filter = [];
for (var t = 0; t < raw_data.tasks.length; t++) {
for (var i = 0; i < raw_data.params.length - 2; i++) {
var temp = new Set();
for (var j = 0; j < raw_data.data[t].length; j++) {
temp.add(raw_data.data[t][j][i+2]);
}
var filt_name = [];
// convert set to list
for (v of temp) {
filt_name.push(v);
}
filter.push(filt_name);
}
}
// dealing with geo mean
gmean_data = [];
for (var i in series) {
gmean_data.push([]);
var gmeanPrep = data_transform(series[i], gmean_list, 'non-overlay');
for (var k in gmeanPrep) {
var groupedX = data_transform(gmeanPrep[k], [(0-2)+''], 'xy group');
seriesXY = [];
for (var l in groupedX) {
groupedX[l] = reduceToGmean(groupedX[l]);
var paramReduced = k.substring(2, k.length).split(' ');
for (var h in gmean_list) {
paramReduced.splice(gmean_list[h], 0, '');
}
seriesXY.push([l, groupedX[l]].concat(paramReduced));
gmean_data[i].push([l,groupedX[l]].concat(paramReduced));
}
findAxisScale(seriesXY, xNameMap[i], i);
}
}
series = gmean_data;
// dealing with overlay axes
// traverse all tasks
range = [];
manipulatedData = {};
for (var i = 0; i < series.length; i++) {
var grouped_series = data_transform(series[i], overlay_list, 'non-overlay');
range.push({x: [], y: []});
range[i]['x'] = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
range[i]['y'] = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
manipulatedData[raw_data.tasks[i]] = {};
for (var k in grouped_series) {
findAxisScale(grouped_series[k], xNameMap[i], i);
}
// add task name
var c = d3.select('#chart').append('div').attr('class', 'task_container').attr('id', 'task_'+i);
var t = c.append('h3').attr('class', 'task_title');
t.append('span').attr('class', 'h_grey').text('Task Name: ');
t.append('span').attr('class', 'h_dark').text(raw_data.tasks[i]);
var s = c.append('h4').attr('class', 'task_title');
s.append('span').attr('class', 'h_grey').text('Geo Mean Axes: ');
var temp = _.map(gmean_list, function(d){return raw_data.params[Number(d)+2];}).join();
if (temp == '') {
s.append('span').attr('class', 'h_dark').text('None');
} else {
s.append('span').attr('class', 'h_dark').text(temp);
}
var count = 0;
for (var k in grouped_series) {
count += 1;
}
if (count <= maxPlotsAllowed) {
for (var k in grouped_series) {
simple_plot(raw_data.params, grouped_series[k], overlay_list, xNameMap[i], i, 'normalTitle');
}
} else {
count = 0;
alert(errMsg['ENUMPLOTS']);
}
sizingTaskContainer(count, 'task_'+i);
}
// hide customization panel
//custom_panel.style.visibility = 'hidden';
}
function scroll_to(element) {
console.log("Scrolling to:");
console.log(element);
$('html, body').animate({
scrollTop: $(element).offset().top - 200
}, 1000);
}
function sizingTaskContainer (numPlots, id) {
var level = Math.ceil(numPlots / numPlotsPerRow);
// TODO: should get height of canvas directly
var wholeWidth = document.getElementById('display_canvas').offsetWidth / numPlotsPerRow;
var wholeHeight = wholeWidth * ratio.wholeHeiToWid;
// TODO: should not hard code 180
d3.select('#'+id).style('height', level*wholeHeight + 180);
}
/*
* to find the max and min of the x y axis
* used to unify the axis scale over all subplots
*/
function findAxisScale(series, xNM, t) {
for (var i = 0; i < series.length; i++) {
if (xNM.values.length == 0) {
range[t]['x'][0] = (range[t]['x'][0] > Number(series[i][0])) ? Number(series[i][0]) : range[t]['x'][0];
range[t]['x'][1] = (range[t]['x'][1] < Number(series[i][0])) ? Number(series[i][0]) : range[t]['x'][1];
}
if (Number(series[i][1]) != invalidY) {
range[t]['y'][0] = (range[t]['y'][0] > Number(series[i][1])) ? Number(series[i][1]) : range[t]['y'][0];
range[t]['y'][1] = (range[t]['y'][1] < Number(series[i][1])) ? Number(series[i][1]) : range[t]['y'][1];
}
}
}
/*
* return the data grouped by a certain axis (filter or x, y)
*/
function data_transform (series, overlay_list, mode) {
// group by
if (mode == 'non-overlay') {
return _.groupBy(series, function (dot_info) { axis_group = "";
for (var i = 2; i < dot_info.length; i++) {
if ($.inArray(i-2+"", overlay_list) == -1){
axis_group += ' '+dot_info[i];
}
}
return axis_group;
} );
} else if (mode == 'overlay') {
return _.groupBy(series, function (dot_info) { axis_group = "";
for (var i = 2; i < dot_info.length; i++) {
if ($.inArray(i-2+"", overlay_list) > -1){
axis_group += ' '+dot_info[i];
}
}
return axis_group;
} );
} else if (mode == 'xy group') {
return _.groupBy(series, function (dot_info) { axis_group = '';
for (var i = 0; i < 2; i++) {
if ($.inArray(i-2+'', overlay_list) > -1){
// don't add space when group by x or y axis
axis_group += dot_info[i];
}
}
return axis_group;
} );
} else {
alert('data_transform: unknown mode');
}
}
function epochTimeConverter(unixEpochTime, paramXY) {
// deal with unix epoch time
var date = unixEpochTime;
if (unixEpoch.indexOf(paramXY) != -1) {
var date = new Date(Number(date) * 1000);
}
return date;
}
/*
* setup data for plotter:
* 1. convert to data structure representing series
* 2. dealing with some corner case of range
*/
function prepareData(params, series, overlay_list, xNM, t, titleMani) {
//setup data
if (range[t]['y'][0] == Number.NEGATIVE_INFINITY) {
range[t]['y'][0] = 0;
range[t]['y'][1] = 0;
}
if (range[t]['x'][0] == range[t]['x'][1]) {
range[t]['x'][0] -= 0.5*range[t]['x'][0];
range[t]['x'][1] += 0.5*range[t]['x'][1];
}
if (range[t]['y'][0] == range[t]['y'][1]) {
range[t]['y'][0] -= 0.5*range[t]['y'][0];
range[t]['y'][1] += 0.5*range[t]['y'][1];
}
var lineData = data_transform(series, overlay_list, 'overlay');
console.log('///// lineData /////');
console.log(lineData);
// setup manipulatedData
for (var i in lineData) {
var val = i.split(' ');
var name = '';
for (var c = 1; c < val.length; c++) {
name += raw_data.params[Number(overlay_list[c-1]) + 2] + '..';
name += val[c] + '--';
}
manipulatedData[raw_data.tasks[t]][titleMani][name] = lineData[i];
}
for (var i in manipulatedData) {
for (var j in manipulatedData[i]) {
for (var k in manipulatedData[i][j]) {
for (var m in manipulatedData[i][j][k]) {
manipulatedData[i][j][k][m] = [manipulatedData[i][j][k][m][0],
manipulatedData[i][j][k][m][1]];
}
}
}
}
var lineInfo = [];
for (var k in lineData) {
var lineVal = [];
for (var j = 0; j < lineData[k].length; j ++ ){
// ignore invalid data points
if (lineData[k][j][1] == invalidY) {
continue;
}
if (isNaN(Number(lineData[k][j][0]))) {
lineVal.push({x: lineData[k][j][0], y: lineData[k][j][1]});
} else {
lineData[k][j][0] = Number(lineData[k][j][0]);
lineVal.push({x: lineData[k][j][0], y: lineData[k][j][1]});
}
}
if (xNM.values.length == 0) {
lineVal = _.sortBy(lineVal, 'x');
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
lineVal = _.map(lineVal, function(d) {return {x: epochTimeConverter(d.x, params[0]), y: epochTimeConverter(d.y, params[1])};});
} else {
// sort by the order in xNM.values
lineVal = _.map(lineVal, function(d) {return {x: xNM.values.indexOf(d.x), y: epochTimeConverter(d.y,params[1])};} );
lineVal = _.sortBy(lineVal, 'x');
lineVal = _.map(lineVal, function(d) {return {x: xNM.values[d.x], y: d.y};});
}
if (isNaN(Number(k))) {
lineInfo.push({values: lineVal, key: k});
} else {
// TODO: this Number() won't work if k is a combination of numerical and non-numerical
lineInfo.push({values: lineVal, key: Number(k)});
}
}
lineInfo = _.sortBy(lineInfo, 'key');
console.log('-=-=-=-=-= lineInfo =-=-=-=-=-=-');
console.log(lineInfo);
return lineInfo;
}
/*
* setup axis for simple_plot
* dealing with different axis types: numerical, non-numerical, time
* y axis is always numerical
*/
function setupAxis(params, xNM, t, width, height) {
// setup axis
var x = null;
if (xNM.values.length == 0) {
var xAxisRange = [range[t]['x'][0] - 0.1*(range[t]['x'][1]-range[t]['x'][0]), range[t]['x'][1] + 0.1*(range[t]['x'][1]-range[t]['x'][0])];
xAxisRange[0] = epochTimeConverter(xAxisRange[0], params[0]);
xAxisRange[1] = epochTimeConverter(xAxisRange[1], params[0]);
if (unixEpoch.indexOf(params[0]) == -1) {
x = d3.scale.linear()
.domain(xAxisRange)
.range([0, width]);
} else {
x = d3.time.scale().domain(xAxisRange)
.range([0, width]);
}
} else {
x = d3.scale.ordinal()
.domain(xNM.values)
.rangePoints([0, width], 0.3);
}
var y = null;
var yAxisRange = [range[t]['y'][0] - 0.2*(range[t]['y'][1]-range[t]['y'][0]), range[t]['y'][1] + 0.2*(range[t]['y'][1]-range[t]['y'][0])];
yAxisRange[0] = epochTimeConverter(yAxisRange[0], params[1]);
yAxisRange[1] = epochTimeConverter(yAxisRange[1], params[1]);
if (unixEpoch.indexOf(params[1]) == -1) {
y = d3.scale.linear()
.domain(yAxisRange)
.range([height, 0]);
} else {
y = d3.time.scale().domain(yAxisRange)
.range([height, 0]);
}
return [x, y];
}
/*
* generate the svg plot
* using d3.js
* t: task index
* element layout:
* task_container
* |
* `-- task_title
* |
* `-- chart_container
* |
* `-- svg
* | |
* | `-- g
* | |
* | `-- rect, axis, path
* |
* `--legend_container
* |
* `-- svg
* |
* `-- legend_title, legend
*
*/
function simple_plot(params, series, overlay_list, xNM, t, titleMode) {
var titleMani = '';
for (var i = 2; i < series[0].length; i ++){
if ($.inArray(i-2+'', overlay_list) == -1
&& String(series[0][i]).length != 0) {
titleMani += params[i];
titleMani += '..';
titleMani += series[0][i];
titleMani += '--';
}
}
console.log(raw_data.tasks[t]);
console.log(manipulatedData);
manipulatedData[raw_data.tasks[t]][titleMani] = {};
// ...............................
// setup geometry of plotting area
// all geometry values are derived by the width of the whole page
var wholeWidth = document.getElementById('display_canvas').offsetWidth / numPlotsPerRow;
var wholeHeight = wholeWidth * ratio.wholeHeiToWid;
// basically width: whole = canvas + legend + small margin
// height: whole = canvas = legend
var canvWidth = wholeWidth * ratio.canvToWholeWid;
var canvHeight = wholeHeight;
// width & height is the size of the actual plotting area (i.e.: rect in canvas svg)
var width = canvWidth - wholeWidth * (ratio.marginLefToWhole + ratio.marginRigToWhole);
var height = canvHeight - wholeHeight * (ratio.marginTopToWhole + ratio.marginBotToWhole);
// width and height for legend container
var legWidth = wholeWidth * ratio.legToWholeWid;
var legHeight = wholeHeight;
/*
console.log('---- whole:');
console.log('Width: '+wholeWidth+' Height: '+wholeHeight);
console.log('---- canv:');
console.log('Width: '+canvWidth+' Height: '+canvHeight);
console.log('---- plot');
console.log('Width: '+width+' Height: '+height);
*/
// ................................
// small tuning of plot title position
var plotTitleY = -10;
// const for legend, unit: px
var dataDotRadius = 4;
var legendSize = 14;
var legendMargin = 8;
var legendPeriGap = 10;
// .................................
// data
var lineInfo = prepareData(params, series, overlay_list, xNM, t, titleMani);
// .................................
// axis
var xy = setupAxis(params, xNM, t, width, height);
var x = xy[0];
var y = xy[1];
// 's' is for SI prefix in axis ticks
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(10, 's')
.tickSize(-height);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10, 's')
.tickSize(-width);
// ...................................
// behavior
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1, 10])
.on("zoom", zoomed);
// ...................................
// assembly
var chart = d3.select('#task_'+t).append('div').attr('class', 'chart_container');
var svg = chart.append('svg').attr('class', 'canvas_container')
.attr("width", canvWidth)
.attr("height", canvHeight)
.attr('shape-rendering', 'geometricPrecision');
var gTransX = wholeWidth * ratio.marginLefToWhole;
var gTransY = wholeHeight * ratio.marginTopToWhole;
// NB: the translation on g is not strict. By inspecting the actual generated webpage,
// it seems that the translation of (gTransX, gTransY) is eventually falling on rect.
svg = svg.append("g")
.attr('transform', 'translate(' + gTransX + ',' + gTransY + ')');
//.attr("transform", "translate(" + plotMargin['left'] + "," + plotMargin['top'] + ")")
// only support zooming for numerical x axis
if (xNM.values.length == 0) {
svg.call(zoom);
}
// svg -> g -> rect: background color
svg.append("rect")
.attr('fill', '#f0f0f0')
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// reassemble x and y axis name, with underscore eliminated
var xParam = _.reduce(params[0].split('_'), function(memo, num) {return memo == '' ? num : memo+' '+num;}, '');
var yParam = _.reduce(params[1].split('_'), function(memo, num) {return memo == '' ? num : memo+' '+num;}, '');
// the hardcoded 30 in "attr('y', height+30)" should be fine. cuz vertical px is irrelevant to window size
svg.append('text').attr('class', 'x label').attr('text-anchor', 'middle')
.attr('x', width/2).attr('y', height+30).style('font-size','14px')
.text(xParam);
// TODO: the hardcoded -50 in "attr('y', -50)" may need adjustment
svg.append('text').attr('class', 'y label').attr('text-anchor', 'middle')
.attr('x', -height/2).attr('y', -50).attr('dy', '.75em')
.attr('transform', 'rotate(-90)').style('font-size','14px')
.text(yParam);
//.on("click", reset);
// .............
// add line and dots
var lineGen = d3.svg.line()
.x(function(d) {return x(d['x']);})
.y(function(d) {return y(d['y']);});
var clip = svg.append('svg:clipPath').attr('id', 'clip')
.append('svg:rect').attr('fill', '#f0f0f0').attr('x', 0).attr('y', 0)
.attr('width', width).attr('height', height);
var color = d3.scale.category10();
for (var i in lineInfo){
var svgg = svg.append('g').attr('clip-path', 'url(#clip)');
svgg.append('svg:path').attr('fill', 'none')
.datum(lineInfo[i]['values'])
.attr('class', 'line').attr('d', lineGen)
.style('stroke', function() {return color(lineInfo[i]['key']);}); // here each key is associated with a color --> for future legend
svgg.selectAll('.dots')
.data(lineInfo[i]['values'])
.enter()
.append('circle')
.attr('class', 'dots')
.attr('r', dataDotRadius)
.attr('fill', function() {return color(lineInfo[i]['key']);})
.attr('transform', function(d) {return 'translate(' + x(d['x'])+ ',' + y(d['y']) + ')'; });
}
/*
// TODO: add display coordination function
var focus = svg.append('g').attr('class', 'focus').style('display', 'none');
focus.append('circle').attr('r', 4.5);
focus.append('x', 9).attr('dy', '.35em');
svg.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);
*/
// .............
// add legend
// the svg size should be chosen to fit the text
var svgLegWidth = 0;
var svgLegHeight = 0;
var divLegWidth = wholeWidth * ratio.legToWholeWid;
var divLegHeight = wholeHeight * (1 - ratio.marginBotToWhole);
var svgLegend = chart.append('div').attr('class', 'legend_container')
.style('width', divLegWidth).style('height', divLegHeight)
.append('svg').attr('class', 'legend_svg');
// NOTE: has to set font here, or getBBox will return inprecise value due to the unloaded font
d3.selectAll('svg').style('font', '10px sans-serif');
if (lineInfo.length > 1 || lineInfo[0]['key'] != '') {
var legend = svgLegend.selectAll('.legend')
.data(color.domain()) // this step is the key to legend
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function (d, i) {
var height = legendSize + legendMargin;
var horz = legendPeriGap;
var vert = (i+1) * height + wholeHeight * ratio.marginTopToWhole + legendPeriGap;
return 'translate(' + horz + ', ' + vert + ')';
});
var legendTitle = _.map(overlay_list, function(d) {return '<'+params[Number(d)+2]+'>';});
for (var i in legendTitle) {
legendTitle[i] = _.reduce(legendTitle[i].split('_'), function(memo, num){return memo == '' ? num : memo+' '+num;}, '');
}
var textElement = svgLegend.append('text').attr('x', legendPeriGap).attr('y', legendPeriGap + wholeHeight * ratio.marginTopToWhole)
.attr('text-anchor', 'left').style('font-size', '12px')
.style('font-weight', 'bold').text(String(legendTitle));
// dynamically adjust the svg legend size
svgLegWidth = (svgLegWidth > textElement.node().getBBox().width) ? svgLegWidth : textElement.node().getBBox().width;
svgLegHeight += textElement.node().getBBox().height;
legend.append('rect')
.attr('width', legendSize).attr('height', legendSize)
.attr('x', 0).attr('y', -5)
.style('fill', color).style('stroke', color);
legend.append('text')
.attr('x', legendSize + legendMargin).attr('y', legendSize - legendMargin)
.style('font-size', '12px')
.text(function(d) {return d;});
legend.each(function() {
svgLegWidth = (svgLegWidth > this.getBBox().width) ? svgLegWidth : this.getBBox().width;
svgLegHeight += legendSize+legendMargin; });
svgLegWidth += legendPeriGap;
svgLegHeight += legendPeriGap + wholeHeight * ratio.marginTopToWhole + legendSize;
svgLegend.attr('width', svgLegWidth).attr('height', svgLegHeight);
} else {
// no legend at all
svgLegend.attr('width', 0).attr('height', 0);
}
// .............
// add title
var cTitle = svg.append("text")
.attr("x", width/2)
.attr("y", plotTitleY)
.attr("text-anchor", "middle")
.attr("class", "chart-title")
.style("font-size", "16px");
for (var i = 2; i < series[0].length; i ++){
if ($.inArray(i-2+'', overlay_list) == -1
&& String(series[0][i]).length != 0) {
var paramReduced;
if (titleMode == 'normalTitle') {
paramRejoin = _.reduce(params[i].split('_'), function(memo, num) {return memo == '' ? num : memo+' '+num;}, '');
cTitle.append('svg:tspan')
.style('fill', 'rgb(111,111,111)')
.text(paramRejoin + ':');
}
cTitle.append('svg:tspan')
.style('font-weight', 'bold')
.text(' '+series[0][i]+' ');
}
}
// dealing with styling
// have to append this rect after the plots, in order for the peripheral stroke
// being placed at the very front
svg.append("rect")
.attr('class', 'canvas_peripheral')
.attr('fill', 'none')
.attr("width", width)
.attr("height", height);
d3.selectAll('rect.canvas_peripheral').style('stroke', '#000');
d3.selectAll('path').style('fill', 'none');
d3.selectAll('path.domain').style('stroke', '#fff');
d3.selectAll('.axis line').style('stroke', '#fff').style('fill', 'none');
d3.selectAll('.axis path').style('stroke', '#fff').style('fill', 'none');
d3.selectAll('.line').style('fill', 'none').style('stroke-width','1.5px');
// ..............
// interaction
function zoomed() {
if (xNM.values.length == 0) {
// update styling
d3.selectAll('.axis line').style('stroke', '#fff').style('fill','none');
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
svg.selectAll('.line').attr('class', 'line').attr('d', lineGen);
svg.selectAll('.dots').attr('transform', function(d) {
return 'translate(' + x(d['x']) + ',' + y(d['y']) + ')';});
} else {
//
}
}
/*
* have not implemented function of 'reset canvas' yet
*/
function reset() {
d3.transition().duration(750).tween("zoom", function() {
var ix = d3.interpolate(x.domain(), [-width / 2, width / 2]),
iy = d3.interpolate(y.domain(), [-height / 2, height / 2]);
return function(t) {
zoom.x(x.domain(ix(t))).y(y.domain(iy(t)));
zoomed();
};
});
}
if (first_plot) {
scroll_to(svg.node());
first_plot = false;
}
}
// ..............
// save image
function savePlot() {
var count = 0;
// var all is for compatibility of firefox. FF won't load img correctly unless you call onload function
// so I have to put zip.file() inside onload function
var all = 0;
d3.selectAll('.chart_container').each(function () {all += 1;});
var zip = new JSZip();
d3.selectAll('.chart_container').each(function () {
var html1 = d3.select(this).select('svg')
.attr('version', 1.1).attr('xmlns', 'http://www.w3.org/2000/svg')
.node().parentNode.innerHTML;
var html2 = d3.select(this).select('.legend_container').select('svg')
.attr('version', 1.1).attr('xmlns', 'http://www.w3.org/2000/svg')
.node().parentNode.innerHTML;
var html = html2 + html1;
var plotWidth = Number(d3.select(this).select('.canvas_container').attr('width'));
var plotHeight = Number(d3.select(this).select('.canvas_container').attr('height'));
var legendWidth = Number(d3.select(this).select('.legend_svg').attr('width'));
var legendHeight = Number(d3.select(this).select('.legend_svg').attr('height'));
var svgWidth = plotWidth + legendWidth;
var svgHeight = plotHeight > legendHeight ? plotHeight : legendHeight;
d3.select('#temp').append('svg').attr('width', svgWidth).attr('height', svgHeight)
.attr('version',1.1).attr('xmlns', 'http://www.w3.org/2000/svg').html(html);
d3.select('#temp').select('.legend_svg').attr('x', plotWidth);
var tempEle = document.getElementById('temp');
var tempSvg = tempEle.getElementsByTagName('svg')[0];
var rmDiv = tempSvg.getElementsByClassName('legend_container')[0];
tempSvg.removeChild(rmDiv);
html = d3.select('#temp').node().innerHTML;
var imgsrc = 'data:image/svg+xml;base64,'+btoa(html);
console.log('/// imgsrc ///');
console.log(imgsrc);
d3.select('#temp').append('canvas').attr('width', svgWidth).attr('height', svgHeight);
var canvas = document.querySelector('canvas');
var context = canvas.getContext('2d');
var image = new Image;
image.src = imgsrc;
image.onload = function() {
count += 1;
context.drawImage(image, 0, 0);
var canvasdata = canvas.toDataURL('image/png');
/*
// this is an alternative option for saving img
var a = document.createElement('a');
a.download = 'plot'+count+'.png';
a.href = canvasdata;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
*/
zip.file('png/plotPng-'+count+'.png', canvasdata.substr(canvasdata.indexOf(',')+1), {base64: true});
zip.file('svg/plotSvg-'+count+'.svg', btoa(html), {base64: true});
if (count == all) {
var content = zip.generate({type: 'blob'});
saveAs(content, 'plots.zip');
}
}
d3.select('#temp').html('');
});
}
function savePlotData() {
var zip = new JSZip();
var task, plot, line;
for (var i in manipulatedData) {
task = (i == '') ? 'task':i;
for (var j in manipulatedData[i]) {
plot = j;
if (plot[plot.length-1] == '-' && plot[plot.length-2] == '-') {
plot = plot.substring(0, plot.length-2);
}
plot = (plot == '') ? 'plot':plot;
for (var k in manipulatedData[i][j]) {
line = k;
if (line[line.length-1] == '-' && line[line.length-2] == '-') {
line = line.substring(0, line.length-2);
}
line = (line == '') ? 'line':line;
var arr = '';
for (var m in manipulatedData[i][j][k]) {
arr += String(manipulatedData[i][j][k][m][0]) + '\t';
arr += String(manipulatedData[i][j][k][m][1]) + '\n';
}
zip.file(task+'/'+plot+'/'+line, arr);
}
}
}
var content = zip.generate({type: 'blob'});
saveAs(content, 'plot_data.zip');
}