1
\$\begingroup\$

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)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.