<!doctype html>
<html>
<head>
<script src="//cdn.anychart.com/js/7.4.0/anychart.min.js"></script>
<style>
html, body, #container {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="container"></div>
<script type="text/javascript">
anychart.onDocumentReady(function() {
// Contents of the sample:
// 1) Lines 123 to 345: Data description - all data for the entire dashboard is located here.
// 2) Lines 347 to 638: Utility functions - some utility functions to make routine operations easy.
// 3) Lines 640 to 1945: Functions, that form reports for the dashboard. Each function form its own report and
// returns an object consisting of a layer that contains the full report with title and contents positioned and a
// anychart.math.rect() describing report bounds.
// 4) Lines 1947 to 2070: A bunch of code that forms the report itself, using everything above.
// We set global fontFamily here to make it default
anychart.fontFamily = 'trebuchet, helvetica, arial, sans-serif';
// region Data description
// All the data imitates the manner as if we get it from a database.
// System Availability raw data. Columns are: ['System', 'Month', 'Availability']
var SARawData = [
['Network', Date.UTC(2013, 10), 98.6],
['Network', Date.UTC(2013, 11), 98.5],
['Network', Date.UTC(2014, 0), 98.5],
['Network', Date.UTC(2014, 1), 99.0],
['Network', Date.UTC(2014, 2), 99.2],
['Network', Date.UTC(2014, 3), 99.0],
['Network', Date.UTC(2014, 4), 99.3],
['Network', Date.UTC(2014, 5), 99.1],
['Network', Date.UTC(2014, 6), 99.0],
['Network', Date.UTC(2014, 7), 99.3],
['Network', Date.UTC(2014, 8), 99.5],
['Network', Date.UTC(2014, 9), 99.7],
['ERP', Date.UTC(2013, 10), 98.6],
['ERP', Date.UTC(2013, 11), 98.9],
['ERP', Date.UTC(2014, 0), 98.8],
['ERP', Date.UTC(2014, 1), 98.3],
['ERP', Date.UTC(2014, 2), 98.6],
['ERP', Date.UTC(2014, 3), 98.7],
['ERP', Date.UTC(2014, 4), 98.9],
['ERP', Date.UTC(2014, 5), 98.3],
['ERP', Date.UTC(2014, 6), 98.1],
['ERP', Date.UTC(2014, 7), 99.0],
['ERP', Date.UTC(2014, 8), 98.9],
['ERP', Date.UTC(2014, 9), 99.3],
['Data Warehouse', Date.UTC(2013, 10), 95.3],
['Data Warehouse', Date.UTC(2013, 11), 95.9],
['Data Warehouse', Date.UTC(2014, 0), 96.7],
['Data Warehouse', Date.UTC(2014, 1), 95.6],
['Data Warehouse', Date.UTC(2014, 2), 96.8],
['Data Warehouse', Date.UTC(2014, 3), 95.8],
['Data Warehouse', Date.UTC(2014, 4), 96.3],
['Data Warehouse', Date.UTC(2014, 5), 95.6],
['Data Warehouse', Date.UTC(2014, 6), 95.4],
['Data Warehouse', Date.UTC(2014, 7), 95.5],
['Data Warehouse', Date.UTC(2014, 8), 96.7],
['Data Warehouse', Date.UTC(2014, 9), 96.9],
['Web Site', Date.UTC(2013, 10), 97.9],
['Web Site', Date.UTC(2013, 11), 98.4],
['Web Site', Date.UTC(2014, 0), 98.5],
['Web Site', Date.UTC(2014, 1), 98.8],
['Web Site', Date.UTC(2014, 2), 99.0],
['Web Site', Date.UTC(2014, 3), 99.3],
['Web Site', Date.UTC(2014, 4), 99.2],
['Web Site', Date.UTC(2014, 5), 99.4],
['Web Site', Date.UTC(2014, 6), 99.4],
['Web Site', Date.UTC(2014, 7), 99.5],
['Web Site', Date.UTC(2014, 8), 99.6],
['Web Site', Date.UTC(2014, 9), 99.7],
['Email', Date.UTC(2013, 10), 99.0],
['Email', Date.UTC(2013, 11), 98.4],
['Email', Date.UTC(2014, 0), 99.1],
['Email', Date.UTC(2014, 1), 98.2],
['Email', Date.UTC(2014, 2), 98.2],
['Email', Date.UTC(2014, 3), 97.9],
['Email', Date.UTC(2014, 4), 98.6],
['Email', Date.UTC(2014, 5), 99.1],
['Email', Date.UTC(2014, 6), 98.4],
['Email', Date.UTC(2014, 7), 99.2],
['Email', Date.UTC(2014, 8), 99.2],
['Email', Date.UTC(2014, 9), 99.3],
['HR', Date.UTC(2013, 10), 97.0],
['HR', Date.UTC(2013, 11), 97.9],
['HR', Date.UTC(2014, 0), 98.2],
['HR', Date.UTC(2014, 1), 98.9],
['HR', Date.UTC(2014, 2), 98.2],
['HR', Date.UTC(2014, 3), 98.7],
['HR', Date.UTC(2014, 4), 98.4],
['HR', Date.UTC(2014, 5), 98.5],
['HR', Date.UTC(2014, 6), 98.6],
['HR', Date.UTC(2014, 7), 98.5],
['HR', Date.UTC(2014, 8), 98.7],
['HR', Date.UTC(2014, 9), 98.8],
['Problem Tracking', Date.UTC(2013, 10), 96.1],
['Problem Tracking', Date.UTC(2013, 11), 96.1],
['Problem Tracking', Date.UTC(2014, 0), 96.0],
['Problem Tracking', Date.UTC(2014, 1), 95.9],
['Problem Tracking', Date.UTC(2014, 2), 95.7],
['Problem Tracking', Date.UTC(2014, 3), 95.5],
['Problem Tracking', Date.UTC(2014, 4), 95.0],
['Problem Tracking', Date.UTC(2014, 5), 94.9],
['Problem Tracking', Date.UTC(2014, 6), 94.8],
['Problem Tracking', Date.UTC(2014, 7), 95.0],
['Problem Tracking', Date.UTC(2014, 8), 94.8],
['Problem Tracking', Date.UTC(2014, 9), 94.4]
];
// System Availability accepted values. Columns are: ['System', 'Accepted Availability']
var SAAcceptedAvailability = [
['Network', 99],
['ERP', 98],
['Data Warehouse', 98],
['Web Site', 98],
['Email', 98],
['HR', 96],
['Problem Tracking', 93]
];
// CPU Capacity % for today. Columns are: ['DateTime', 'Capacity']
var HCCPUData = [
[Date.UTC(2014, 9, 15, 0), 94.4],
[Date.UTC(2014, 9, 15, 1), 92.0],
[Date.UTC(2014, 9, 15, 2), 89.6],
[Date.UTC(2014, 9, 15, 3), 87.7],
[Date.UTC(2014, 9, 15, 4), 89.6],
[Date.UTC(2014, 9, 15, 5), 87.0],
[Date.UTC(2014, 9, 15, 6), 84.0],
[Date.UTC(2014, 9, 15, 7), 73.4],
[Date.UTC(2014, 9, 15, 8), 73.2],
[Date.UTC(2014, 9, 15, 9), 72.5],
[Date.UTC(2014, 9, 15, 10), 74.2],
[Date.UTC(2014, 9, 15, 11), 70.8],
[Date.UTC(2014, 9, 15, 12), 71.4],
[Date.UTC(2014, 9, 15, 13), 74.9],
[Date.UTC(2014, 9, 15, 14), 74.5],
[Date.UTC(2014, 9, 15, 15), 70.6],
[Date.UTC(2014, 9, 15, 16), 68.4],
[Date.UTC(2014, 9, 15, 17), 70.3],
[Date.UTC(2014, 9, 15, 18), 74.0],
[Date.UTC(2014, 9, 15, 19), 75.5],
[Date.UTC(2014, 9, 15, 20), 74.7],
[Date.UTC(2014, 9, 15, 21), 78.1],
[Date.UTC(2014, 9, 15, 22), 78.8],
[Date.UTC(2014, 9, 15, 23), 81.6]
];
// Storage Capacity % for last 12 month. Columns are: ['Month', 'Capacity']
var HCStorage = [
[Date.UTC(2013, 10), 61.2],
[Date.UTC(2013, 11), 64.1],
[Date.UTC(2014, 0), 65.8],
[Date.UTC(2014, 1), 67.5],
[Date.UTC(2014, 2), 69.0],
[Date.UTC(2014, 3), 70.3],
[Date.UTC(2014, 4), 71.6],
[Date.UTC(2014, 5), 71.4],
[Date.UTC(2014, 6), 73.0],
[Date.UTC(2014, 7), 73.2],
[Date.UTC(2014, 8), 73.8],
[Date.UTC(2014, 9), 74.6]
];
// Network Capacity % for last 12 month. Columns are: ['Month', 'Capacity']
var HCNetwork = [
[Date.UTC(2013, 10), 68.8],
[Date.UTC(2013, 11), 72.5],
[Date.UTC(2014, 0), 74.1],
[Date.UTC(2014, 1), 77.7],
[Date.UTC(2014, 2), 85.1],
[Date.UTC(2014, 3), 83.0],
[Date.UTC(2014, 4), 83.9],
[Date.UTC(2014, 5), 79.3],
[Date.UTC(2014, 6), 81.7],
[Date.UTC(2014, 7), 75.9],
[Date.UTC(2014, 8), 79.8],
[Date.UTC(2014, 9), 82.8]
];
// Values to form hardware capacity bullet ranges. They are supposed to be just known for each case.
// These values represent range borders:
// from 0 to 80 - good case,
// from 80 to 90 - excessive case,
// from 90 to 100 - critical case.
var HCCPURanges = [0, 80, 90, 100];
// from 0 to 60 - good case,
// from 60 to 80 - excessive case,
// from 80 to 100 - critical case.
var HCStorageRanges = [0, 60, 80, 100];
// from 0 to 60 - good case,
// from 60 to 80 - excessive case,
// from 80 to 100 - critical case.
var HCNetworkRanges = [0, 60, 80, 100];
// Daily Network Traffic for different periods of time. Columns are ['Hour', 'Average Traffic'].
// These data are supposed to be pre-calculated by a server or something else.
// DNT for last six month.
var DNT6MonthAvgData = [
[0, 171320],
[1, 140377],
[2, 119245],
[3, 58867],
[4, 46037],
[5, 15094],
[6, 25660],
[7, 135094],
[8, 188679],
[9, 186415],
[10, 166037],
[11, 160754],
[12, 135849],
[13, 166792],
[14, 175849],
[15, 175094],
[16, 144905],
[17, 166037],
[18, 129056],
[19, 66415],
[20, 54339],
[21, 35471],
[22, 39245],
[23, 160754]
];
// DNT for last week.
var DNTWeekAvgData = [
[0, 179622],
[1, 147924],
[2, 125283],
[3, 65660],
[4, 39245],
[5, 3773],
[6, 12075],
[7, 142641],
[8, 193962],
[9, 193962],
[10, 176603],
[11, 156226],
[12, 140377],
[13, 179622],
[14, 169056],
[15, 169811],
[16, 149433],
[17, 178867],
[18, 121509],
[19, 58113],
[20, 44528],
[21, 40754],
[22, 45283],
[23, 170566]
];
// DNT for yesterday.
var DNTYesterdayData = [
[0, 193207],
[1, 156226],
[2, 132075],
[3, 42264],
[4, 23396],
[5, 9056],
[6, 15849],
[7, 151698],
[8, 189433],
[9, 190188],
[10, 182641],
[11, 159245],
[12, 152452],
[13, 174339],
[14, 174339],
[15, 180377],
[16, 153962],
[17, 172830],
[18, 107924],
[19, 63396],
[20, 57358],
[21, 66415],
[22, 81509],
[23, 181886]
];
// Key Non-System Metrics report data:
// Summary expenses YTD. Columns are ['Month', 'Value'].
var KNSMExpensesData = [
[Date.UTC(2014, 0), 100000],
[Date.UTC(2014, 1), 97000],
[Date.UTC(2014, 2), 98000],
[Date.UTC(2014, 3), 98000],
[Date.UTC(2014, 4), 99000],
[Date.UTC(2014, 5), 100000],
[Date.UTC(2014, 6), 99000],
[Date.UTC(2014, 7), 98000],
[Date.UTC(2014, 8), 98000],
[Date.UTC(2014, 9), 97000]
];
// Customers satisfaction level YTD. Columns are ['Month', 'Satisfaction level'].
var KNSMSatisfactionData = [
[Date.UTC(2014, 0), 90],
[Date.UTC(2014, 1), 97],
[Date.UTC(2014, 2), 98],
[Date.UTC(2014, 3), 98],
[Date.UTC(2014, 4), 99],
[Date.UTC(2014, 5), 100],
[Date.UTC(2014, 6), 99],
[Date.UTC(2014, 7), 98],
[Date.UTC(2014, 8), 98],
[Date.UTC(2014, 9), 97]
];
// Level 1 Problems numbers YTD. Columns are ['Month', 'Number of Problems'].
var KNSMProblemsData = [
[Date.UTC(2014, 0), 45],
[Date.UTC(2014, 1), 97],
[Date.UTC(2014, 2), 95],
[Date.UTC(2014, 3), 87],
[Date.UTC(2014, 4), 99],
[Date.UTC(2014, 5), 78],
[Date.UTC(2014, 6), 99],
[Date.UTC(2014, 7), 86],
[Date.UTC(2014, 8), 98],
[Date.UTC(2014, 9), 97]
];
// Values to form KNSM report. They are supposed to be just known for each case.
// Target budget value. Reads like "target is to spend no more than 1175000 per year".
var KNSMBudgetTarget = 1175000;
// Target Level 1 Problems count. Reads like "target is to get no more than 100 level 1 problems per month".
var KNSMProblemsTarget = 100;
// These values represent range borders in percent of the target budget per month:
// from 0 to 90 - good case,
// from 90 to 110 - excessive case,
// from 110 to 150 - critical case.
var KNSMExpensesRanges = [0, 90, 110, 150];
// These values represent range borders in customers satisfaction level per month:
// from 150 to 100 - good case,
// from 100 to 80 - excessive case,
// from 80 to 0 - critical case.
var KNSMSatisfactionRanges = [150, 100, 80, 0];
// These values represent range borders in level 1 problems count per month:
// from 0 to 90 - good case,
// from 90 to 110 - excessive case,
// from 110 to 150 - critical case.
var KNSMProblemsRanges = [0, 90, 110, 150];
// Some Major Project Milestones for MPM report. Columns are ['Project', 'Milestone', 'Due date']
var MPMData = [
['ERP Upgrade', 'Full system test', Date.UTC(2014, 9, 24)],
['Add services data to DW', 'ETL coding', Date.UTC(2014, 9, 10)],
['Upgrade mainframe OS', 'Prepare plan', Date.UTC(2014, 9, 17)],
['Disaster recovery site', 'Install hardware', Date.UTC(2014, 9, 20)],
['Budgeting system', 'Hire team', Date.UTC(2014, 9, 2)],
['Web site face-lift', 'Move into production', Date.UTC(2014, 9, 22)]
];
// Some Projects for TPQ report. Columns are ['Project', 'Status', 'Funding approved', 'Schedule start']
var TPQData = [
['Professional service module', 'Pending available staff', true, Date.UTC(2014, 9, 22)],
['Upgrade MS Office', 'Cost-benefit analysis', false, Date.UTC(2014, 11, 1)],
['Failover for ERP', 'Preparing proposal', false, Date.UTC(2015, 0, 30)],
['Upgrade data warehouse HW', 'Evaluating options', true, Date.UTC(2015, 1, 13)],
['Executive dashboard', 'Vendor assessment', false, Date.UTC(2015, 4, 2)]
];
// Dashboard "today" date. It is set here like this to make the dashboard look "up to date" with these static data.
var Today = new Date(Date.UTC(2014, 9, 15));
// endregion
// region Utility functions
/**
* Utility function to setup property to a whole row. Samples of usage:
* 1) setupRowProp(table, 0, ['content', 'padding', 'left'], 10);
* Sets left padding of cell content in row 0 to 10 if there is a content, where left padding can be set in cells.
* So its equivalent to call cell.content().padding().left(10) for all cells in 0 row.
* 2) setupRowProp(table, 1, 'padding', [0, 1, 2, 3]);
* Sets padding of all cells in the 1 row to (0, 1, 2, 3).
* Its equivalent to call cell.padding(0, 1, 2, 3) for all cells in row 1.
* This two ways of usage can be combined, for example:
* setupRowProp(table, 0, ['content', 'padding'], [1, 2, 3, 4]);
* It is equivalent to calling cell.content().padding(1, 2, 3, 4) for all cells in row 0.
* This method fails if the property chain is incorrect (for example there is no such property you ask).
* @param {anychart.ui.table} table Table to setup row for.
* @param {number} rowIndex Row index.
* @param {!Array.<string>|string} propNameOrChain Property name to access, like 'border' or chain of property names
* to access, e.g. ['content', 'fontSize']. Note: no checking on valid results is done, so it's up to you to
* ensure property existence.
* @param {*|Array.<*>} propValueOrArray Value or array of values to set.
*/
function setupRowProp(table, rowIndex, propNameOrChain, propValueOrArray) {
// if passed row index is out of passed table - exit
if (rowIndex >= table.rowsCount()) return;
// normalize propNameOrChain to an array handle in one way
if (typeof propNameOrChain == 'string') propNameOrChain = [propNameOrChain];
// cache last chain index
var chainLastIndex = propNameOrChain.length - 1;
// for all column indexes in the table
for (var i = 0; i < table.colsCount(); i++) {
// get the cell
var prop = table.getCell(rowIndex, i);
// pass over the cell to the last but one property
for (var j = 0; j < chainLastIndex; j++) {
var name = propNameOrChain[j];
if (name in prop)
prop = prop[name]();
else
prop = null;
if (!prop) break;
}
var lastName = propNameOrChain[chainLastIndex];
// if property getter returns null, or there is no such property to call, we skip the cell
if (!prop || !(lastName in prop)) continue;
// a way to check if propValueOrArray is an array - if it is, we call apply to pass all array elements to a setter
if (propValueOrArray != null && typeof propValueOrArray != 'string' && typeof propValueOrArray.length == 'number')
prop[lastName].apply(prop, propValueOrArray);
// or just call the setter with one parameter
else
prop[lastName](propValueOrArray);
}
}
/**
* Utility function to setup property to a whole column. Samples of usage:
* 1) setupColProp(table, 0, ['content', 'padding', 'left'], 10);
* Sets left padding of cell content in column 0 to 10 if there is a content, where left padding can be set in cells.
* So its equivalent to call cell.content().padding().left(10) for all cells in column 0.
* 2) setupColProp(table, 1, 'padding', [0, 1, 2, 3]);
* Sets padding of all cells in the 1 row to (0, 1, 2, 3).
* Its equivalent to call cell.padding(0, 1, 2, 3) for all cells in column 1.
* This two ways of usage can be combined, for example:
* setupRowProp(table, 0, ['content', 'padding'], [1, 2, 3, 4]);
* It is equivalent to calling cell.content().padding(1, 2, 3, 4) for all cells in column 0.
* If the last parameter is set to true, than the first row (that can be a header row) will be skipped.
* This method fails if the property chain is incorrect (for example there is no such property you ask).
* @param {anychart.ui.table} table Table to setup row for.
* @param {number} colIndex Row index.
* @param {!Array.<string>|string} propNameOrChain Property name to access, like 'border' or chain of property names
* to access, e.g. ['content', 'fontSize']. Note: no checking on valid results is done, so it's up to you to
* ensure property existence.
* @param {*|Array.<*>} propValueOrArray Value or array of values to set.
* @param {boolean=} opt_skipFirstRow Set true to skip first row.
*/
function setupColProp(table, colIndex, propNameOrChain, propValueOrArray, opt_skipFirstRow) {
// if passed column index is out of passed table - exit
if (colIndex >= table.colsCount()) return;
// normalize propNameOrChain to an array handle in one way
if (typeof propNameOrChain == 'string') propNameOrChain = [propNameOrChain];
// cache last chain index
var chainLastIndex = propNameOrChain.length - 1;
// for all rows in the table (except first, if opt_skipFirstRow is true
for (var i = opt_skipFirstRow ? 1 : 0; i < table.rowsCount(); i++) {
// get the cell
var prop = table.getCell(i, colIndex);
// pass over the cell to the last but one property
for (var j = 0; j < chainLastIndex; j++) {
var name = propNameOrChain[j];
if (name in prop)
prop = prop[name]();
else
prop = null;
if (!prop) break;
}
var lastName = propNameOrChain[chainLastIndex];
// if property getter returns null, or there is no such property to call, we skip the cell
if (!prop || !(lastName in prop)) continue;
// a way to check if propValueOrArray is an array - if it is, we call apply to pass all array elements to a setter
if (propValueOrArray != null && typeof propValueOrArray != 'string' && typeof propValueOrArray.length == 'number')
prop[lastName].apply(prop, propValueOrArray);
// or just call the setter with one parameter
else
prop[lastName](propValueOrArray);
}
}
/**
* Utility function to calculate the sum of field values over a view.
* @param {anychart.data.View} view
* @param {string} fieldName
* @return {number}
*/
function calcSum(view, fieldName) {
var sum = 0;
var count = 0;
// we iterate over the view and sum up the field value
var iter = view.getIterator();
while (iter.advance()) {
count++;
sum += iter.get(fieldName);
}
return sum;
}
/**
* Utility function to calculate the average value of the field over a view.
* @param {anychart.data.View} view
* @param {string} fieldName
* @return {number}
*/
function calcAvg(view, fieldName) {
// we use calcSum() function to get a sum over the field and then divide by the number of rows in the view.
return calcSum(view, fieldName) / view.getIterator().getRowsCount();
}
/**
* Utility function to get the value of the field in last row in the view.
* @param {anychart.data.View} view
* @param {string} fieldName
* @return {*}
*/
function getLastFieldValue(view, fieldName) {
var iterator = view.getIterator();
if (iterator.select(iterator.getRowsCount() - 1))
return iterator.get(fieldName);
else
return undefined;
}
/**
* Creates a small tight date time scale (no gaps at all outside of the range).
* @return {anychart.scales.DateTime}
*/
function createTightDTScale() {
// removing gaps and ticks to make the line fill the horizontal space of the cell
return anychart.scales.dateTime()
.minimumGap(0)
.maximumGap(0)
.ticks([]);
}
/**
* Formats the number to the US currency format.
* @param {number} value
* @param {number=} opt_decimalDigits
* @return {string}
*/
function formatNumber(value, opt_decimalDigits) {
// Short way to add thousand separators to the number.
return value.toFixed(opt_decimalDigits || 2).replace(/\d(?=(\d{3})+\.)/g, '$&,');
}
/**
* Formats the date in manner we need it to be formatted in the sample.
* @param {Date} value
* @return {string}
*/
function formatDate(value) {
var m = value.getMonth() + 1;
if (m < 10) m = '0' + m;
var d = value.getDate();
if (d < 10) d = '0' + d;
var y = value.getFullYear() - 2000;
return m + '/' + d + '/' + y;
}
/**
* Returns difference between two dates in days. Result is positive, if date2 is later than date1.
* @param {Date} date1
* @param {Date} date2
* @return {number}
*/
function getDiffInDays(date1, date2) {
/**
* Returns if a year is a leap year.
* @param {number} year
* @return {boolean}
*/
function isLeapYear(year) {
// Leap year logic; the 4-100-400 rule
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
}
/**
* Returns the number of days in a month. Also needs year to handle leap years.
* @param {number} year
* @param {number} month
* @return {number}
*/
function getNumberOfDaysInMonth(year, month) {
switch (month) {
case 1:
return isLeapYear(year) ? 29 : 28;
case 3:
case 5:
case 8:
case 10:
return 30;
}
return 31;
}
/**
* Returns the number of days in a year.
* @param {number} year
* @return {number}
*/
function getNumberOfDaysInYear(year) {
return isLeapYear(year) ? 366 : 365;
}
// the sign of the difference
var sign = 1;
// we swap dates if the difference is "negative" to shorten the calculations and remember the sign
if (date1.getTime() > date2.getTime()) {
var tmp = date1;
date1 = date2;
date2 = tmp;
sign = -1;
}
// getting some info from the dates.
var y1 = date1.getUTCFullYear();
var y2 = date2.getUTCFullYear();
var m1 = date1.getUTCMonth();
var m2 = date2.getUTCMonth();
var d1 = date1.getUTCDate();
var d2 = date2.getUTCDate();
var result = 0, i;
// if the dates are from different years
if (y1 < y2) {
// than we should add all full years to the result
for (i = y1 + 1; i < y2; i++)
result += getNumberOfDaysInYear(i);
// all full months from the rest of the first year of the range
for (i = m1 + 1; i < 12; i++)
result += getNumberOfDaysInMonth(y1, i);
// and all full month from the beginning of the last year of the range
for (i = 0; i < m2; i++)
result += getNumberOfDaysInMonth(y2, i);
// and all days from the rest of the month of the first date
result += getNumberOfDaysInMonth(y1, m1) - d1;
// and all days from the beginning of the month of the second date
result += d2;
} else if (m1 < m2) {
// else, if the year is the same but month are different
// than we should add all full months in the range between the two dates
for (i = m1 + 1; i < m2; i++)
result += getNumberOfDaysInMonth(y1, i);
// and all days from the rest of the month of the first date
result += getNumberOfDaysInMonth(y1, m1) - d1;
// and all days from the beginning of the month of the second date
result += d2;
} else {
// else we can just substract one date from another and get the result
result += d2 - d1;
}
// and in the end we should take into account the sign of the difference.
return result * sign;
}
/**
* Creates filtering functions to filter out rows from a view that are not equal to the fieldValue.
* It is used in this sample to filter the SARawData by the system.
* @param {string} fieldValue
* @return {Function}
*/
function filterBySystem(fieldValue) {
return function(value) {
return fieldValue == value;
}
}
// endregion
// region Reports
/**
* Forms System Availability report using passed data.
* @param {Array.<Array>} rawData Raw system availability data. See SARawData variable for sample.
* @param {Array.<Array>} acceptedAvailabilities Data to form "acceptation ranges". Also responsible for systems order
* in the report. See SAAcceptedAvailability variable for sample.
* @return {{report: anychart.graphics.vector.Layer, bounds: anychart.math.Rect}} Report object.
*/
function formSAReport(rawData, acceptedAvailabilities) {
// we are not going to need different mapping on the raw data, so we map it at the same time
// we use 'System' and 'Availability' fields in our calculations and 'x' and 'value' fields are used by line charts.
var rawView = anychart.data.set(rawData).mapAs({'System': [0], 'Availability': [2], 'x': [1], 'value': [2]});
// we use common scales for charts in columns to make them comparable
var bulletScale = anychart.scales.linear();
// settings manual minimum and maximum to show the range we need.
bulletScale
.minimum(85)
.maximum(100);
// we use common scale for lines also
var lineScale = createTightDTScale();
// we create a markers factory to draw some markers and configure it here
var markers = anychart.ui.markersFactory();
markers
.anchor('center')
.position('center')
.type('circle')
.fill('#c00')
.stroke('2 #900')
.size(5);
// to use markers in a table we don't need call markers.draw() at all - we just need to add a marker obtained by
// markers.add(null) call to cell contents, the table will do the rest
// Preparing report data.
// We will build a table using table.contents() method, so we need to build table contents first
// Here we make a header row contents. There will be 5 columns in the table
var contents = [['Last 12 Month', null, 'System', 'Availability %', null]];
// Now we will form row contents for each system listed in acceptedAvailabilities
for (var i = 0; i < acceptedAvailabilities.length; i++) {
// system name stored in first column
var system = acceptedAvailabilities[i][0];
// accepted availability value - in second
var availability = acceptedAvailabilities[i][1];
// preparing data for the row
// we filter the main data set by the system name and get the view that contain rows only for the current system.
var systemData = rawView.filter('System', filterBySystem(system));
// and calculate average availability for that filtered view.
var avgAvailability = calcAvg(systemData, 'Availability');
// we will need one line chart per system
// we don't need any other chart elements besides the chart line, so we can use line series directly here.
var line = anychart.cartesian.series.line(systemData);
// we set come line properties to make it look better and also we set the common x scale
line
.stroke('2 #000')
.xScale(lineScale);
// we don't want a tooltip on these lines
line.tooltip().enabled(false);
// we do not call .draw() method here, because the table will do it for us
// if average availability is less than accepted for the system, we will place a marker in the row
var marker;
if (avgAvailability < availability)
marker = markers.add(null);
else
marker = null;
// we do not call .draw() method here and we won't call markers.draw(), because the table will do it for us
// we will also need one bullet chart per system to show current situation. We create it and setup.
var bullet = anychart.bullet([{'value': avgAvailability, 'type': 'line', 'gap': 0.4}]);
bullet
.scale(bulletScale)
.padding(0)
.margin(0);
bullet.range(0)
.from(availability)
.to(100)
.fill('#ccc');
bullet.title().enabled(false);
bullet.axis().enabled(false);
bullet.background()
.enabled(true)
.stroke('#ccc')
.fill('#fff');
// we do not call .draw() method here, because the table will do it for us
// and finally we form and push next row to table contents array.
contents.push([line, marker, system, bullet, avgAvailability.toFixed(1) + '%']);
}
// we wil also need an axis to show bullet scale, because they are usless without it
// so we create and set it up here
var axis = anychart.axes.linear();
axis
.scale(bulletScale)
.orientation('bottom')
.staggerMode(false)
.stroke('#ccc');
axis.ticks().stroke('#ccc');
axis.minorTicks().enabled(false);
axis.title().enabled(false);
axis.labels()
.fontSize('9px')
.textFormatter(function(value) {
return value['tickValue'] + '%';
});
// and form a row that will contain our axis
contents.push([null, null, null, axis, null]);
// now, as we have our table contents prepared, we can form a layer that will contain the report
// we create a layer
var container = anychart.graphics.layer();
// we create some variables to handle report bounds information
var titleHeight = 20;
var tableHeight = (contents.length - 1) * 24 + 20;
var reportBounds = anychart.math.rect(0, 0, 357, tableHeight + titleHeight);
// we create report title, set it up and draw it to the layer
var title = anychart.ui.title();
title
.container(container)
.parentBounds(reportBounds)
.fontWeight('normal')
.text('<span style="color:#ED8C2B; font-size: 15px;">System Availability</span> <span style="color: #666666; font-size: 10px; font-weight: normal;">(last 30 days)</span>')
.orientation('top')
.align('left')
.vAlign('bottom')
.margin(0)
.padding(0)
.height(titleHeight)
.useHtml(true)
.draw();
// we also create a legend for our bullet charts and place it into the title
var legendParentBounds = anychart.math.rect(reportBounds.left, reportBounds.top, reportBounds.width, titleHeight);
var legend = anychart.ui.legend();
// we use custom legend items and custom icon drawers here
legend.itemsProvider([
{
'index': 0,
'text': 'Actual',
'iconType': function(path, size) {
path.clear();
var x = Math.round(size / 2);
var y = Math.round(size / 2);
var height = size * 0.6;
path.clear()
.moveTo(x, y - height / 2)
.lineTo(x, y + height / 2)
.lineTo(x + 2, y + height / 2)
.lineTo(x + 2, y - height / 2)
.close();
},
'iconStroke': 'none',
'iconFill': '#000'
},
{
'index': 1,
'text': 'Acceptable',
'iconType': function(path, size) {
path.clear();
var x = Math.round(size / 2);
var y = Math.round(size / 2);
var height = size * 0.8;
path.clear()
.moveTo(x - 2, y - height / 2)
.lineTo(x - 2, y + height / 2)
.lineTo(x + 3, y + height / 2)
.lineTo(x + 3, y - height / 2)
.close();
},
'iconStroke': 'none',
'iconFill': '#ccc'
}
]);
legend
.fontSize('9px')
.fontFamily('trebuchet, helvetica, arial, sans-serif')
.itemsLayout('horizontal')
.iconTextSpacing(0)
.container(container)
.align('right')
.position('bottom')
.padding(0)
.margin(0)
.itemsSpacing(0)
.parentBounds(legendParentBounds);
legend.title().enabled(false);
legend.titleSeparator().enabled(false);
legend.paginator().enabled(false);
legend.background().enabled(false);
legend.draw();
// we finally create, setup and draw the table to the layer
var table = anychart.ui.table();
table
.container(container)
.top(title.getRemainingBounds().getTop())
.height(tableHeight)
.width(reportBounds.width)
.contents(contents)
.rowHeight(0, 20)
.colWidth(0, 93)
.colWidth(1, 20)
.colWidth(2, 106)
.colWidth(3, 95)
.colWidth(4, 43)
.cellBorder(null);
table.cellTextFactory()
.padding(0)
.vAlign('center')
.hAlign('left');
table.getCell(table.rowsCount() - 1, 3).padding(0, 2, 5);
table.getCell(0, 3)
.colSpan(2)
.content()
.hAlign('right');
// we use our utility method here to setup some properties for entire rows and columns
setupRowProp(table, 0, 'bottomBorder', '2 #ccc');
setupRowProp(table, 0, ['content', 'fontFamily'], 'verdana, helvetica, arial, sans-serif');
setupRowProp(table, 0, ['content', 'vAlign'], 'bottom');
setupRowProp(table, 0, ['padding', 'bottom'], 2);
setupColProp(table, 3, 'padding', [5, 2, 4], true);
setupColProp(table, 4, ['content', 'hAlign'], 'right', true);
// and draw the table
table.draw();
// we return an object with both report layer and desired bounds
return {
'report': container,
'bounds': reportBounds
};
}
/**
* Forms Hardware Capacity report using passed data.
* @param {Array.<Array>} CPUData Raw CPU Capacity data.
* @param {Array.<Array>} StorageData Raw Storage Capacity data.
* @param {Array.<Array>} NetworkData Raw Network Capacity data.
* @param {Array} CPURanges Data used to make "acceptation ranges" for CPU capacity levels.
* @param {Array} StorageRanges Data used to make "acceptation ranges" for Storage capacity levels.
* @param {Array} NetworkRanges Data used to make "acceptation ranges" for Storage capacity levels.
* @return {{report: anychart.graphics.vector.Layer, bounds: anychart.math.Rect}} Report object.
*/
function formHCReport(CPUData, StorageData, NetworkData, CPURanges, StorageRanges, NetworkRanges) {
// we declare some variables to collect content elements for the report table
var lines = [], bullets = [], averages = [];
// we use a common bullet scale like in the previous report to make them comparable
var bulletScale = anychart.scales.linear();
// we setup minimum and maximum because we know what range we want to show
bulletScale
.minimum(0)
.maximum(100);
// also we setup ticks count to make our tiny axis that will use this scale look better
bulletScale.ticks()
.count(3);
bulletScale.minorTicks()
.count(3);
// we make almost the same manipulation for all three metrics that we want to show in this report
// (CPU, storage, network), so we can make them in a "for", collecting results to arrays declared before
for (var i = 0; i < 3; i++) {
// we create a data set and map in a default way, because it suites us well in this case (we have an array of
// arrays in our incomming data, where in the first column there is 'x' and in the second column there is 'value')
var data = anychart.data.set(arguments[i]).mapAs();
// we calculate average value of capacity to show it in the table
var avg = calcAvg(data, 'value');
// we will need one line chart per hardware system
// we don't need any other chart elements besides the chart line, so we can use line series directly here.
var line = anychart.cartesian.series.line(data);
// we set come line properties to make it look better and also we set the common x scale
line
.stroke('2 #000')
.xScale(createTightDTScale());
// we don't want a tooltip on these lines
line
.tooltip()
.enabled(false);
// we do not call .draw() method here, because the table will do it for us
// we cache ranges array corresponding to current hardware system
var ranges = arguments[3 + i];
// we will also need one bullet chart per system
var bullet = anychart.bullet([{'value': avg, 'type': 'bar', gap: 0.6}]);
// we create some ranges using passed ranges array to show what levels of used capacity are good and what are not
bullet.range(0)
.from(ranges[0])
.to(ranges[1])
.fill('#ccc');
bullet.range(1)
.from(ranges[1])
.to(ranges[2])
.fill('#aaa');
bullet.range(2)
.from(ranges[2])
.to(ranges[3])
.fill('#666');
// we also setup some apparance settings of the bullet chart to make it look better
bullet
.scale(bulletScale)
.padding(0)
.margin(0);
bullet.title().enabled(false);
bullet.axis().enabled(false);
bullet.background().enabled(false);
// we do not call .draw() method here, because the table will do it for us
// we push elements we made to proper arrays
lines.push(line);
bullets.push(bullet);
averages.push(avg.toFixed() + '%');
}
// we will need an axis to show scale values for bullet charts, so we create it and set it up
var axis = anychart.axes.linear();
axis
.scale(bulletScale)
.orientation('bottom')
.stroke('#ccc');
axis.title().enabled(false);
axis.ticks().stroke('#ccc');
axis.minorTicks()
.enabled(true)
.stroke('#ccc');
axis.labels()
.fontSize('9px')
.textFormatter(function(value) {
return value['tickValue'] + '%';
});
// now we have all we need to prepare table contents array - the shortest way to create a table
var contents = [
['CPU', 'Today', lines[0], 'Overall', bullets[0], averages[0]],
['Storage', 'Last 12 Mo.', lines[1], 'Today', bullets[1], averages[1]],
['Network', 'Last 12 Mo.', lines[2], 'Today', bullets[2], averages[2]],
[null, null, null, null, axis, null]
];
// now we create a report layer and declare some info about report bounds
var container = anychart.graphics.layer();
var titleHeight = 20;
var tableHeight = (contents.length - 1) * 18 + 24;
var reportBounds = anychart.math.rect(0, 0, 357, tableHeight + titleHeight);
// we create, setup and draw report title to the layer
var title = anychart.ui.title();
title
.container(container)
.parentBounds(reportBounds)
.fontFamily('trebuchet, helvetica, arial, sans-serif')
.fontWeight('normal')
.fontSize('15px')
.fontColor('#ED8C2B')
.text('Hardware % of Capacity')
.orientation('top')
.align('left')
.vAlign('bottom')
.margin(0)
.padding(0, 0, 2, 0)
.height(titleHeight)
.useHtml(false)
.draw();
// then we create, setup and draw a custom legend for bullet charts to make them more useful
var legend = anychart.ui.legend();
// we setup legend items first using custom icon drawers
legend.itemsProvider([
{
'index': 0,
'text': 'Actual',
'iconType': function(path, size) {
var x = Math.round(size / 2);
var y = Math.round(size / 2);
var width = size * 0.6;
path
.clear()
.moveTo(x - width / 2, y - 2)
.lineTo(x + width / 2, y - 2)
.lineTo(x + width / 2, y + 1)
.lineTo(x - width / 2, y + 1)
.close();
},
'iconStroke': 'none',
'iconFill': '#000'
},
{
'index': 1,
'text': 'Good',
'iconType': function(path, size) {
var x = Math.round(size / 2);
var y = Math.round(size / 2);
var height = size * 0.8;
path
.clear()
.moveTo(x - 2, y - height / 2)
.lineTo(x - 2, y + height / 2)
.lineTo(x + 3, y + height / 2)
.lineTo(x + 3, y - height / 2)
.close();
},
'iconStroke': 'none',
'iconFill': '#ccc'
},
{
'index': 2,
'text': 'Excessive',
'iconType': function(path, size) {
var x = Math.round(size / 2);
var y = Math.round(size / 2);
var height = size * 0.8;
path
.clear()
.moveTo(x - 2, y - height / 2)
.lineTo(x - 2, y + height / 2)
.lineTo(x + 3, y + height / 2)
.lineTo(x + 3, y - height / 2)
.close();
},
'iconStroke': 'none',
'iconFill': '#aaa'
},
{
'index': 3,
'text': 'Critical',
'iconType': function(path, size) {
var x = Math.round(size / 2);
var y = Math.round(size / 2);
var height = size * 0.8;
path
.clear()
.moveTo(x - 2, y - height / 2)
.lineTo(x - 2, y + height / 2)
.lineTo(x + 3, y + height / 2)
.lineTo(x + 3, y - height / 2)
.close();
},
'iconStroke': 'none',
'iconFill': '#666'
}
]);
// then we setup some legend appearance setting to position it and make it look nice
legend
.fontSize('9px')
.fontFamily('trebuchet, helvetica, arial, sans-serif')
.itemsLayout('horizontal')
.iconTextSpacing(0)
.container(container)
.align('right')
.position('bottom')
.padding(0, 0, 2, 0)
.margin(0)
.itemsSpacing(0)
.parentBounds(anychart.math.rect(reportBounds.left, reportBounds.top, reportBounds.width, titleHeight));
legend.title().enabled(false);
legend.titleSeparator().enabled(false);
legend.paginator().enabled(false);
legend.background().enabled(false);
legend.draw();
// and finally we create, setup and draw the report body - the table
var table = anychart.ui.table();
table
.container(container)
.top(title.getRemainingBounds().top + 2)
.height(tableHeight)
.width(reportBounds.width)
.contents(contents)
.rowHeight(table.rowsCount() - 1, 24)
.colWidth(0, 60)
.colWidth(1, 60)
.colWidth(2, 77)
.colWidth(3, 50)
.colWidth(4, 80)
.colWidth(5, 30)
.cellBorder(null);
table.cellTextFactory()
.padding(0)
.vAlign('center')
.hAlign('left')
.fontSize('10px');
// we use our utility method to setup some properties to entier rows and columns
setupRowProp(table, 0, 'topBorder', '2 #ccc');
setupColProp(table, 0, ['content', 'fontSize'], '12px', false);
setupColProp(table, 2, 'padding', [3, 3], false);
setupColProp(table, 3, 'padding', [0, 0, 0, 8], false);
setupColProp(table, 4, 'padding', [5, 2, 4], false);
setupColProp(table, 5, ['content', 'hAlign'], 'right', false);
setupColProp(table, 5, ['content', 'fontSize'], '11px', false);
table.draw();
// we return an object with both report layer and desired bounds
return {
'report': container,
'bounds': reportBounds
};
}
/**
* Forms Daily Network Traffic report using passed data.
* @param {Array.<Array>} sixMonthsData Average daily traffic for last 6 months.
* @param {Array.<Array>} weekData Average daily traffic for last week.
* @param {Array.<Array>} yesterdayData Average daily traffic for last day.
* @return {{report: anychart.graphics.vector.Layer, bounds: anychart.math.Rect}} Report object.
*/
function formDNTReport(sixMonthsData, weekData, yesterdayData) {
// we create a report layer and declare some info about report bounds
var container = anychart.graphics.layer();
var titleHeight = 20;
var chartHeight = 140;
var reportBounds = anychart.math.rect(0, 0, 357, chartHeight + titleHeight);
// now we create, setup and draw report title
var title = anychart.ui.title();
title
.container(container)
.parentBounds(reportBounds)
.fontFamily('trebuchet, helvetica, arial, sans-serif')
.fontWeight('normal')
.text('<span style="color:#ED8C2B; font-size: 15px;">Daily Network Traffic</span> <span style="color: #666666; font-size: 10px; font-weight: normal;">(kilobytes)</span>')
.orientation('top')
.align('left')
.vAlign('bottom')
.margin(0)
.padding(0, 0, 2, 0)
.height(titleHeight)
.useHtml(true)
.draw();
// we cannot use table borders to draw this grey line, so we draw it using native graphics path
container.path()
.moveTo(0, titleHeight)
.lineTo(reportBounds.width, titleHeight)
.stroke('2 #ccc');
// Now we will create the main object of this report - the chart
// We want to do a rather complex thing here - we want the plotting area where series are drawn to have a top border.
// We can't use background here, because plotting area itself has no background. So we are forced to use top axis,
// but we don't want any ticks or labels on it, so we will turn off all that staff.
// Also we want to show 24 points with 25 labels on the bottom axis - to place each data point between two ticks.
// The only way to do it - to make different scales for data and for the bottom axis.
// Also we want custom label formats for the axis and only 3 lines of the grid (it means 5 ticks on our 25-ticks
// scale, or 6 hours tick interval). Let's see how we can do all that staff.
// let's create our custom scale for bottom axis and x-grid first:
// we make a date-time scale and set fixed minimum and maximum for it
var xAxisScale = anychart.scales.dateTime();
// it doesn't matter what date we set - only the time matters, so let's make it from 01/01/1900 to 01/02/1900
xAxisScale.minimum(Date.UTC(0, 0, 1, 0));
xAxisScale.maximum(Date.UTC(0, 0, 2, 0));
// we set major and minor tick intervals to 6 and 1 hours respectively
xAxisScale.ticks().interval(0, 0, 0, 6);
xAxisScale.minorTicks().interval(0, 0, 0, 1);
// now we create the chart and setup it's bounds.
var chart = anychart.cartesianChart();
chart
.container(container)
.left(0)
.top(titleHeight)
.width(reportBounds.width)
.height(chartHeight);
// first we will create series we need
// we will use palette to setup series colors:
chart.palette(['#aaa', '#666', '#000']);
var line1 = chart.line(sixMonthsData);
line1.name('Daily mean for last 6 month');
line1.markers().enabled(false);
line1.tooltip().enabled(false);
var line2 = chart.line(weekData);
line2.name('Daily mean for last 7 days');
line2.markers().enabled(false);
line2.tooltip().enabled(false);
var line3 = chart.line(yesterdayData);
line3.name('Yesterday');
line3.markers().enabled(false);
line3.tooltip().enabled(false);
// then we will setup the bottom axis:
// we create it, as it doesn't exist yet
var bottomAxis = chart.xAxis(0);
// we setup our custom scale for the axis and make some appearance improvements
bottomAxis
.scale(xAxisScale)
.staggerMode(false)
.overlapMode('allowOverlap')
.stroke('#ccc');
// we switch off axis title and all ticks, as we don't need them
bottomAxis.title().enabled(false);
bottomAxis.ticks().enabled(false);
bottomAxis.minorTicks().enabled(false);
// now we will setup minor axis labels - we want them to show just hours in 12-hours system and nothing more
bottomAxis.minorLabels()
.enabled(true)
.textFormatter(function(value) {
var date = new Date(value['tickValue']);
var h = date.getUTCHours() % 12;
return h || 12;
});
// now we will setup major axis labels - we know, that our custom scale has 5 ticks:
// 12AM, 6AM, 12PM, 6PM and 12AM again
// we want major labels to look like minor, but to show AM and PM strings below on first 12AM and 12PM ticks,
// so we do that using custom textFormatter:
bottomAxis.labels()
.enabled(true)
.textFormatter(function(value) {
var date = new Date(value['tickValue']);
var hour = date.getUTCHours();
var h = (hour % 12) || 12;
if (hour == 0 && date.getUTCDay() == 1)
return h + '\nAM';
else if (hour == 12)
return h + '\nPM';
else
return h;
});
// now we can setup a grid
// we use our custom scale in it to fit axis labels and grid lines
chart.grid()
.scale(xAxisScale)
.oddFill(null)
.evenFill(null)
.stroke('#ccc')
.layout('vertical');
// then we will setup our top "border" - the top axis
// we create it and turn off all it's elements except the axis line
var topAxis = chart.xAxis(1);
topAxis
.stroke('#ccc')
.orientation('top');
topAxis.ticks().enabled(false);
topAxis.minorTicks().enabled(false);
topAxis.title().enabled(false);
topAxis.labels().enabled(false);
topAxis.minorLabels().enabled(false);
// now we will setup Y scale and axis:
chart.yScale()
.maximumGap(0)
.minimumGap(0);
var leftAxis = chart.yAxis();
leftAxis.stroke('#ccc');
leftAxis.title().enabled(false);
leftAxis.ticks().stroke('#ccc');
leftAxis.minorTicks().enabled(false);
// we want custom formatting on Y axis labels, because it needs less space
leftAxis.labels().textFormatter(function(value) {
return (value['tickValue'] / 1000).toFixed(0) + 'K';
});
// the last thing we need to setup for this chart is the legend
var legend = chart.legend();
legend
.enabled(true)
.fontSize('9px')
.fontFamily('trebuchet, helvetica, arial, sans-serif')
.itemsLayout('horizontal')
.iconTextSpacing(3)
.itemsSpacing(4)
.align('right')
.position('top')
.padding(0, 0, 2, 0)
.margin(3, 0);
// we disable other legend elements, as we don't need them
legend.background().enabled(false);
legend.title().enabled(false);
legend.titleSeparator().enabled(false);
legend.paginator().enabled(false);
legend.tooltip().enabled(false);
// finally we draw the chart to the layer
chart.draw();
// we return an object with both report layer and desired bounds
return {
'report': container,
'bounds': reportBounds
};
}
/**
* Forms Key Non-System Metrics report using passed data.
* @param {Array.<Array>} expensesData Raw expenses data.
* @param {Array.<Array>} satisfactionData Raw customers satisfaction data.
* @param {Array.<Array>} problemsData Raw level 1 problems data.
* @param {Array} expensesRanges Data used to make "acceptation ranges" for company expenses levels.
* @param {Array} satisfactionRanges Data used to make "acceptation ranges" for customer satisfaction levels.
* @param {Array} problemsRanges Data used to make "acceptation ranges" for level 1 problems levels.
* @param {number} budget Year budget value.
* @param {number} problemsTarget Problems planned level.
* @return {{report: anychart.graphics.vector.Layer, bounds: anychart.math.Rect}} Report object.
*/
function formKNSMReport(expensesData, satisfactionData, problemsData, expensesRanges, satisfactionRanges,
problemsRanges, budget, problemsTarget) {
// as the first step we should map passed raw data arrays to be able to work with them
var views = [
anychart.data.set(expensesData).mapAs(),
anychart.data.set(satisfactionData).mapAs(),
anychart.data.set(problemsData).mapAs()
];
// then we prepare some data we want to show in the report using our utility methods we declared above
var actualExpenses = calcSum(views[0], 'value');
var actualSatisfaction = getLastFieldValue(views[1], 'value');
var actualProblems = calcAvg(views[2], 'value');
// we will use these values to determing metrics health
var actualValues = [
actualExpenses / (budget / expensesData.length * 12) * 100,
actualSatisfaction,
actualProblems
];
// these texts will be shown in the last column
var actualTexts = [
'$' + formatNumber(actualExpenses / 1000, 1) + 'K',
actualSatisfaction + '/100',
actualProblems.toFixed(0)
];
// these values will be used to setup range markers for line charts in the first column
var rangesForLines = [
[0, budget / 12],
[satisfactionRanges[0], satisfactionRanges[1]],
[problemsRanges[0] / 100 * problemsTarget, problemsRanges[2] / 100 * problemsTarget]
];
// these strings will be shown in the third column
var metrics = [
'Expenses YTD',
'Customer Satisfaction',
'Level 1 Problems'
];
// we use common scales for charts in columns to make them comparable
var bulletScale = anychart.scales.linear();
// we set manual minimum and maximum to show the range we need.
bulletScale
.minimum(0)
.maximum(150);
// we create a markers factory to draw some markers and configure it here
var markers = anychart.ui.markersFactory();
markers
.anchor('center')
.position('center')
.type('circle')
.fill('#c00')
.stroke('2 #900')
.size(5);
// preparing report table contents
var contents = [['Year-to-Date', null, 'Metric', '% of Target', 'Actual']];
// forming a row for each system
for (var i = 0; i < 3; i++) {
// preparing data for the row
var ranges = arguments[i + 3];
// we want to show a range marker for each metric, so we can't use just standalone series - we need charts
// so we create and empty cartesian chart
var chart = anychart.cartesianChart();
// setup X scale
chart.xScale(createTightDTScale());
// create and setup a line on it
var line = chart.line(views[i]);
line.stroke('2 #000');
line.markers().enabled(false);
line.tooltip().enabled(false);
// and create and setup a range marker on it (it has horizontal layout by default)
chart.rangeMarker(0)
.from(rangesForLines[i][0])
.to(rangesForLines[i][1]);
// and also we enable chart background to show chart borders
chart.background()
.enabled(true)
.stroke('#ccc')
.fill('none');
// now we deside whether to show a marker for the row or not using our data ranges
var marker;
if (actualValues[i] > Math.max(ranges[0], ranges[1]) || actualValues[i] < Math.min(ranges[0], ranges[1]))
marker = markers.add(null);
else
marker = null;
// we will also need one bullet chart per system
// so we crete it
var bullet = anychart.bullet([{'value': actualValues[i], 'type': 'bar', 'gap': 0.6}]);
// setup the scale and some appearance settings
bullet
.scale(bulletScale)
.padding(0)
.margin(0);
// create three ranges that we need to show and configure them
bullet.range(0)
.from(ranges[0])
.to(ranges[1])
.fill('#ccc');
bullet.range(1)
.from(ranges[1])
.to(ranges[2])
.fill('#aaa');
bullet.range(2)
.from(ranges[2])
.to(ranges[3])
.fill('#666');
// and disable chart elements we don't need
bullet.title().enabled(false);
bullet.axis().enabled(false);
bullet.background().enabled(false);
// and add the row we just made contents for to an array
contents.push([chart, marker, metrics[i], bullet, actualTexts[i]]);
}
// also we need an axis to show common bullet scale ticks so we create it and set it up here
var axis = anychart.axes.linear();
axis
.scale(bulletScale)
.orientation('bottom')
.staggerMode(false)
.overlapMode('allowOverlap')
.stroke('#ccc');
axis.title().enabled(false);
axis.ticks().stroke('#ccc');
axis.minorTicks().enabled(false);
axis.labels()
.fontSize('9px')
.textFormatter(function(value) {
return value['tickValue'] + '%';
});
// and add it to table contents array
contents.push([null, null, null, axis, null]);
// now we declare some variables to create report layer and store some info about report bounds
var container = anychart.graphics.layer();
var titleHeight = 20;
var tableHeight = (contents.length - 1) * 24 + 20;
var reportBounds = anychart.math.rect(0, 0, 380, tableHeight + titleHeight);
// we will need a title for the report, so we create, setup and draw it to the report layer
var title = anychart.ui.title();
title
.container(container)
.parentBounds(reportBounds)
.fontFamily('trebuchet, helvetica, arial, sans-serif')
.fontWeight('normal')
.fontSize('15px')
.fontColor('#ED8C2B')
.text('Key Non-System Metrics')
.orientation('top')
.align('left')
.vAlign('bottom')
.margin(0)
.padding(0, 0, 2, 0)
.height(titleHeight)
.useHtml(false)
.draw();
// also we will need a legend for bullet charts, so we create, setup and draw it to the report layer
// this legend has custom drawers for legend item icons, as you can see below
var legendParentBounds = anychart.math.rect(reportBounds.left, reportBounds.top, reportBounds.width, titleHeight);
var legend = anychart.ui.legend();
legend
.container(container)
.parentBounds(legendParentBounds)
.itemsLayout('horizontal')
.iconTextSpacing(0)
.itemsSpacing(0)
.align('right')
.position('bottom')
.padding(0, 0, 2, 0)
.margin(0)
.fontSize('9px')
.fontFamily('trebuchet, helvetica, arial, sans-serif')
.itemsProvider([
{
'index': 0,
'text': 'Actual',
'iconType': function(path, size) {
path.clear();
var x = Math.round(size / 2);
var y = Math.round(size / 2);
var width = size * 0.6;
path.clear()
.moveTo(x - width / 2, y - 2)
.lineTo(x + width / 2, y - 2)
.lineTo(x + width / 2, y + 1)
.lineTo(x - width / 2, y + 1)
.close();
},
'iconStroke': 'none',
'iconFill': '#000'
},
{
'index': 1,
'text': 'Good',
'iconType': function(path, size) {
path.clear();
var x = Math.round(size / 2);
var y = Math.round(size / 2);
var height = size * 0.8;
path.clear()
.moveTo(x - 2, y - height / 2)
.lineTo(x - 2, y + height / 2)
.lineTo(x + 3, y + height / 2)
.lineTo(x + 3, y - height / 2)
.close();
},
'iconStroke': 'none',
'iconFill': '#ccc'
},
{
'index': 2,
'text': 'Excessive',
'iconType': function(path, size) {
path.clear();
var x = Math.round(size / 2);
var y = Math.round(size / 2);
var height = size * 0.8;
path.clear()
.moveTo(x - 2, y - height / 2)
.lineTo(x - 2, y + height / 2)
.lineTo(x + 3, y + height / 2)
.lineTo(x + 3, y - height / 2)
.close();
},
'iconStroke': 'none',
'iconFill': '#aaa'
},
{
'index': 3,
'text': 'Critical',
'iconType': function(path, size) {
path.clear();
var x = Math.round(size / 2);
var y = Math.round(size / 2);
var height = size * 0.8;
path.clear()
.moveTo(x - 2, y - height / 2)
.lineTo(x - 2, y + height / 2)
.lineTo(x + 3, y + height / 2)
.lineTo(x + 3, y - height / 2)
.close();
},
'iconStroke': 'none',
'iconFill': '#666'
}
]);
legend.background().enabled(false);
legend.title().enabled(false);
legend.titleSeparator().enabled(false);
legend.paginator().enabled(false);
legend.draw();
// finally we can create the main report object - the table
var table = anychart.ui.table();
// we setup some nessessary properties of the table to tell it how and where to be drawn
table
.container(container)
.top(title.getRemainingBounds().getTop())
.height(tableHeight)
.width(reportBounds.width)
.contents(contents)
.rowHeight(0, 20)
.colWidth(0, 92)
.colWidth(1, 20)
.colWidth(2, 130)
.colWidth(3, 82)
.colWidth(4, 56)
.cellBorder(null);
table.cellTextFactory()
.padding(0)
.vAlign('center')
.hAlign('left');
table.getCell(table.rowsCount() - 1, 3).padding(0, 2, 5);
table.getCell(0, 4).content().hAlign('right');
// we use our utility methods to make setting up whole rows and columns easier
setupRowProp(table, 0, 'bottomBorder', '2 #ccc');
setupRowProp(table, 0, ['content', 'fontFamily'], 'verdana, helvetica, arial, sans-serif');
setupRowProp(table, 0, ['content', 'vAlign'], 'bottom');
setupRowProp(table, 0, ['padding', 'bottom'], 2);
setupColProp(table, 0, 'padding', [2, 0], true);
setupColProp(table, 3, 'padding', [6, 2, 5], true);
setupColProp(table, 4, ['content', 'hAlign'], 'right', true);
// and finally we draw the table to the report layer
table.draw();
// but there is one more thing we want to draw on this report - a thin vertical line over all bullet charts,
// showing the 100% mark
// to do that we should determine exact X position of the 100% mark, using the common bullet scale and axis position
// so we get the upper cell we want to draw the line at to determine both X and top Y coordinates
var cell = table.getCell(1, 3);
// determine its bounds
var bounds = cell.getBounds();
// take cell padding into consideration
var padding = cell.padding();
bounds.left += padding.left();
bounds.width -= padding.left() + padding.right();
// transform the value "100" using the scale of the axis
// the scale.transform() method returns a ratio of the value passed between minimum and maximum of the scale
// this ration is usually between 0 and 1, if the passed value is between minimum and maximum of the scale
// so to get pixel coords we need to use proportions:
var x = bulletScale.transform(100) * bounds.width + bounds.left;
// lets make an offset of two pixels from the top of the cell
var top = bounds.top + 2;
// to determine the bottom line position let's use a handy axis method getRemainingBounds, using which we can
// get axis line Y coordinate in this situation and axis major tick lengths to make the line we want to draw
// look good
var bottom = axis.getRemainingBounds().getBottom() + axis.ticks().length();
// now we have all coordinates we need to draw the line we want
container.path()
.stroke('#000')
.fill('none')
.moveTo(x, top)
.lineTo(x, bottom);
// we return an object with both report layer and desired bounds
return {
'report': container,
'bounds': reportBounds
};
}
/**
* Forms Major Project Milestones report using passed data.
* @param {Array.<Array>} data Raw Major Project Milestones data.
* @param {Date} today Today's date.
* @return {{report: anychart.graphics.vector.Layer, bounds: anychart.math.Rect}} Report object.
*/
function formMPMReport(data, today) {
// for this report we will need a common bullet scale with the range we want to show
var bulletScale = anychart.scales.linear();
bulletScale
.minimum(-20)
.maximum(20);
// and a markers factory to place some markers in rows we want to draw attention to
var markers = anychart.ui.markersFactory();
markers
.anchor('center')
.position('center')
.type('circle')
.fill('#c00')
.stroke('2 #900')
.size(5);
// now we will start forming report table contents
// we place headings to the first row of the table
var contents = [[null, 'Project', 'Milestone', 'Days Until/\nPast Due', 'Due\nDate']];
// than we aggregate passed data to a data set and map it to make iterating over it easier
var view = anychart.data.set(data).mapAs({'Project': [0], 'Milestone': [1], 'Due': [2]});
// now we get an iterator over the mapped data
var iterator = view.getIterator();
// and iterate it
while (iterator.advance()) {
// we determine milestone due using our iterator
var dueDate = new Date(iterator.get('Due'));
// and count the difference in days between today and the due date
var diff = getDiffInDays(today, dueDate);
// we will also need one bullet chart per system
// we fill bullet bars with different color depending if the milestone is past or before due
var bullet = anychart.bullet([{
'value': diff,
'type': 'bar',
'gap': 0.4,
'fill': ((diff >= 0) ? '#999' : '#000')
}]);
// we setup some bullet appearance properties and bullet scale here
bullet
.scale(bulletScale)
.padding(0)
.margin(0);
bullet.title().enabled(false);
bullet.axis().enabled(false);
bullet.background().enabled(false);
// now we determine if we need a marker in this row (if milestone due is more than 10 days before today)
var marker;
if (diff <= -10)
marker = markers.add(null);
else
marker = null;
// and finally add the formed row to contents array using some utility functions and iterator
contents.push([
marker,
iterator.get('Project'),
iterator.get('Milestone'),
bullet,
formatDate(dueDate)
]);
}
// to make bullets more clear and comparable we use common axis for them
// here we create and set it up
var axis = anychart.axes.linear();
axis
.scale(bulletScale)
.orientation('bottom')
.staggerMode(false)
.overlapMode('allowOverlap')
.stroke('#ccc');
axis.title().enabled(false);
axis.labels().fontSize('9px');
axis.ticks().stroke('#ccc');
axis.minorTicks().enabled(false);
// and add the axis to the table contents array
contents.push([null, null, null, axis, null]);
// now we are ready to create report layer and define some variables to store report bounds data
var container = anychart.graphics.layer();
var titleHeight = 20;
var tableTop = titleHeight - 20;
var tableHeight = (contents.length - 1) * 24 + 40;
var reportBounds = anychart.math.rect(0, 0, 380, tableHeight + tableTop);
// we create, setup and draw a title for the report
var title = anychart.ui.title();
title
.container(container)
.parentBounds(reportBounds)
.height(titleHeight)
.fontFamily('trebuchet, helvetica, arial, sans-serif')
.fontWeight('normal')
.text('<span style="color:#ED8C2B; font-size: 15px;">Major Project Milestones</span> <span style="color: #666666; font-size: 10px; font-weight: normal;">(by priority)</span>')
.orientation('top')
.align('left')
.vAlign('bottom')
.margin(0)
.padding(0)
.useHtml(true)
.draw();
// then we create the report main object - the table
var table = anychart.ui.table();
// we set it up and draw it to the report layer
table
.container(container)
.top(tableTop)
.height(tableHeight)
.width(reportBounds.width)
.contents(contents)
.rowHeight(0, 40)
.colWidth(0, 20)
.colWidth(1, 129)
.colWidth(2, 108)
.colWidth(3, 75)
.colWidth(4, 48)
.cellBorder(null);
table.cellTextFactory()
.padding(0)
.vAlign('center')
.hAlign('left')
.fontSize('11px');
// here we use our utility methods described above to make rows and columns setting up easier
setupRowProp(table, 0, 'bottomBorder', '2 #ccc');
setupRowProp(table, 0, ['content', 'vAlign'], 'bottom');
setupRowProp(table, 0, ['content', 'fontSize'], '11px');
setupRowProp(table, 0, ['content', 'fontFamily'], 'verdana, helvetica, arial, sans-serif');
setupRowProp(table, 0, ['padding', 'bottom'], 2);
setupColProp(table, 0, 'padding', [2, 0], true);
setupColProp(table, 3, 'padding', [6, 2, 5], true);
setupColProp(table, 4, ['content', 'hAlign'], 'right', true);
table.getCell(table.rowsCount() - 1, 3).padding(0, 2, 5, 1);
table.getCell(0, 3).content().hAlign('center');
table.getCell(0, 4).content().hAlign('right');
// and finally we can draw the table
table.draw();
// but there is one more thing we want to draw on this report - a thin vertical line over all bullet charts,
// showing the zero mark
// to do that we should determine exact X position of the 0 mark, using the common bullet scale and axis position
// so we get the upper cell we want to draw the line at to determine both X and top Y coordinates
var cell = table.getCell(1, 3);
// determine its bounds
var bounds = cell.getBounds();
// take cell padding into consideration
var padding = cell.padding();
bounds.left += padding.left();
bounds.width -= padding.left() + padding.right();
// transform the value "100" using the scale of the axis
// the scale.transform() method returns a ratio of the value passed between minimum and maximum of the scale
// this ration is usually between 0 and 1, if the passed value is between minimum and maximum of the scale
// so to get pixel coords we need to use proportions:
var x = bulletScale.transform(0) * bounds.width + bounds.left;
// lets make an offset of two pixels from the top of the cell
var top = bounds.top + 2;
// to determine the bottom line position let's use a handy axis method getRemainingBounds, using which we can
// get axis line Y coordinate in this situation and axis major tick lengths to make the line we want to draw
// look good
var bottom = axis.getRemainingBounds().getBottom() + axis.ticks().length();
// now we have all coordinates we need to draw the line we want
container.path()
.stroke('#ccc')
.fill('none')
.moveTo(x, top)
.lineTo(x, bottom);
// we return an object with both report layer and desired bounds
return {
'report': container,
'bounds': reportBounds
};
}
/**
* Forms Top Projects in Queue report using passed data.
* @param {Array.<Array>} data Raw Top Projects in Queue data.
* @return {{report: anychart.graphics.vector.Layer, bounds: anychart.math.Rect}} Report object.
*/
function formTPQReport(data) {
// this tiny report contains just of a table and a title, so all we need to prepare is a contents array for the table
// so we start from making an array with table column headings
var contents = [[null, 'Project', 'Status', 'Funding\nApproved', 'Sched.\nStart']];
// then we aggregate and map the data to make data iterating and retrieving easier
var view = anychart.data.set(data).mapAs({'Project': [0], 'Status': [1], 'Approved': [2], 'Start': [3]});
// then we get data iterator and use it to pass over the data and form the array with content
var iterator = view.getIterator();
for (var i = 0; iterator.advance(); i++) {
// we use iterator.get() method to retrieve data values
contents.push([
++i,
iterator.get('Project'),
iterator.get('Status'),
iterator.get('Approved') ? 'X' : null,
formatDate(new Date(iterator.get('Start')))
]);
}
// now we are ready to create content layer and declare some variables to store info about the report bounds
var container = anychart.graphics.layer();
var titleHeight = 20;
var tableTop = titleHeight - 20;
var tableHeight = (contents.length - 1) * 24 + 40;
var reportBounds = anychart.math.rect(0, 0, 380, tableHeight + tableTop);
// we create, setup and draw the report title
var title = anychart.ui.title();
title
.container(container)
.parentBounds(reportBounds)
.fontFamily('trebuchet, helvetica, arial, sans-serif')
.fontWeight('normal')
.fontSize('15px')
.fontColor('#ED8C2B')
.text('Top Projects in the Queue')
.orientation('top')
.align('left')
.vAlign('bottom')
.margin(0)
.padding(0, 0, 2, 0)
.height(titleHeight)
.useHtml(false)
.draw();
// and then we create, setup and draw the report table
var table = anychart.ui.table();
table
.container(container)
.top(tableTop)
.height(tableHeight)
.width(reportBounds.width)
.contents(contents)
.rowHeight(0, 40)
.colWidth(0, 20)
.colWidth(1, 150)
.colWidth(2, 110)
.colWidth(3, 52)
.colWidth(4, 48)
.cellBorder(null);
table.cellTextFactory()
.padding(0)
.vAlign('center')
.hAlign('left')
.fontSize('11px');
// we use our utility methods to setup common properties for the whole rows and columns
setupRowProp(table, 0, 'bottomBorder', '2 #ccc');
setupRowProp(table, 0, ['content', 'vAlign'], 'bottom');
setupRowProp(table, 0, ['padding', 'bottom'], 2);
setupColProp(table, 0, 'padding', [2, 0], true);
setupColProp(table, 3, ['content', 'hAlign'], 'center', true);
setupColProp(table, 4, ['content', 'hAlign'], 'right', true);
table.getCell(0, 3).content().hAlign('center');
table.getCell(0, 4).content().hAlign('center');
// and then we tell the table to draw to its container
table.draw();
// we return an object with both report layer and desired bounds
return {
'report': container,
'bounds': reportBounds
};
}
// endregion
// region Dashboard assemblage
// to draw the dashboard we need to create a stage first, so we do it, settings stage container id and its size
// stage is a global variable on this page.
stage = anychart.graphics.create('container', 772, 580);
// then we suspend stage redrawing because we know that now we are going to draw a lot on this stage and do want it
// to render all its content only in the end, when everything will be ready
stage.suspend();
// the height of the all dashboard titles
// we use three titles in this dashboard, because they all are positioned differently and contain different info
var titleHeight = 50;
// also we want one of the titles to contain "today's" date, so we format it properly here
var formattedToday = (new Date(Today)).toLocaleDateString('en-US', {year: 'numeric', month: 'long', day: 'numeric'});
// the first title is a disclaimer and is positioned at the top of everything, aligning to the left, with some padding
anychart.ui.title()
.container(stage)
.parentBounds(anychart.math.rect(0, 0, stage.width(), stage.height()))
.fontFamily('trebuchet, helvetica, arial, sans-serif')
.fontWeight('normal')
.fontSize('10px')
.fontColor('#666')
.text('This sample is based on the dashboard sample in "Information Dashboard Design: Displaying Data for At-a-Glance Monitoring" by Stephen Few')
.orientation('top')
.align('left')
.vAlign('bottom')
.margin(0)
.padding(3, 0, 0, 10)
.useHtml(true)
.draw();
// the second title is a "CIO Dashboard" label, also aligned to the left.
// but we also use vAlign and height properties combined to make that positioning you see
anychart.ui.title()
.container(stage)
.parentBounds(anychart.math.rect(0, 0, stage.width(), stage.height()))
.fontFamily('trebuchet, helvetica, arial, sans-serif')
.fontWeight('normal')
.fontSize('20px')
.fontColor('#ED8C2B')
.text('CIO Dashboard')
.orientation('top')
.align('left')
.vAlign('bottom')
.margin(0)
.padding(0, 10, 4, 10)
.height(titleHeight)
.useHtml(true)
.draw();
// the third title is a today's date label aligned to the right.
// it also has vAlign and height properties set in combination to make that positioning you see
anychart.ui.title()
.container(stage)
.parentBounds(anychart.math.rect(0, 0, stage.width(), stage.height()))
.fontFamily('trebuchet, helvetica, arial, sans-serif')
.fontWeight('normal')
.fontSize('12px')
.fontColor('#666')
.text('(As of ' + formattedToday + ')')
.orientation('top')
.align('right')
.vAlign('bottom')
.margin(0)
.padding(0, 10, 4, 10)
.height(titleHeight)
.useHtml(true)
.draw();
// then we draw the thick grey line, separating the titles from the content
stage.path()
.moveTo(10, titleHeight)
.lineTo(stage.width() - 10, titleHeight)
.stroke('4 #ccc');
// then we create to arrays to handle report objects for left and right columns of the dashboard
var leftColumnReports = [
formSAReport(SARawData, SAAcceptedAvailability),
formHCReport(HCCPUData, HCStorage, HCNetwork, HCCPURanges, HCNetworkRanges, HCStorageRanges),
formDNTReport(DNT6MonthAvgData, DNTWeekAvgData, DNTYesterdayData)
];
var rightColumnReports = [
formKNSMReport(KNSMExpensesData, KNSMSatisfactionData, KNSMProblemsData, KNSMExpensesRanges, KNSMSatisfactionRanges,
KNSMProblemsRanges, KNSMBudgetTarget, KNSMProblemsTarget),
formMPMReport(MPMData, Today),
formTPQReport(TPQData)
];
// and now we are ready to start to position the reports from the top with some margins
// down to the bottom, left column first
var top = titleHeight + 15;
var left = 10;
var width = 0;
var layer, bounds, i;
// for all left-column reports
for (i = 0; i < leftColumnReports.length; i++) {
layer = leftColumnReports[i]['report'];
bounds = leftColumnReports[i]['bounds'];
// we add the report layer to the stage
stage.addChild(layer);
// and translate it to spread report among the stage
layer.translate(left, top);
top += bounds.height + 5;
width = Math.max(width, bounds.width);
}
top = titleHeight + 15;
left += width + 15;
width = 0;
// for all left-column reports
for (i = 0; i < rightColumnReports.length; i++) {
layer = rightColumnReports[i]['report'];
bounds = rightColumnReports[i]['bounds'];
// we add the report layer to the stage
stage.addChild(layer);
// and translate it to spread report among the stage
layer.translate(left, top);
top += bounds.height;
width = Math.max(width, bounds.width);
}
// and finally we resume stage redrawing to make it render all the staff we placed on it
stage.resume();
// endregion
});
</script>
</body>
</html>