I was creating a script to pull data from a SharePoint list and display its contents in graphical form on another page. My original script was very messy and I had asked advice on how I should increase readability. Taking some inspiration from Steve Kwan's GitHub I tried to refactor to the script below. It's about twice as long as the previous one but I believe much more readable.
I would like some advice on where/how I can improve this script. Primarily in terms of style, but any obvious efficiencies would be appreciated as well.
I do have to make the script compatible with Explorer, so new(er) functionality such as arrow expressions (arr.map(a => a*2)
) or spread syntax ([obj, ...arr2]
isn't usable.
// Namespace for the current initiative
var printingStatistics = printingStatistics || {};
// Module for showing the current page statistics
printingStatistics.graphPages = function () {
var oData = null; // Data array sent to the Google Chart
var oElem = null;
// initializing method. Inputs are Data and a document.querySelector() input.
var init = function (data, location) {
if (!data)
printingStatistics.requireData('data')
if (!location)
printingStatistics.requireData('location')
// Create the parent element that the chart(s) will be inserted into the page.
oElem = document.createElement("div");
oElem.id = "container";
oElem = document.querySelector(location).appendChild(oElem);
dataManip(data);
}
// data aggregator for each object name.
var dataManip = function (data) {
oData = {};
// Itterate through the data and put it in a more useable format.
for (var i = 0; i < data.length; i++) {
// Should be either Current Quarter or Previous Quarter
var qStatus = data[i][1];
// Take the name and remove sub categorization (-zvl) and
// replace an observed discontinuity (Counsel) in names.
var qStatusGroup = data[i][0].replace(/\s*\-\s*ZVL/i, "").replace(/Council/g, "Counsel");
// Will be the n-th week within a Quarter. Used to combine entries from the same week.
var weekNumbers;
// variable shortener for temporary use.
var tmp = null;
// 2 long array holding [number of pages, week Number]
var pageWeekTuple = null;
if (["Current Quarter", "Previous Quarter"].indexOf(qStatus) === -1)
continue;
// Create backbone of datastructure from the initialized attributes
if (!oData[qStatus])
oData[qStatus] = {};
if (!oData[qStatus][qStatusGroup])
oData[qStatus][qStatusGroup] = [];
tmp = oData[qStatus][qStatusGroup];
if (qStatus === "Current Quarter") {
// Take the date value and find week number the date was in.
// week = (ISO week of the year) - ( (Quarter #) -1 ) * 13
var eDate = new Date(data[i][2]);
var week = eDate.getWeek() - (eDate.getWeekQuarter() - 1) * 13;
// Get a duplicate week, if it exists
pageWeekTuple = tmp[tmp.map(function (o) {
return o[0] === week
}).indexOf(true)]
// Was there a duplicate? If not, create a new array.
if (!pageWeekTuple) {
tmp.push([0, 0])
pageWeekTuple = tmp[tmp.length - 1];
}
// week = (ISO week of the year) - ( (Quarter #) -1 ) * 13
pageWeekTuple[0] = eDate.getWeek() - (eDate.getWeekQuarter() - 1) * 13;
}
else if (qStatus === "Previous Quarter") {
// Only interested in one array here. There shouldn't be any duplicates.
if (!tmp[tmp.length - 1])
tmp.push([0, 0])
pageWeekTuple = tmp[tmp.length - 1]
}
// Sum all the weeks page numbers together
pageWeekTuple[1] += data[i][3];
}
google.charts.setOnLoadCallback(defineCharts);
}
// Creates the columns and rows for the Google Chart
var defineCharts = function () {
var gDataTable = new google.visualization.DataTable();
var cols = [];
// Create the rows for each label in the Google Chart
for (var propert in oData["Current Quarter"]) {
// If the label isn't in both Previous Quarter or Current Quarter, skip it.
if (!oData["Previous Quarter"].hasOwnProperty(propert)) continue;
if (!oData["Current Quarter"].hasOwnProperty(propert)) continue;
// [Label, col1, col2, ...]
var row = [];
row.push(propert);
// For each item for the current label, compare its value to the previous quarter's total
// Push that ratio off as a percentage (0-100) and push a placeholder value in for the tooltip.
oData["Current Quarter"][propert].forEach(function (obj) {
var lastQuarter;
if (oData["Previous Quarter"][propert])
lastQuarter = oData["Previous Quarter"][propert][0][1]
var ratio = Math.round(obj[1] / lastQuarter * 2000) / 20
row.push(ratio);
row.push(0);
});
// Get the cumulative percentage and create the tooltip HTML string.
var total = parseInt(20 * row.slice(1).sum()) / 20
for (var i = 2; i < row.length; i += 2) {
row[i] = createHTMLObject(propert, total, row[i - 1], parseInt(i / 2)).outerHTML;
}
// Finish by pushing the row to a collection array.
cols.push(row);
}
// Filter each column by type === number and sort them in increasing order.
cols.sort(function (a, b) {
var f = function (v) {
return typeof v === 'number'
}
var first = a.filter(f).sum()
var second = b.filter(f).sum()
return first - second
});
// Find the largest length sub-array in the 2d array.
var highest = 0;
highest = Math.max.apply(
Math,
cols.map(function (o) {
return o.length
})
);
// Add the respective columns.
// Every even column >0 is a tooltip
// Every odd column is a week's ratio value
gDataTable.addColumn('string', 'SBU');
for (var i = 1; i < highest; i++) {
if (i % 2)
gDataTable.addColumn('number', "Week " + (i).toString());
else
gDataTable.addColumn({
'type': 'string',
'role': 'tooltip',
'id': 'Quarter (%):',
'p': {
'html': true
}
});
}
// add the rows to the table, now that the columns are sorted
// Draw the table with the specified title.
gDataTable.addRows(cols)
drawChart('Percentage of pages printed, relative to last quarter (by SBU)', gDataTable)
}
// Draws the Google Charts chart with pre-defined options
// Inputs are the title of the Chart (chartTitle)
// the data to be displayed.
var drawChart = function (chartTitle, dTable) {
var options = {
title: chartTitle,
legend: 'none',
height: 700,
isStacked: true,
'tooltip': {
'isHtml': true
},
colors: ['#137C14', '#257F15', '#378216', '#498517', '#5B8818', '#6D8B1A', '#7F8E1B', '#92911C', '#A4941D', '#B6971F', '#C89A20', '#DA9D21', '#ECA022', '#FFA324']
};
// Instantiate and draw the chart.
var elem = createContainer(chartTitle.replace(/[^a-zA-Z]/g, ""));
var chart = new google.visualization.ColumnChart(elem);
chart.draw(dTable, options);
}
// Creates containers as needed, with the id being a string who's
// non-alpha characters are stripped
var createContainer = function (id) {
var newElem = document.createElement("div");
newElem.id = id.replace(/[^a-zA-Z]+/);
oElem.appendChild(newElem);
return newElem;
}
// Creates the tooltips for each weeks data.
// Creates the element structured as follows:
// <returnElem><titleElem/><totalElem/><weekElem/></returnElem>
var createHTMLObject = function (Title, totalPercent, weekPercent, weekNum) {
var returnElem = document.createElement('div');
var titleElem = document.createElement('div');
titleElem.textContent = Title
titleElem.style.fontWeight = 'bolder'
var totalElem = document.createElement('div');
totalElem.textContent = 'Quarter (%): ' + totalPercent.toString() + '%'
var weekElem = document.createElement('div');
weekElem.textContent = 'Week ' + weekNum + ' (%): ' + weekPercent.toString() + '%'
returnElem.appendChild(titleElem)
returnElem.appendChild(totalElem)
returnElem.appendChild(weekElem)
return returnElem;
}
// Getter for the Data created.
var getData = function () {
return oData;
}
// Makes variables/methods public.
var oPublic = {
init: init,
getData: getData
}
return oPublic;
}();
// Module for collecting user-specified data.
printingStatistics.executeQueries = function () {
var oListTitle = null; // To prepare the ClientContext
var oViewTitle = null; // To prepare the ClientContext
var oView; // To prepare the ClientContext
var oProperties = null; // Push the specified properties into oDictStats
var oItems = null; // Used to initialize the enumerator after async request
var oClientContext = null; // used to hold ClientContext for later use
var oDictStats = null; // Output variable
// Define intenral variables
var init = function (listName, properties, viewName) {
if (!listName)
printingStatistics.requireData('listName')
oListTitle = listName;
if (viewName)
oViewTitle = viewName;
if (properties)
oProperties = properties
}
// Run the queries after initializing.
// Ensures sp.js is loaded and that sp.clientContext is available.
var execute = function () {
SP.SOD.executeFunc('sp.js', 'SP.ClientContext', queryInit);
}
// First query.
// If there's a list view defined (oViewTitle) then get the CAML query from the view
// Otherwise get all items from list. This can be long if many items in list.
var queryInit = function () {
var oList;
oClientContext = new SP.ClientContext.get_current();
oList = oClientContext.get_web().get_lists().getByTitle(oListTitle);
if (oViewTitle) {
oView = oList.get_views().getByTitle(oViewTitle);
oClientContext.load(oView);
oClientContext.executeQueryAsync(
queryGetListView,
queryFailed
);
}
else {
oItems = oList.getItems();
oClientContext.load(oItems);
oClientContext.executeQueryAsync(
queryGetListItems,
queryFailed
);
}
}
// On failed queries, notify the user.
var queryFailed = function (sender, args) {
alert('Request failed. ' + args.get_message() + '\n' +
args.get_stackTrace() + '\n' +
'Please contact an administrator');
}
// Get the CAML view from the list, then get all items from said view.
var queryGetListView = function (sender, args) {
oClientContext = new SP.ClientContext.get_current();
oList = oClientContext.get_web().get_lists().getByTitle(oListTitle);
var query = new SP.CamlQuery();
query.set_viewXml(oView.get_viewQuery());
oItems = oList.getItems(query);
oClientContext.load(oItems);
oClientContext.executeQueryAsync(
queryGetListItems,
queryFailed
);
}
// enumerates through the items and pulls the requeisted data from objects.
var queryGetListItems = function (sender, args) {
var listItemEnumerator = oItems.getEnumerator();
var oListItem;
oDictStats = [];
while (listItemEnumerator.moveNext()) {
// Get all defined fields
oListItem = listItemEnumerator.get_current().get_fieldValues();
var quarter = oListItem.PreviousOrCurrentQuarter;
var itemTitle = oListItem.Title.replace(/\s*\-\s*ZvL/i, "");
var itemStats = [];
// if properties were defined by requester, pull the data
// Otherwise pull the whole object.
if (oProperties) {
for (var i = 0; i < oProperties.length; i++) {
itemStats.push((oListItem[oProperties[i]]));
}
}
else {
itemStats.push(oListItem);
}
oDictStats.push(itemStats);
}
}
// Getter for the stats.
var getStats = function () {
return oDictStats;
}
// Makes variables and methods available to public.
var oPublic = {
init: init,
execute: execute,
getStats: getStats
}
return oPublic;
}();
// For throwing errors when there's missing data
printingStatistics.requireData = function (paramName) {
throw "Missing parameter: " + paramName.toString();;
}
// Module for adding additional functionalty to datastructures
printingStatistics.addPrototypes = function () {
/*
* * * * * * Additional Scripting tools * * * * * *
*
* This script is released to the public domain and may be used, modified and
* distributed without restrictions. Attribution not necessary but appreciated.
* Source: https://weeknumber.net/how-to/javascript
*
* Returns the ISO week of the date.
*/
Date.prototype.getWeek = function () {
var date = new Date(this.getTime());
date.setHours(0, 0, 0, 0);
// Thursday in current week decides the year.
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
// January 4 is always in week 1.
var week1 = new Date(date.getFullYear(), 0, 4);
// Adjust to Thursday in week 1 and count number of weeks from date to week1.
return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 -
3 + (week1.getDay() + 6) % 7) / 7);
}
// Returns how many weeks are in a given year.
Date.prototype.getWeeksInYear = function () {
var date = new Date(this.getTime());
var isLeap = new Date(date.getYear() + 1900, 1, 29).getMonth() === 1;
//check for a Jan 1 that's a Thursday or a leap year that has a
//Wednesday jan 1. Otherwise it's 52
return date.getDay() === 4 || isLeap && date.getDay() === 3 ? 53 : 52
}
// Returns what quarter a specified date is in
Date.prototype.getWeekQuarter = function () {
var date = new Date(this.getTime());
weekNum = date.getWeek();
return weekNum == 53 ? 4 : Math.ceil(weekNum / 13);
}
// Creates a summation of the array
Array.prototype.sum = function () {
return this.reduce(function (a, b) {
return a + b
}, 0);
}
}();
var interval = null;
var done = function (obj) {
// Function called at interval that checks if the object exists and runs
// printingStatistics.graphPages.init() once it does.
if (obj) {
clearInterval(interval)
printingStatistics.graphPages.init(obj, ".ms-rte-layoutszone-inner")
}
}
printingStatistics.executeQueries.init(
"Printing Stats",
["Title",
"PreviousOrCurrentQuarter",
"RelevantDate",
"TotalPrintedPages"],
"Current Stats"
);
printingStatistics.executeQueries.execute()
interval = setInterval(function () {
done(printingStatistics.executeQueries.getStats())
}, 200)