6
\$\begingroup\$

Among the mod-tools there is a few that are intended to just help get a bird's eye perspective on what happens on the site. These tools generally consist of tables. Tables over tables.

Stuff that would make an executive business analyst happy.

One of these tools reports some quite useful numbers as both absolute numbers and percentages. These percentages are pretty useful, but each of them is in a separate column.

The table columns basically follow this schema:

  • User
  • Metric A
  • Metric B
  • ...
  • Metric F
  • Metric A%
  • Metric B%
  • ...
  • Metric F%
  • Metric G
  • Metric H
  • ...

Not every metric has a corresponding percentage report. None of the percentage reports comes before its corresponding metric. Unfortunately there's a rather large number of these metrics, which results in the table exceeding the horizontal space available on my screen most of the time.

That's not useful, and neither is the separation between the Metrics and their percentage report. So I wrote the following user-script to collapse the percentage reports into the same column as the absolute numbers.

// ==UserScript==
// @name Advanced Review Stats TableCollapser
// @namespace http://github.com/Vogel612
// @version 1.0
// @description Collapse review-stats columns
// @updateURL https://raw.githubusercontent.com/Vogel612/mini-se-userscripts/master/advanced-review-stats-collapser.user.js
// @downloadURL https://raw.githubusercontent.com/Vogel612/mini-se-userscripts/master/advanced-review-stats-collapser.user.js
// @author Vogel612
// @match *://*.stackexchange.com/admin/review/breakdown*
// @match *://*.stackoverflow.com/admin/review/breakdown*
// @match *://*.superuser.com/admin/review/breakdown*
// @match *://*.serverfault.com/admin/review/breakdown*
// @match *://*.askubuntu.com/admin/review/breakdown*
// @match *://*.stackapps.com/admin/review/breakdown*
// @match *://*.mathoverflow.net/admin/review/breakdown*
// @grant none
// ==/UserScript==
(function() {
 'use strict';
 let statsTable = document.querySelectorAll("#content .mainbar-full table")[0];
 let headers = Array.from(statsTable.getElementsByTagName("th"));
 let percentageHeaders = filterHeaders(headers);
 let collapsings = buildCollapseSpecs(headers, percentageHeaders);
 collapseTable(statsTable, collapsings);
 function filterHeaders(headers) {
 let percentageHeaders = [];
 for (let head of headers) {
 if (head.innerHTML.endsWith("%")) {
 percentageHeaders.push(head);
 }
 }
 return percentageHeaders;
 }
 function buildCollapseSpecs(headers, percentageHeaders) {
 let collapsings = [];
 for (let percentage of percentageHeaders) {
 let h = percentage.innerHTML;
 // drop trailing %
 let header = h.substr(0, h.length - 1);
 let from = headers.indexOf(percentage);
 // find matching non-percentage header
 var to = -1;
 for (let tableHead of headers) {
 if (tableHead.innerHTML === header) {
 to = headers.indexOf(tableHead);
 break;
 }
 }
 // we can't collapse upwards
 if (to !== -1 && to <= from) {
 collapsings.push({from: from, to: to});
 }
 }
 return collapsings;
 }
 function collapseTable(table, collapsings) {
 // sort collapsings by from descending to allow us to drop the column we're done with
 collapsings.sort((a,b) => { return b.from - a.from });
 // collapse the columns
 for (let collapse of collapsings) {
 for (let row of table.getElementsByTagName("tr")) {
 collapseRow(row, collapse);
 }
 }
 }
 function collapseRow(row, collapseSpec) {
 let from = row.children[collapseSpec.from];
 let percentageValue = from.innerHTML.trim();
 if (from.tagName === "TH") {
 row.children[collapseSpec.to].innerHTML += " (%)";
 } else {
 row.children[collapseSpec.to].innerHTML += " (" + percentageValue + ")";
 }
 row.removeChild(from);
 }
})();

How could this script be more extensible and maintainable?

janos
113k15 gold badges154 silver badges396 bronze badges
asked Jul 7, 2018 at 19:14
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Algorithm

The algorithm to build the collapse-specs is inefficient:

  1. It makes a pass over all headers to find the percentage headers
  2. Then for each percentage header:
    1. It finds the index using Array.indexOf, which is \$O(n)\$
    2. It loops over the headers to find the one corresponding to the percentage header by name, and again finds the index of that header using Array.indexOf

That is, the multiple Array.indexOf calls to find the index of header columns is inefficient, when the indexes could be recorded in one preparatory pass.

A simpler and more efficient alternative is possible:

  • Build a map of { name: {from: ..., to: ...} }, where name is the trailing % stripped from the column header being visited.
  • For each header:
    • If it's not a percentage header, update in the map the to value for the header's name
    • If it's a percentage header, update in the map the from value for the header's sanitized name
  • Extract from the map the values that have both from and to values -> this should be equivalent to the collapsings in the posted code

Functional programming

Functional writing style can make the code more compact, and potentially more natural and easier to read. For example you could replace filterHeaders with this one-liner:

return headers.filter(head => head.innerHTML.endsWith("%"))

Similarly, the sort can be written simpler too:

collapsings.sort((a, b) => b.from - a.from);

Other parts of the script can be written simpler too, using functional features, but I suggest to change the algorithm first before optimizing the existing code.

answered Jul 7, 2018 at 21:26
\$\endgroup\$
0

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.