3
\$\begingroup\$

This thread is devoted to the development of an answer-chaining stack snippet, which was originally used in Evolution of OEIS. By making this snippet available here (following this meta guideline), other users can use it as a jumping-off point to make snippets for their own answer-chaining challenges. See also: Leaderboard Snippet

Feel free to put feature requests and bug reports in answers, or let me know in chat.

The use of this snippet in other challenges will probably require some significant adjustment, since the other challenge will certainly be scraping the answers for different information. Most of the adjustment will consist of:

  • Adjusting the regexes to fetch other data from the answer
  • Adding/removing properties of the answer/user objects as appropriate
  • Adjusting the text displayed on the tree diagram
  • Adjusting the columns of the tables
  • Changing the scoring formula

But in the end this should still be significantly less work than writing a whole new snippet.

Features

  • Generates a tree diagram showing how each answer is descended from a previous answer. Each node is labeled with the username and is linked to the corresponding answer.
  • One column is a list of answers and details. In this case, the list is sorted by sequence number.
  • Another column is a ranking of all users, sorted by score, including details such as their number of answers.

Usage

  • Similar to the leaderboard snippet, the snippet can be targeted at a different challenge thread by searching for QUESTION_ID and changing that value to the ID of the challenge. You can also search for codegolf and change that to something else, such as meta.codegolf if you are testing it out on a meta thread.

Formatting instructions

This snippet is currently configured to accept answers with the following header:

# [language], [number] bytes, depth [number], A[new sequence] from A[old sequence] 
  • [language] - used only as part of the list of answers
  • [number] bytes - used in scoring calculation
  • depth [number] - used in scoring calculation (not needed to create the tree)
  • A[new sequence] - a unique identifier for this answer, used to make the tree
  • from A[old sequence] - the unique identifier of the previous answer, used to make the tree. If omitted, then this answer is inserted under the generic "root".

Furthermore, this snippet takes one additional piece of information from the answer, which is a line of numbers preceded by the word terms: which was used to add some labels to the tree diagram.

Uncompressed Snippet:

Run it, hit "full page," and scroll to see everything.

/* Configuration */

var QUESTION_ID = 49223; // Obtain this from the url
// It will be like http://XYZ.stackexchange.com/questions/QUESTION_ID/... on any question page
var ANSWER_FILTER = "!t)IWYnsLAZle2tQ3KqrVveCRJfxcRLe";

/* App */

var data = [{
  "name": "OEIS",
  "parent": "null",
  "term": "",
  "author": "",
  "URL": "https://oeis.org/"
}];

var answers = [],
  page = 1;

function answersUrl(index) {
  return "https://api.stackexchange.com/2.2/questions/" + QUESTION_ID + "/answers?page=" + index + "&pagesize=100&order=desc&sort=creation&site=codegolf&filter=" + ANSWER_FILTER;
}

function getAnswers() {
  jQuery.ajax({
    url: answersUrl(page++),
    method: "get",
    dataType: "jsonp",
    crossDomain: true,
    success: function(data) {
      answers.push.apply(answers, data.items);
      if (data.has_more) getAnswers();
      else process();
    }
  });
}

getAnswers();

var SIZE_REG = /\d+(?=[^\d&]*(?:<(?:s>[^&]*<\/s>|[^&]+>)[^\d&]*)*,)/;
var DEPTH_REG = /\d+, A/;
var NUMBER_REG = /\d+/;
var LANGUAGE_REG = /^#*\s*([^,]+)/;
var SEQUENCE_REG = /A\d+/;
var PARENT_REG = /from\s*A\d+/;
var SEQDATA_REG = /terms:\s*(?:(?:-)?\d+,\s*)*((?:-)?\d+)/;

function shouldHaveHeading(a) {
  var pass = false;
  var lines = a.body_markdown.split("\n");
  try {
    pass |= /^#/.test(a.body_markdown);
    pass |= ["-", "="].indexOf(lines[1][0]) > -1;
    pass &= LANGUAGE_REG.test(a.body_markdown);
  } catch (ex) {}
  return pass;
}

function shouldHaveScore(a) {
  var pass = false;
  try {
    pass |= SIZE_REG.test(a.body_markdown.split("\n")[0]);
  } catch (ex) {}
  return pass;
}

function getAuthorName(a) {
  return a.owner.display_name;
}

function decodeEntities(user) {
  return $("<textarea>").html(user).text()
}

function process() {
  answers = answers.filter(shouldHaveScore).filter(shouldHaveHeading);
  answers.reverse();

  var users = {};
  var sequserpairs = [];
  var place = 1;
  var lastSize = null;
  var lastPlace = 1;
  var legalanswers = [];

  answers.forEach(function(a) {
    var headline = a.body_markdown.split("\n")[0];
    var user = getAuthorName(a);
    var sequence = headline.match(SEQUENCE_REG)[0];
    sequence = sequence.trim();
    var parent = "from A000000";
    if (PARENT_REG.test(headline)) parent = headline.match(PARENT_REG)[0];
    parent = parent.substring(5).trim();
    if (parent == "A000000") parent = "OEIS";

    var seqdata = "";
    if (SEQDATA_REG.test(a.body_markdown)) seqdata = a.body_markdown.match(SEQDATA_REG)[1];

    var isoriginal = true;
    for (var i = 0; i < sequserpairs.length; ++i) {
      isoriginal = isoriginal && !(sequserpairs[i] === sequence);
    }
    var islegal = true;
    for (var i = 0; i < sequserpairs.length; ++i) {
      islegal = islegal && !(sequserpairs[i] === sequence) && !(sequserpairs[i] === sequence + user) && !(sequserpairs[i] === parent + user);
    }
    sequserpairs.push(sequence);
    sequserpairs.push(sequence + user);
    sequserpairs.push(parent + user);
    if (isoriginal) data.push({
      "name": sequence,
      "parent": parent,
      "term": seqdata + " : ",
      "author": decodeEntities(user),
      "URL": a.share_link
    });
    if (islegal) {
      legalanswers.push(a);
    }
  });

  answers.sort(function(a, b) {
    var aB = (a.body_markdown.split("\n")[0].match(SEQUENCE_REG)),
      bB = (b.body_markdown.split("\n")[0].match(SEQUENCE_REG));
    if (aB < bB) return -1;
    if (aB > bB) return 1;
  })

  answers.forEach(function(a) {
    var headline = a.body_markdown.split("\n")[0];
    //console.log(a);

    var num = headline.match(NUMBER_REG)[0];
    var size = (headline.match(SIZE_REG) || [0])[0];
    var depth = parseInt((headline.match(DEPTH_REG) || [0])[0]).toString();
    var sequence = headline.match(SEQUENCE_REG)[0];
    var parent = "from A000000";
    if (PARENT_REG.test(headline)) parent = headline.match(PARENT_REG)[0];
    parent = parent.substring(5);
    var language = headline.match(LANGUAGE_REG)[1];
    if (language.indexOf("]") > 0) language = language.substring(1, language.indexOf("]"));
    var user = getAuthorName(a);
    var islegal = false;
    for (var i = 0; i < legalanswers.length; ++i) {
      islegal = islegal || (legalanswers[i] === a);
    }
    if (islegal) {
      var answer = jQuery("#answer-template").html();
      if (size != lastSize) lastPlace = place;
      lastSize = size;
      ++place;

      var score = (Math.pow(parseInt(depth), 0.5) * 1024 / (parseInt(size) + 256));

      answer = answer.replace("{{SEQUENCE}}", sequence) //lastPlace + "."
        .replace("{{SEQUENCE}}", sequence)
        .replace("{{NAME}}", user)
        .replace("{{LANGUAGE}}", language)
        .replace("{{SIZE}}", size)
        .replace("{{DEPTH}}", depth)
        .replace("{{LINK}}", a.share_link);
      answer = jQuery(answer);
      jQuery("#answers").append(answer);

      users[user] = users[user] || {
        lang: language,
        user: user,
        size: "0",
        numanswers: "0",
        link: a.share_link
      };
      users[user].size = (parseFloat(users[user].size) + score).toString();
      users[user].numanswers = (parseInt(users[user].numanswers) + 1).toString();
    }
  });

  var userlist = [];
  for (var user in users)
    if (users.hasOwnProperty(user)) userlist.push(users[user]);

  userlist.sort(function(a, b) {
    if (parseFloat(a.size) > parseFloat(b.size)) return -1;
    if (parseFloat(a.size) < parseFloat(b.size)) return 1;
    return 0;
  });

  for (var i = 0; i < userlist.length; ++i) {
    var userentry = jQuery("#language-template").html();
    var user = userlist[i];
    userentry = userentry.replace("{{RANK}}", i + 1 + ".")
      .replace("{{NAME}}", user.user)
      .replace("{{NUMANSWERS}}", user.numanswers)
      .replace("{{SIZE}}", user.size);
    userentry = jQuery(userentry);
    jQuery("#languages").append(userentry);
  }
  createTree();
}

//everything below credit to d3noob at http://bl.ocks.org/d3noob/8329404

// *********** Convert flat data into a nice tree ***************
// create a name: node map
function createTree() {
  var dataMap = data.reduce(function(map, node) {
    map[node.name] = node;
    return map;
  }, {});

  // create the tree array
  var treeData = [];
  data.forEach(function(node) {
    // add to parent
    var parent = dataMap[node.parent];
    if (parent) {
      // create child array if it doesn't exist
      (parent.children || (parent.children = []))
      // add node to child array
      .push(node);
    } else {
      // parent is null or missing
      treeData.push(node);
    }
  });


  // ************** Generate the tree diagram	 *****************
  var margin = {
      top: 20,
      right: 120,
      bottom: 20,
      left: 120
    },
    width = 3203 - margin.right - margin.left,
    height = 4000 - margin.top - margin.bottom;

  var i = 0;

  var tree = d3.layout.tree()
    .size([height, width]);

  var diagonal = d3.svg.diagonal()
    .projection(function(d) {
      return [d.y, d.x];
    });

  var svg = d3.select("body").append("svg")
    .attr("width", width + margin.right + margin.left)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  root = treeData[0];

  update(root);

  function update(source) {

    // Compute the new tree layout.
    var nodes = tree.nodes(root).reverse(),
      links = tree.links(nodes);

    // Normalize for fixed-depth.
    nodes.forEach(function(d) {
      d.y = d.depth * 180;
    });

    // Declare the nodes…
    var node = svg.selectAll("g.node")
      .data(nodes, function(d) {
        return d.id || (d.id = ++i);
      });

    // Enter the nodes.
    var nodeEnter = node.enter().append("g")
      .attr("class", "node")
      .attr("transform", function(d) {
        return "translate(" + d.y + "," + d.x + ")";
      });

    nodeEnter.append("a").attr("xlink:href", function(d) {
        return d.URL
      }).append("circle")
      .attr("r", 10)
      .style("fill", "#fff");

    nodeEnter.append("text")
      .attr("x", function() {
        return 0;
      })
      .attr("y", function() {
        return 20;
      })
      .attr("dy", ".35em")
      .attr("text-anchor", function() {
        return "middle";
      })
      .text(function(d) {
        return d.term + d.name;
      })
      .style("fill-opacity", 1);

    nodeEnter.append("text")
      .attr("x", function() {
        return 0;
      })
      .attr("y", function() {
        return 35;
      })
      .attr("dy", ".35em")
      .attr("text-anchor", function() {
        return "middle";
      })
      .text(function(d) {
        return d.author;
      })
      .style("fill-opacity", 1);

    // Declare the links…
    var link = svg.selectAll("path.link")
      .data(links, function(d) {
        return d.target.id;
      });

    // Enter the links.
    link.enter().insert("path", "g")
      .attr("class", "link")
      .attr("d", diagonal);

  }
}
body {
  text-align: left !important
}

#answer-list {
  padding: 10px;
  width: 550px;
  float: left;
}

#language-list {
  padding: 10px;
  width: 290px;
  float: left;
}

table thead {
  font-weight: bold;
}

table td {
  padding: 5px;
}

.node circle {
  fill: #fff;
  stroke: steelblue;
  stroke-width: 3px;
}

.node text {
  font: 12px sans-serif;
}

.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 2px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v3.min.js"></script>
<link rel="stylesheet" type="text/css" href="//cdn.sstatic.net/codegolf/all.css?v=83c949450c8b">
<div id="answer-list">
  <h2>Sequence List</h2>
  <table class="answer-list">
    <thead>
      <tr>
        <td>Sequence</td>
        <td>Author</td>
        <td>Language</td>
        <td>Size</td>
        <td>Depth</td>
      </tr>
    </thead>
    <tbody id="answers"></tbody>
  </table>
</div>
<div id="language-list">
  <h2>Leaderboard</h2>
  <table class="language-list">
    <thead>
      <tr>
        <td>Rank</td>
        <td>User</td>
        <td>Answers</td>
        <td>Score</td>
      </tr>
    </thead>
    <tbody id="languages"></tbody>
  </table>
</div>
<table style="display: none">
  <tbody id="answer-template">
    <tr>
      <td><a href="https://oeis.org/{{SEQUENCE}}">{{SEQUENCE}}</a></td>
      <td>{{NAME}}</td>
      <td>{{LANGUAGE}}</td>
      <td>{{SIZE}}</td>
      <td>{{DEPTH}}</td>
      <td><a href="{{LINK}}">Link</a> </td>
    </tr>
  </tbody>
</table>
<table style="display: none">
  <tbody id="language-template">
    <tr>
      <td>{{RANK}}</td>
      <td>{{NAME}}</td>
      <td>{{NUMANSWERS}}</td>
      <td>{{SIZE}}</td>
    </tr>
  </tbody>
</table>

\$\endgroup\$

You must log in to answer this question.

Browse other questions tagged .