Jump to content

User:Md gilbert/vte.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Md gilbert (talk | contribs) at 18:10, 16 October 2014 (Using https for backend calls, update to use Members module, bug fixing and cleanup.). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// global variables, as required

var vte = {
  // initialize - application constructor
  initialize: function() {
    // Create the VTE button
    var $btn = $(
      "<div class='vectorMenu' id='p-vte'>" +
      "  <h3><span>VTE</span></h3>" +
      "</div>"
    ).attr("title", "Open the Virtual Team Explorer");
    // Add the button to the left of the search box
    $("#p-search").before($btn);
    // Define our click action
    $("#p-vte").on("click", function() {
      console.log("opening vte");
      vte.renderOverlay();
    });
  }, // end initialize

  // renderOverlay - draws the initial vte lightbox
  renderOverlay: function() {
    // If the window already exists, just display it
    if ( $("#vte-window").length > 0 ) {
      $("#vte-window").show();
      return;
    }

    // Otherwise, create the vte window
    var $vteWindow = $(
      "<div id='vte-window'>" +
      "  <div id='vte-window-left'>" +
      "    <div id='vte-window-left-project' />" +
      "    <div id='vte-window-left-nav' />" +
      "  </div>" +
      "  <div id='vte-window-right'> " +
      "    <div id='vte-window-right-title' />" +
      "    <div id='vte-window-right-tool' />" +
      "    <div id='vte-window-right-content' />" +
      "  </div>" +
      "  <div style='clear: both;'></div>" +
      "</div>"
    );
    // Create and style the main vte window
    $vteWindow.css(s_vteWindow);
    $("#content").append($vteWindow);
    $("#vte-window-left").css(s_vteWindowLeft);
    $("#vte-window-left-project").css(s_vteWindowLeftProject);
    $("#vte-window-left-nav").css(s_vteWindowLeftNav);
    $("#vte-window-right").css(s_vteWindowRight);
    $("#vte-window-right-title").css(s_vteWindowRightTitle);
    $("#vte-window-right-tool").css(s_vteWindowRightTool);
    $("#vte-window-right-content").css(s_vteWindowRightContent);
    // Fill in vte elements
    vte.populateTitle();
    vte.populateTool();
    vte.populateProject();
    vte.populateNav();
    vte.populateContent();
    // Add external libraries (d3)
    // Add to head either https://raw.githubusercontent.com/mbostock/d3/master/d3.min.js or
    // https://rawgit.com/mbostock/d3/master/d3.min.js ???
    // https://alahele.ischool.uw.edu:8997/js/d3.min.js
    var head = document.getElementsByTagName("head")[0];
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = 'https://alahele.ischool.uw.edu:8997/js/d3.min.js';
    head.appendChild(script);
  },

  // populateTitle - draws title bar content
  populateTitle: function() {
    var $vteTitle = $(
      "<div id='vte-title'>Virtual Team Explorer</div>" +
      "<div id='vte-title-actions'>" +
      "  <div id='vte-title-action-user' title='View user information'>" +
      "    <img src='/media/wikipedia/commons/0/0a/Gnome-stock_person.svg' width='15' height='15' style='padding: 5px;'/>" +
      "  </div>" +
      "  <div id='vte-title-action-settings' title='View VTE settings'>" +
      "    <img src='/media/wikipedia/commons/7/77/Gear_icon.svg' width='25' height='25'/>" +
      "  </div>" +
      "  <div id='vte-title-action-close' title='Close the VTE'>" +
      "    <img src='/media/wikipedia/commons/6/60/Close_icon.svg' width='25' height='25'/>" +
      "  </div>" +
      "</div>"
    );
    // Attributions, via Wikimedia Commons:
    // User: By GNOME icon artists (GNOME download / GNOME FTP) [GPL (http://www.gnu.org/licenses/gpl.html)]
    // Gear: By MGalloway (WMF) (Own work) [CC-BY-SA-3.0 (http://creativecommons.org/licenses/by-sa/3.0)]
    // Close: By MGalloway (WMF) (Own work) [CC-BY-SA-3.0 (http://creativecommons.org/licenses/by-sa/3.0)]

    // Add the title and style the elements
    $("#vte-window-right-title").html($vteTitle);
    $("#vte-title").css(s_vteTitle);
    $("#vte-title-actions").css(s_vteTitleActions);
    $("#vte-title-action-user").css(s_vteTitleAction);
    $("#vte-title-action-settings").css(s_vteTitleAction);
    $("#vte-title-action-close").css(s_vteTitleAction);
    // Add the actions
    $("#vte-title-action-user").on("click", function() {

    });
    $("#vte-title-action-settings").on("click", function() {

    });
    $("#vte-title-action-close").on("click", function() {
      console.log("closing the vte (will maintain current view).");
      $("#vte-window").hide();
    });
  },

  // populateTool - draws tool bar content
  populateTool: function(tool) {

  },

  // populateProject - draws the project browser content
  populateProject: function() {
    // Fetch all projects or load from previous request
    // TODO: allow cookies before deploying
    var projects = []; // $.cookie( 'vte-projects' );
    if (projects.length > 0) {
      vte.drawProjectSelect(projects);
    } else {
      // Add loading message/icon
      var $projectSelect = $(
        "<div id='vte-project-select-label'>Enter a WikiProject to explore:</div>" +
        "<div id='vte-project-select'>" +
        "  <input id='vte-project-select-input' type='text' placeholder='Loading WikiProject data...'/>" +
        "</div>" +
        "<div id='vte-project-select-multi' />"
      );
      // Add it
      $("#vte-window-left-project").html($projectSelect);
      // Style it
      $("#vte-project-select").css(s_vteProjectSelect);
      $("#vte-project-select-label").css(s_vteProjectSelectLabel);
      $("#vte-project-select-input").css(s_vteProjectSelectInput);

      // Make the request
      //var url = 'http://tools.wmflabs.org/catscan2/catscan2.php?depth=10&categories=Active_WikiProjects&ns%5B4%5D=1&doit=1&format=json';
      //var url = 'http://tools.wmflabs.org/catscan2/catscan2.php?depth=2&categories=Active_WikiProjects&ns%5B4%5D=1&topcats_no_talk=1&format=json&doit=1';
      var url = 'https://alahele.ischool.uw.edu:8997/api/getProjects';
      $.ajax({
        url: url,
        dataType: "json",
        success: function(data, stat, xhr) {
          //var projects = data["*"][0]["*"];
          var projects = data["result"];
          // TODO: Allow cookies before deploying
          /*
          $.cookie('vte-projects', projects, {
            expires: 7, // expire in 7 days
            path: '/'   // domain-wide, entire wiki
          });
          */
          vte.drawProjectSelect(projects);
        },
        error: function(xhr, stat, err) {
          console.error("Failed to request project data from catscan2: " + JSON.stringify(xhr));
          $("#vte-window-left-project").html("Failed to request project data from catscan2: " + JSON.stringify(xhr));
        },
        complete: function() {
          $("#vte-project-loading").remove();
        },
      });
    }
  },

  // drawProjectSelect - called by populateProject after successfully requesting project data. 
  // Draws project select box
  drawProjectSelect: function(projects) {
    // Removing loading placeholder
    $("#vte-project-select-input").attr("placeholder", "");
    // Add the actions (everytime there's a key-up, update a box of matching projects)
    $("#vte-project-select-input").on("keyup", function() {
      // Empty the project selection div
      $("#vte-project-select-multi").html("");
      // Then for each project add to the dropdown if it matches the input text
      $.each(projects, function(i,v) {
        // Make sure the div exists
        if ($("#vte-project-select-multi").length === 0) {
          $("#vte-project-select").after("<div id='vte-project-select-multi' />");
        }
        $("#vte-project-select-multi").show();
        //var t = projects[i]['a']['title'].toLowerCase().replace(/_/g, " ");
        var t = projects[i]['p_title'].toLowerCase().replace(/_/g, " ");
        var v = $("#vte-project-select-input").val().toLowerCase();
        var ind = t.indexOf(v);
        if (t.indexOf(v) != -1) {
          //console.log("matches: " + projects[i]['a']['title'].replace("_", " "));
          $("#vte-project-select-multi").append(
            "<div class='vte-project-select-multi-proj' vte-p-id='" + projects[i]['p_id'] + "' " +
            "  vte-p-title='" + projects[i]['p_title'] + "' vte-p-seen='0' " +
            "  vte-p-touched='0' vte-p-created='" + projects[i]['p_created'] + "' >" + 
               projects[i]['p_title'].replace(/_/g, " ") + 
            "</div>"
          );
        }
      });
      if ($("#vte-project-select-multi").html() == "") {
        $("#vte-project-select-multi").append(
          "<div class='vte-project-select-multi-proj' style='color: #848484;'>No matching projects found</div>"
        );
      }
      // Style the container and projects
      $("#vte-project-select-multi").css(s_vteProjectSelectMulti);
      $(".vte-project-select-multi-proj").css(s_vteProjectSelectMultiProj);
      // Add hover color for project
      $(".vte-project-select-multi-proj").hover(
        function() {
          $( this ).css("color", "#3B0B0B");
        }, function() {
          $( this ).css("color", "#000");
        }
      );
      // Add click action to hide the list
      $("body").on("click", function(evt) {
        $("#vte-project-select-multi").hide();
      });
      // Add click action to load project summary
      $(".vte-project-select-multi-proj").on("click", function(evt) {
        var id      = $(evt.currentTarget).attr("vte-p-id");
        var title   = $(evt.currentTarget).attr("vte-p-title");
        var seen    = $(evt.currentTarget).attr("vte-p-seen");
        var touched = $(evt.currentTarget).attr("vte-p-touched");
        var created = $(evt.currentTarget).attr("vte-p-created");
        // Clear the project selection div
        $("#vte-project-select-multi").remove();
        $("#vte-project-select-input").val($(evt.currentTarget).html());
        // Load the project summary
        console.log("loading summary for project " + title + ", id: " + id);
        $("#vte-window").data("vte-project", title);
        $("#vte-window").data("vte-project-id", id);
        vte.populateNav();
        vte.drawProjectSummary(title, id, created);
      });
    });
  },

  // drawProjectSummary - draws summary information for the project once it is selected
  //   from the vte-project-select-multi dropdown
  drawProjectSummary: function(title, id, created) {
    // First clear any existing data in the content window and add summary divs
    $("#vte-window-right-content").html(
      "<div id='vte-window-right-content-summary'>" +
      "  <div id='vte-window-right-content-summary-title'>" + 
           $("#vte-window").data("vte-project") + " - Summary" +
      "  </div>" +
      "  <div id='vte-window-right-content-summary-p-edits'>" +
      "    Edits to Project (blue) and Project Talk (grey) pages" +
      "    <div style='height: 10px; width: 100%; border-bottom: 1px solid #000;'></div>" +
      "    <div id='vte-loading-edits' class='vte-loading'>Loading project edit data...</div>" +
      "  </div>" +
      "  <div id='vte-window-right-content-summary-pages'>" +
      "    Summary of pages in project (pages per namespace)" +
      "    <div style='height: 10px; width: 100%; border-bottom: 1px solid #000;'></div>" +
      "    <div id='vte-loading-pages' class='vte-loading'>Loading project pages...</div>" +
      "  </div>" +
      "  <div id='vte-window-right-content-summary-new'>" +
      "    Latest articles created in project" +
      "    <div style='height: 10px; width: 100%; border-bottom: 1px solid #000;'></div>" +
      "    <div id='vte-loading-new' class='vte-loading'>Loading project pages...</div>" +
      "  </div>" +
      "</div>"
    );
    $("#vte-window-right-content-summary").css(s_vteWindowRightContentSummary);
    $(".vte-loading").css(s_vteLoadingText);
    $("#vte-window-right-content-summary-p-edits").css(s_vteWindowRightContentSummaryGraph);
    $("#vte-window-right-content-summary-pages").css(s_vteWindowRightContentSummaryPages);
    $("#vte-window-right-content-summary-new").css(s_vteWindowRightContentSummaryNew);

    // Request summary data from our backend
    var t = title.replace(/ /g, "_");
    var sd = created.substr(0, 8);
    var sw = vte.convertDateToWikiWeek(sd);
    var url = "https://alahele.ischool.uw.edu:8997/api/getEdits?page=" + t + "&namespace=4|5&group=page|user|date&sd=" + sd;
    $.ajax({
      url: url,
      dataType: "json",
      success: function(data, stat, xhr) {
        vte.drawProjectEdits(data, sw);
      },
      error: function(xhr, stat, err) {
        console.error("Failed to request project edits: " + JSON.stringify(xhr));
        $("#vte-window-right-content-summary").append("Failed to request project edits: " + JSON.stringify(xhr));
      },
      complete: function() {
        $("#vte-loading-edits").remove();
      },
    });

    // Request project pages
    url = "https://alahele.ischool.uw.edu:8997/api/getProjectPages?project=" + t;
    $.ajax({
      url: url,
      dataType: "json",
      success: function(data, stat, xhr) {
        vte.drawProjectPages(data);
        // Once we've got project pages, also grab edit histories for the most recent pages created
        var latest = {};
        for (var p in data["result"]) {
          for (var i in data["result"][p]) {
            if (data["result"][p][i].tp_namespace != 1) continue;
            latest[ data["result"][p][i].pp_id ] = data["result"][p][i].tp_title;
          }
        }
        var keys = Object.keys(latest);
        keys.sort( function(a,b) { return b-a; });
        var aLatest = [];
        for (var i in keys) {
          if (i > 20) continue;
          $("#vte-window-right-content-summary-new").append( 
            keys[i] + " - <a href='http://en.wikipedia.org/wiki/" + latest[ keys[i] ] + "'>" + latest[ keys[i] ] + "</a><br/>" 
          );
        }
      },
      error: function(xhr, stat, err) {
        console.error("Failed to request project pages: " + JSON.stringify(xhr));
        $("#vte-window-right-content-summary").append("Failed to request project pages: " + JSON.stringify(xhr));
      },
      complete: function() {
        $("#vte-loading-pages").remove();
        $("#vte-loading-new").remove();
      },
    });
  },

  // drawProjectPages - draws a summary of pages in a project
  drawProjectPages: function(data) {
    // Structure the pages for vis
    var pages = Array(16);
    for (var i = 0; i < pages.length; i++) pages[i] = { "namespace": i, "count": 0 };
    for (var p in data["result"]) {
      for (var i in data["result"][p]) {
        if (data["result"][p][i].tp_namespace > 15) continue;
        pages[ data["result"][p][i].tp_namespace ].count += 1;
      }
    }
    // D3 bar graph
    var margin = {top: 10, right: 20, bottom: 30, left: 60},
        w = $("#vte-window-right-content").width() - 30 - margin.right - margin.left,
        h = 100 - margin.top - margin.bottom;
    var x = d3.scale.ordinal()
        .rangeRoundBands([0, w], .1);
    var y = d3.scale.linear()
        .range([h, 0]);
    var xAxis = d3.svg.axis()
        .scale(x)
        .orient("bottom")
        .ticks(16, "");
    var yAxis = d3.svg.axis()
        .scale(y)
        .orient("left")
        .ticks(4, "");
    var vis = d3.select("#vte-window-right-content-summary-pages")
        .append("svg:svg")
        .attr("width", w + margin.left + margin.right)
        .attr("height", h + margin.top + margin.bottom)
      .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    x.domain(pages.map(function(d) { return d.namespace; }));
    y.domain([0, d3.max(pages, function(d) { return d.count; })]);
    vis.append("g")
        .attr("transform", "translate(0," + h + ")")
        .call(xAxis)
      .append("text")
        .attr("y", h-10)
        .attr("x", w / 2)
        .attr("dy", ".71em")
        .style("text-anchor", "center")
        .text("Namespace");
    vis.append("g")
        .call(yAxis)
      .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 6)
        .attr("dy", ".71em")
        .style("text-anchor", "end")
        .text("# Pages");

    vis.selectAll(".bar")
        .data(pages)
      .enter().append("rect")
        .attr("x", function(d) { return x(d.namespace); })
        .attr("width", x.rangeBand())
        .attr("y", function(d) { return y(d.count); })
        .attr("height", function(d) { return h - y(d.count); });
  },

  // drawProjectEdits - draws summary edit information for a project and its corresponding Talk page
  drawProjectEdits: function(data, sw) {
    // Structure the edits
    var ew = vte.convertDateToWikiWeek() - 2;
    var talk_edits = Array(ew - sw);
    var page_edits = Array(ew - sw);
    for (var i = 0; i < talk_edits.length; i++) talk_edits[i] = 0;
    for (var i = 0; i < page_edits.length; i++) page_edits[i] = 0;
    for (var i in data["result"]) {
      if (data["result"][i].rc_page_namespace % 2 == 0) page_edits[ data["result"][i].rc_wikiweek - sw ] += data["result"][i].rc_edits;
      if (data["result"][i].rc_page_namespace % 2 == 1) talk_edits[ data["result"][i].rc_wikiweek - sw ] += data["result"][i].rc_edits;
    }
    // D3 sparkline graph
    var w = $("#vte-window-right-content").width() - 20,
        h = 80;
    var t_max = d3.max(talk_edits);
    var p_max = d3.max(page_edits);
    var maxy = t_max > p_max ? t_max : p_max;
    var y = d3.scale.linear()
      .domain([0, maxy])
      .range([0, h]);
    var x = d3.scale.linear()
      .domain([0, page_edits.length])
      .range([0, w]);
    var vis = d3.select("#vte-window-right-content-summary-p-edits")
      .append("svg:svg")
      .attr("width", w)
      .attr("height", h);
    var g1 = vis.append("svg:g").attr("transform", "translate(2, " + h + ")");
    var g2 = vis.append("svg:g").attr("transform", "translate(2, " + h + ")");
    var line = d3.svg.line()
      .x(function(d, i) {
        return x(i);
      })
      .y(function(d) {
        return -1 * y(d);
      });
    g1.append("svg:path").attr("d", line(page_edits)).style({"stroke": "#0000FF", "fill": "transparent"});
    g2.append("svg:path").attr("d", line(talk_edits)).style({"stroke": "#545454", "fill": "transparent"});

    // Add the legend text
    var count_text = [
      { "cx": 10, "cy": 12, "text": maxy + " edits" },
      { "cx": 10, "cy": h-5, "text": "0" }
    ];
    var date_text = [
      { "cx": w / 3, "text": vte.convertWikiWeekToDate( ((ew - sw) / 3) + sw ) },
      { "cx": w * 2 / 3, "text": vte.convertWikiWeekToDate( ((ew - sw) * 2 / 3) + sw) }
    ];
    var text_c = vis.selectAll("text.count")
      .data(count_text)
      .enter().append("text")
      .attr("x", function(d) { return d.cx; })
      .attr("y", function(d) { return d.cy; })
      .text( function(d) { return d.text; })
      .attr("font-family", s_wpFont)
      .attr("font-size", "10px")
      .attr("fill", "#000000");
    var text_d = vis.selectAll("text.date")
      .data(date_text)
      .enter().append("text")
      .attr("x", function(d) { return d.cx; })
      .attr("y", function(d) { return 12; })
      .text( function(d) { return d.text.substring(0,4) + "/"+d.text.substring(4,6) + "/"+d.text.substring(6,8); })
      .attr("font-family", s_wpFont)
      .attr("font-size", "10px")
      .attr("text-anchor", "middle")
      .attr("fill", "#848484");
  },

  // populateNav - draws the navigation content
  populateNav: function() {
    if ($("#vte-window").data("vte-project")) {
      $("#vte-window-left-nav").html(
        "<div id='vte-members' class='vte-nav-link'>Members</div>" +
        "<div id='vte-roles' class='vte-nav-link'>Roles</div>" +
        "<div id='vte-tasks' class='vte-nav-link'>Tasks</div>" +
        "<div id='vte-communication' class='vte-nav-link'>Communication</div>"
      );
      $(".vte-nav-link").css(s_vteNavLink);
      // Add the click actions for the links
      $(".vte-nav-link").click(function(e) {
        var id = $(e.currentTarget).attr("id");
        if (id == "vte-members") {
          vte.clickMembers();
        } else if (id == "vte-roles") {
          vte.clickRoles();
        } else if (id == "vte-tasks") {
          vte.clickTasks();
        } else if (id == "vte-communication") {
          vte.clickCommunication();
        } else {
          console.error("Unknown vte action: " + id);
        }
      });
    }
  },

  // Functions to populate the primary vte systems (ie, members, tasks, etc)
  clickMembers: function() {
    console.log("vte - drawing member content");
    // Load the Members page for the current project (should be Wikipedia:<project name>/Members
    var project = $("#vte-window").data("vte-project");
    $.getJSON(
      mw.util.wikiScript('api'),
      {
        format: 'json',
        action: 'query',
        prop: 'revisions',
        rvprop: 'content',
        rvlimit: 1,
        titles: 'Wikipedia:' + project + "/Members",
      }
    )
    .done(function(data) {
      var page, text;
      try {
        // This should only return one page...
        for ( page in data.query.pages) {
          text = data.query.pages[page].revisions[0]['*'];
        }
      } catch( e ) {
        console.error("Failed to request members page for project " + project);
      }
      vte.drawMembers(text);
    })
    .fail(function() {
      console.error("Failed to request members page for project " + project);
    });

  },
  clickRoles: function() {

  },
  clickTasks: function() {

  },
  clickCommunication: function() {

  },

  drawMembers: function(data) {
    // Clear the current content window
    $("#vte-window-right-content").html(
      "<div id='vte-window-right-content-members-title'>" +
         $("#vte-window").data("vte-project") + " - Members" +
      "</div>"
    );
    $("#vte-window-right-content-members-title").css(s_vteWindowRightContentTitle);

    // Grab current project users from the Members page text
    //var obj = vte.parseMemberPage(data);
    var obj = vte.parseTable(data, "Members", "printMembersTable");

    // Projects have the option to include anything in the members table, but for
    // the VTE we'll want to display the name, member since, and project roles.
    // Possible column names given in [[Module:Members]] (although these aren't enforced).
    var c_map = {};
    for (var i in obj.cols) {
      if (obj.cols[i].slice(0,4) == "name") { c_map["name"] = i; }
      else if (obj.cols[i] == "member_since") { c_map["member_since"] = i; }
      else if (obj.cols[i] == "project_roles") { c_map["project_roles"] = i; }
      else if (obj.cols[i] == "member_status") { c_map["member_status"] = i; }
    }

    // Once we've parsed all the users from the Members page, draw the VTE members content
    var roles_input = "project_roles" in c_map ? 
      " Project Role: <input type='text' id='vte-members-add-comment' />" : "";
    $("#vte-window-right-content").append(
      "<div id='vte-window-right-content-members-add'>" +
      "  <form id='vte-member-submit' action='javascript();'>" +
      "    Add Member: <input type='text' id='vte-members-add-name' />" +
      roles_input +
      "    <input type='submit' value='Add' />" +
      "  </form>" +
      "</div>"
    );
    $("#vte-window-right-content-members-add").css(s_vteWindowRightContentMembersAdd);
    // Display existing users (header on top, only show columns that were saved on the project's Members page)
    $("#vte-window-right-content").append(
      "<div class='vte-members-actions' style='font-weight: bold; text-align: center;'>Actions</div>"
    );
    if ("name" in c_map) {
      $("#vte-window-right-content").append(
        "<div class='vte-members-name' style='font-weight: bold; text-align: center;'>User name</div>"
      );
    }
    if ("member_since" in c_map) {
      $("#vte-window-right-content").append(
        "<div class='vte-members-date' style='font-weight: bold; text-align: center;'>Member since</div>"
      );
    }
    if ("project_roles" in c_map) {
      $("#vte-window-right-content").append(
        "<div class='vte-members-comment' style='font-weight: bold; text-align: center;'>Project roles</div>"
      );
    }
    $("#vte-window-right-content").append("<div style='clear: both;'/>");

    var action = 
      "<svg height='10' width='10'>" +
      "  <polygon points='2,3 8,3 5,8' style='fill:black;stroke:black;stroke-width:1' />" +
      "  Sorry, your browser does not support inline SVG." +
      "</svg>";

    for (var i in obj.struc) {
      // Same as above, try to display name, member_since, and project_roles. Skip values that aren't there.
      var n = "name" in c_map ? obj.struc[i][c_map["name"]] : "";
      var d = "member_since" in c_map ? obj.struc[i][c_map["member_since"]] : "";
      var r = "project_roles" in c_map ? obj.struc[i][c_map["project_roles"]] : "";
      if (! n) n = "&nbsp;";
      if (! d) d = "&nbsp;";
      if (! r) r = "&nbsp;";
      if (d != "&nbsp;") {
        var m = d.match(/(\d+):(\d+), (\d+) (\S+) (\d+)/);
        var date = m === null ? "" : new Date(m[5] + "-" + vte.getMonth(m[4]) + "-" + m[3] + " " + m[1] + ":" + m[2] + ":00 UTC");
        if (date) d = vte.getDateStr(date);
      }
      // Inactive members will be grey
      var inactive = "";
      if ("member_status" in c_map && obj.struc[i][c_map["member_status"]] == "Inactive")
        inactive = " vte-members-inactive ";
      // Action icon is displayed first
      $("#vte-window-right-content").append(
        "<div class='vte-members-actions' vte-name='" + n + "' " + "  id='action_" + n + "'>" + action + "</div>"
      );
      if ("name" in c_map) {
        $("#vte-window-right-content").append(
          "<div id='vte-members-name-" + n + "' class='vte-members-name " + inactive + "'>" + n + "</div>"
        );
      }
      if ("member_since" in c_map) {
        $("#vte-window-right-content").append(
          "<div id='vte-members-date-" + n + "' class='vte-members-date " + inactive + "'>" + d + "</div>" 
        );
      }
      if ("project_roles" in c_map) {
        $("#vte-window-right-content").append(
          "<div id='vte-members-comment-" + n + "' class='vte-members-comment " + inactive + "'>" + r + "</div>"
        );
      }
      $("#vte-window-right-content").append("<div style='clear: both;' />");
      // Add action to open Actions menu on click
      $("#action_" + n).click(vte.clickMemberActions);
    }
    $(".vte-members-name").css(s_vteMembersName);
    $(".vte-members-date").css(s_vteMembersDate);
    $(".vte-members-comment").css(s_vteMembersComment);
    $(".vte-members-actions").css(s_vteMembersAction);
    $(".vte-members-inactive").css(s_vteMembersInactive);


    // Action to add new member
    $("#vte-member-submit").submit(function(e) {
      e.preventDefault();
      console.log("Adding user to members list");
      // TODO: Verify that the user actually exists, potentially?
      var d = new Date();
      var n = $("#vte-members-add-name").val();
      var member = [];
      if("name" in c_map) member[ c_map["name"] ] = $("#vte-members-add-name").val();
      if("project_roles" in c_map) member[ c_map["project_roles"] ] = $("#vte-members-add-comment").val();
      if("member_since" in c_map) member[ c_map["member_since"] ] = vte.getWikiDateStr(d);
      if("member_status" in c_map) member[ c_map["member_status"] ] = "Active";
      obj.struc.push(member)

      // Build the member string
      var members_str = obj.pre + "{{#invoke:" + obj.mod + "|" + obj.func + "|cols=" + obj.cols.join(",") + "\n";
      for (var i in obj.struc) {
        members_str += "|" + obj.struc[i].join("|");
        members_str += parseInt(i)+1 == obj.struc.length ? "}}" : "\n";
      }
      members_str += obj.post;

      // Make the request to update the wikitext for the Members page
      $.ajax({
        url: mw.util.wikiScript( 'api' ),
        type: 'POST',
        dataType: 'json',
        data: {
          format: 'json',
          action: 'edit',
          title: "Wikipedia:" + $("#vte-window").data("vte-project") + "/Members",
          text: members_str, // will replace entire page content
          summary: "[VTE] Adding member to project: " + $("#vte-members-add-name").val(),
          token: mw.user.tokens.get( 'editToken' )
        }
      })
      .done( function(data) {
        // Add the user to the members list
        console.log("Added member: " + n);
        $("#vte-window-right-content").append(
          "<div class='vte-members-actions' vte-name='" + n + "' id='action_" + n + "'>" + action + "</div>"
        );
        if ("name" in c_map) {
          $("#vte-window-right-content").append(
            "<div id='vte-members-name-" + n + "' class='vte-members-name'>" + n + "</div>"
          );
        }
        if ("member_since" in c_map) {
          $("#vte-window-right-content").append(
            "<div id='vte-members-date-" + n + "' class='vte-members-date'>" + vte.getDateStr(d) + "</div>"
          );
        }
        if ("project_roles" in c_map) {
          $("#vte-window-right-content").append(
            "<div id='vte-members-comment-" + n + "' class='vte-members-comment'>" + 
              $("#vte-members-add-comment").val() + "</div>"
          );
        }
        $("#vte-window-right-content").append("<div style='clear: both;'/>");
        // Add action to open Actions menu on click
        $("#action_" + n).click(vte.clickMemberActions);
        $(".vte-members-name").css(s_vteMembersName);
        $(".vte-members-date").css(s_vteMembersDate);
        $(".vte-members-comment").css(s_vteMembersComment);
        $(".vte-members-actions").css(s_vteMembersAction);
        // And clear out the member inputs
        $("#vte-members-add-name").val("");
        $("#vte-members-add-comment").val("");
        // And update the local members object
        var struc = obj.struc;
        var obj = $("#vte-window").data("Members");
        obj.struc = struc;
        $("#vte-window").data("Members", obj);
      })
      .fail( function(xhr) {
        console.error("Failed to update Members page: " + JSON.stringify(xhr));
      });
    });
  },

  parseTable: function(data, mod, func) {
    // Parse out the module text, plus everything before and after it
    var re = new RegExp("([\\s\\S]*)(\\{\\{#invoke:" + mod + "\\|" + func + "\\|[^\\}]+)\\}\\}([\\s\\S]*)");
    var m = data.match(re);
    if (m === null || m.length != 4) {
      console.error("Failed to find module invokation ({#invoke:" + mod + "|" + func + ")");
      console.error("Page contains: " + data);
      return false;
    }
    var pre = m[1];
    var post = m[3];
    // Break apart the module, grabbing columns first and then parsing arguments
    var m1 = m[2].match(/\|cols=([^\|]+)\|/);
    if (m1 === null) {
      console.error("Failed to find column declaration in module invokation: " + m[2]);
      return false;
    }
    var cols = m1[1].split(",");
    cols = cols.map( function(c) { return c.trim(); } );
    var args = m[2].split("|");
    args = args.slice(2);
    // First, strip the columns from the arguments array (need to have an accurate count)
    var col_row = 0;
    for (var i in args) {
      args[i] = args[i].trim();
      if (args[i].slice(0,5) == "cols=") col_row = i;
    }
    args.splice(col_row, 1);
    // Then store arguments in a 2d array representing table values
    var struc = [];
    var row = [];
    for (var i in args) {
      row.push(args[i]);
      if (row.length == cols.length) {
        struc.push(row);
        row = [];
      }
    }
    var obj = {
      pre: pre,
      post: post,
      mod: mod,
      func: func,
      cols: cols,
      struc: struc,
    };
    // Save the data for later
    $("#vte-window").data(mod, obj);
    return obj;
  },

  parseMemberPage: function(data) {
    // Grab current project users from the Members page text
    var m = data.match(/([\s\S]*\<\!-- START MEMBER LIST --\>)([\s\S]*)(\<\!-- END MEMBER LIST --\>[\s\S]*)/);
    if (m === null) {
      console.error("Failed to find member list on members page (between MEMBER LIST comments)");
      console.error("Page contains: " + data);
      return false;
    }
    var pre = m[1];
    var post = m[3];
    var users = m[2].replace(/\n/g, "[[User:Md gilbert|Md gilbert]] ([[User talk:Md gilbert|talk]])").split(/\|\-\~\~\~/);
    //console.log("Found users: " + users.join(" --- "));
    // Go through all members and add to member struc
    var struc = [];
    for (var i in users) {
      if (users[i] == "[[User:Md gilbert|Md gilbert]] ([[User talk:Md gilbert|talk]])") continue;
      // Name, date, and comments each on their own line
      var user = users[i].split(/\~\~\~/);
      // Erase blank lines (and multi-line content) for this user, only add new columns
      var t_user = [];
      for (var j in user) {
        if (user[j][0] == "|") t_user.push(user[j]);
      }
      user = t_user;
      if (user.length != 3) { // Includes each element plus newline at the end
        console.error("Failed to parse user line: " + users[i]);
        console.error(user);
        continue;
      }
      // Remove leading pipe from each element
      user[0] = user[0].match(/^\| (.*)/)[1];
      user[1] = user[1].match(/^\| (.*)/)[1];
      user[2] = user[2].match(/^\| (.*)/)[1];
      var name = user[0].match(/\[\[User:([^\|]+)\|/)[1];
      var m1 = user[1].match(/(\d+):(\d+), (\d+) (\S+) (\d+)/);
      var date = m1 === null ? user[1] : new Date(Date.UTC(m1[5], vte.getMonth(m1[4])-1, m1[3], m1[2], m1[1], 0));
      struc.push({
        name: name,
        name_str: user[0],
        date: date,
        date_str: user[1],
        comment_str: user[2].replace(/\~\~\~/g, ""),
      });
    }
    // Save members so we can update more easily
    var obj = {pre: pre, post: post, struc: struc};
    $("#vte-window").data("Members", obj);
    return obj;
  },

  clickMemberActions: function(e) {
    var name = $(e.currentTarget).attr("vte-name");
    console.log("opening actions for user " + name);
    $(".vte-members-action-div").remove();

    // Draw the actions window, should support posting to user talk page, viewing contributions, and removing
    var obj = $("#vte-window").data("Members");
    var roles_action = obj.cols.indexOf("project_roles") != -1 ? 
      "<div id='vte-action-role' class='vte-members-actions-action' vte-name='" + name + "'>Edit user roles</div>" :
      "";
    // Get the status of the current user
    var name_i = 0;
    var stat = "";
    var inactive_action = "";
    if (obj.cols.indexOf("member_status") != -1) {
      for (var i in obj.cols) { if (obj.cols[i].slice(0,4) == "name") name_i = i; }
      for (var i in obj.struc) {
        if (obj.struc[i][name_i] == name)
          stat = obj.struc[i][ obj.cols.indexOf("member_status") ] == "Active" ? "inactive" : "active";
      }
      inactive_action =
        "<div id='vte-action-remove' class='vte-members-actions-action' vte-name='" + name + "'>Set "+stat+"</div>";
    }
    $("#vte-window").append(
      "<div class='vte-members-actions-div'>" +
      "  <div id='vte-action-message' class='vte-members-actions-action' vte-name='" + name + "'>Post to user talk</div>" +
         roles_action +
      "  <div id='vte-action-user' class='vte-members-actions-action' vte-name='" + name + "'>View user contributions</div>" +
         inactive_action +
      "</div>"
    );
    $(".vte-members-actions-div").css(s_vteMembersActionsDiv);
    $(".vte-members-actions-action").css(s_vteMembersActionsAction);
    // Position the div by the cursor, relative to parent, considering the scroll
    var x = (e.pageX - $('#vte-window').offset().left) - 150;
    var y = (e.pageY - $('#vte-window').offset().top) + 10;
    $(".vte-members-actions-div").css({ "left": x + "px", "top": y + "px" });

    // Close the actions window on hitting escape or clicking outside the actions window
    var opening = true;
    var t = setTimeout(function() { opening = false; }, 200);
    $(document).on('keyup.hide_member_actions', function(e) {
      if (opening == false) {
        if (e.keyCode == 27) {
          console.log("Closing actions menu on escape.");
          $(".vte-members-actions-div").remove();
          $(document).unbind('keyup.hide_member_actions');
        }
      }
    });
    $(document).on('click.hide_member_actions', function(e) {
      if (opening == false) {
        console.log("Closing actions menu on click.");
        $(".vte-members-actions-div").remove();
        $(document).unbind('click.hide_member_actions');
      }
    });

    // Add the post to user talk page action
    $("#vte-action-message").on("click", function(e) {
      // Remove the actions div
      console.log("Removing the action div box");
      $(".vte-members-actions-div").remove();
      // Open the message box, should contain space for subject and the message
      var name = $(e.currentTarget).attr("vte-name");
      $("#vte-window-right-content").append(
        "<div class='vte-members-actions-message'>" +
        "  <form id='vte-member-message' action='javascript();'>" +
        "    <div class='vte-members-actions-message-subject'>" +
        "      Subject: <input type='text' id='vte-message-subject' />" +
        "    </div>" +
        "    <div class='vte-members-actions-message-text'>" +
        "      Message: <textarea rows='10' cols='50' id='vte-message-text' />" +
        "    </div>" +
        "    <input type='submit' value='Send' />" +
        "  </form>" +
        "</div>"
      );
      $(".vte-members-actions-message").css(s_vteMembersActionsMessage);
      // Close the message window on hitting escape or (TODO) clicking the close button
      $(document).on('keyup.hide_member_message', function(e) {
        if (e.keyCode == 27) {
          $(".vte-members-actions-message").remove();
          $(document).unbind('keyup.hide_member_message');
        }
      });

      // Add action to post to the talk page
      $("#vte-member-message").submit(function(e) {
        e.preventDefault();
        console.log("Posting a message to user talk page: " + name);
        // Pull the text from the message box
        var text = "\n== " + $("#vte-message-subject").val() + " ==\n\n" + $("#vte-message-text").val() + "\n";
        // Ensure the post is signed
        if (text.match(/\~\~\~\~/) == null) text += "[[User:Md gilbert|Md gilbert]] ([[User talk:Md gilbert|talk]]) 18:10, 16 October 2014 (UTC)\n";
        $.ajax({
          url: mw.util.wikiScript( 'api' ),
          type: 'POST',
          dataType: 'json',
          data: {
            format: 'json',
            action: 'edit',
            title: "User_talk:" + name,
            appendtext: text,
            summary: "[VTE] Posting directed talk page message - " + $("#vte-message-subject").val(),
            token: mw.user.tokens.get( 'editToken' )
          }
        })
        .done( function(data) {
          mw.util.jsMessage( "Successfully posted to user's Talk page." );
          console.log("Successfully posted to user's talk page");
          $(".vte-members-actions-message").remove();
        })
        .fail( function(xhr) {
          console.error("Failed to post to user talk page: " + JSON.stringify(xhr));
        });
      });

    }); // End post to talk page vte-action-message click

    // Add the update user role action
    $("#vte-action-role").on("click", function(e) {
      // Remove the actions div
      console.log("Removing the action div box");
      $(".vte-members-actions-div").remove();
      // Open the role update dialogue, prepopulate with this member's current roles
      var name = $(e.currentTarget).attr("vte-name");
      var v = $("#vte-members-comment-" + name).html();
      $("#vte-window-right-content").append(
        "<div class='vte-members-actions-role'>" +
        "  <form id='vte-member-role' action='javascript();'>" +
        "    <div class='vte-role'>" +
        "      Project roles: <input type='text' id='vte-roles-input' value='" + v + "' />" +
        "    </div>" +
        "    <input type='submit' value='Update role' />" +
        "  </form>" +
        "</div>"
      );
      $(".vte-members-actions-role").css(s_vteMembersActionsMessage); // TODO: Could clean this up

      // Close the message window on hitting escape or TODO: clicking outside the window
      var t = setTimeout(function() {
        $(document).on('keyup.hide_role', function(e) {
          if (e.keyCode == 27) {
            $(".vte-members-actions-role").remove();
            $(document).unbind('keyup.hide_role');
          }
        });
/*
        $(document).on('click.hide_role', function(e) {
          $(".vte-members-actions-role").remove();
          $(document).unbind('click.hide_role');
        });
*/
      }, 100);

      $("#vte-member-role").submit(function(e) {
        e.preventDefault();
        // Update the Members page first, then the VTE if we're successful. 
        var obj = $("#vte-window").data("Members");
        var members_str = obj.pre + "{{#invoke:" + obj.mod + "|" + obj.func + "|cols=" + obj.cols.join(",") + "\n";
        var name_i = 0;
        for (var i in obj.cols) { if (obj.cols[i].slice(0,4) == "name") name_i = i; break; }
        for (var i in obj.struc) {
          if (obj.struc[i][name_i] == name) 
            obj.struc[i][ obj.cols.indexOf("project_roles") ] = $("#vte-roles-input").val();
          members_str += "|" + obj.struc[i].join("|");
          members_str += parseInt(i)+1 == obj.struc.length ? "}}" : "\n";
        }
        members_str += obj.post;
        // Make the request to update the wikitext for the Members page
        $.ajax({
          url: mw.util.wikiScript( 'api' ),
          type: 'POST',
          dataType: 'json',
          data: {
            format: 'json',
            action: 'edit',
            title: "Wikipedia:" + $("#vte-window").data("vte-project") + "/Members",
            text: members_str, // will replace entire page content
            summary: "[VTE] Updating roles for user " + name +" in project: "+ $("#vte-window").data("vte-project"),
            token: mw.user.tokens.get( 'editToken' )
          }
         })
        .done( function(data) {
          console.log("Successfully updated member roles for " + name);
          // Just update the roles for the current user
          $("#vte-members-comment-" + name).html($("#vte-roles-input").val());
          // And remove the roles dialogue
          $(".vte-members-actions-role").remove();
          // And save the members struc
          $("#vte-window").data("Members", obj);
        })
        .fail( function(xhr) {
          console.error("Failed to update member roles for " + name + ": " + JSON.stringify(xhr));
        });
      });
    }); // End vte-action-role click

    // Add the view user contributions action
    $("#vte-action-user").on("click", function(e) {
      e.preventDefault();
      // Draw the lightbox that user stats will be placed within
      var name = $(e.currentTarget).attr("vte-name");
      var alt_name = name.replace(/_/g, " ");
      $("#vte-window").append(
        "<div id='vte-members-contribution'>" +
        "  <div id='vte-members-contribution-title'>" + name + " - User Contributions</div>" +
        "  <div id='vte-members-contribution-close'>" +
        "    <img src='/media/wikipedia/commons/6/60/Close_icon.svg' width='25' height='25'>"+
        "  </div>" +
        "  <div style='clear: both;'/>" +
        "  <div id='vte-members-contribution-edits'>" +
        "    Weely edits by namespace over time" +
        "    <div style='height: 10px; width: 100%; border-bottom: 1px solid #000;'></div>" +
      "      <div id='vte-loading-edits' class='vte-loading'>Loading user edits...</div>" +
        "  </div>" +
        "</div>"
      );
      $("#vte-members-contribution").css(s_vteMembersContribution);
      $("#vte-members-contribution-title").css({"float": "left"});
      $("#vte-members-contribution-close").css({"float": "right", "cursor": "pointer"});
      $(".vte-loading").css(s_vteLoadingText);
      $("#vte-members-contribution-edits").css(s_vteMembersContributionEdits);

      // Add action to close the window
      $("#vte-members-contribution-close").click(function(e) {
        $("#vte-members-contribution").remove();
      });

      // Add line graph of user edits, separated by namespace
      var url = "https://alahele.ischool.uw.edu:8997/api/getEdits?user=" + alt_name + "&namespace=0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15&group=page|user|date";
      $.ajax({
        url: url,
        dataType: "json",
        success: function(data, stat, xhr) {
          if (data.errostatus == "fail") {
            console.error("Error: " + data.message);
            return false;
          }
          console.log("Sucessfully fetched user edits.");
          $("#vte-loading-edits").remove();
          vte.drawUserEdits(data);
        },
        error: function(xhr, stat, err) {
          console.error("Failed to request user edits: " + JSON.stringify(xhr));
        },
        complete: function() {
          $("#vte-loading-edits").remove();
        },
      });
    }); // End view user contributions vte-action-user click

    // Add the set user inactive action
    $("#vte-action-remove").on("click", function(e) {
      e.preventDefault();
      // Update the Members page first, then the VTE if we're successful. 
      var name = $(e.currentTarget).attr("vte-name");
      var obj = $("#vte-window").data("Members");
      var members_str = obj.pre + "{{#invoke:" + obj.mod + "|" + obj.func + "|cols=" + obj.cols.join(",") + "\n";
      var name_i = 0;
      var stat = "";
      for (var i in obj.cols) { if (obj.cols[i].slice(0,4) == "name") name_i = i; break; }
      for (var i in obj.struc) {
        // If this is the user we're removing, update status and continue
        stat = obj.struc[i][ obj.cols.indexOf("member_status") ] == "Active" ? "Inactive" : "Active";
        if (obj.struc[i][name_i] == name)
          obj.struc[i][ obj.cols.indexOf("member_status") ] = stat;
        members_str += "|" + obj.struc[i].join("|");
        members_str += parseInt(i)+1 == obj.struc.length ? "}}" : "\n";
      }
      members_str += obj.post;
      // Make the request to update the wikitext for the Members page
      $.ajax({
        url: mw.util.wikiScript( 'api' ),
        type: 'POST',
        dataType: 'json',
        data: {
          format: 'json',
          action: 'edit',
          title: "Wikipedia:" + $("#vte-window").data("vte-project") + "/Members",
          text: members_str, // will replace entire page content
          summary: "[VTE] Removing user " + name +" from project members for  project: "+ $("#vte-window").data("vte-project"),
          token: mw.user.tokens.get( 'editToken' )
        }
      })
      .done( function(data) {
        console.log("Successfully set project member as " + stat + ": " + name);
        // Then update the display of the user
        if (stat == "Inactive") {
          $("#vte-members-name-" + name).css(s_vteMembersInactive);
          $("#vte-members-date-" + name).css(s_vteMembersInactive);
          $("#vte-members-comment-" + name).css(s_vteMembersInactive);
        } else {
          $("#vte-members-name-" + name).css(s_vteMembersName);
          $("#vte-members-date-" + name).css(s_vteMembersDate);
          $("#vte-members-comment-" + name).css(s_vteMembersComment);
        }
        // And save the new version of the members struc
        $("#vte-window").data("Members", obj);
      })
      .fail( function(xhr) {
        console.error("Failed to remove project member - " + name + ": " + JSON.stringify(xhr));
      });
    }); // End vte-action-remove click

  },
  drawRoles: function(data) {

  },
  drawTasks: function(data) {

  }, 
  drawCommunication: function(data) {

  },

  // drawUserEdits: Will structure and graph user edits over time, separated by namespace
  drawUserEdits: function(data) {
    // Structure the data for the graph
    var sw = ew = vte.convertDateToWikiWeek();
    for (var i in data["result"]) if (data["result"][i].rc_wikiweek < sw) sw = data["result"][i].rc_wikiweek;
    var edits = Array(ew - sw);
    for (var i = 0; i < edits.length; i++) edits[i] = {
      date: vte.convertWikiWeekToDate(i), 0:0, 1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 
      8:0, 9:0, 10:0, 11:0, 12:0, 13:0, 14:0, 15:0
    };
    var y_max = 0;
    for (var i in data["result"]) {
      edits[ data["result"][i].rc_wikiweek - sw ][ data["result"][i].rc_page_namespace ] += 
        data["result"][i]["rc_edits"];
      if (data["result"][i].rc_edits > y_max) y_max = data["result"][i].rc_edits;
    }

    // Draw with d3
    var margin = {top: 20, right: 80, bottom: 50, left: 50};
    var w = $("#vte-members-contribution").width() - margin.left - margin.right - 20,
        h = 230 - margin.top - margin.bottom;
    var parseDate = d3.time.format("%Y%m%d").parse;
    var x = d3.time.scale()
        .range([0, w]);
    var y = d3.scale.linear()
        .range([h, 0]);
    var color = d3.scale.category10();
    var xAxis = d3.svg.axis()
        .scale(x)
        .orient("bottom");
    var yAxis = d3.svg.axis()
        .scale(y)
        .orient("left");
    var line = d3.svg.line()
        .interpolate("basis")
        .x(function(d) { return x(d.date); })
        .y(function(d) { return y(d.count); });
        //.attr("shape-rendering", "crispEdges");

    var svg = d3.select("#vte-members-contribution-edits").append("svg")
        .attr("width", w + margin.left + margin.right)
        .attr("height", h + margin.top + margin.bottom)
      .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    color.domain(d3.keys(edits[0]).filter( function(key) { return key !== "date"; }));
    edits.forEach(function(d) {
      d.date = parseDate(d.date);
    });
    var namespaces = color.domain().map(function(ns) {
      return {
        namespace: vte.convertIdToNamespace(ns),
        values: edits.map(function(d) {
          return {date: d.date, count: +d[ns]};
        })
      };
    });
    x.domain(d3.extent(edits, function(d) { return d.date; }));
    y.domain([
      d3.min(namespaces, function(c) { return d3.min(c.values, function(v) { return v.count; }); }),
      d3.max(namespaces, function(c) { return d3.max(c.values, function(v) { return v.count; }); })
    ]);

    svg.append("g")
        .style("fill", "none")
        .style("stroke", "#000")
        .style("shape-rendering", "crispEdges")
        .attr("transform", "translate(0," + h + ")")
        .call(xAxis);
    svg.append("g")
        .style("fill", "none")
        .style("stroke", "#000")
        .style("shape-rendering", "crispEdges")
        .call(yAxis)
      .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 6)
        .attr("dy", ".70em")
        .style("text-anchor", "end")
        .text("Edits");

    var ns = svg.selectAll(".ns")
        .data(namespaces)
      .enter().append("g")
        .attr("class", "ns");
    ns.append("path")
        .style("fill", "none")
        .style("stroke", "steelblue")
        .style("stroke-width", "1.5px")
        .attr("d", function(d) { return line(d.values); })
        .style("stroke", function(d) { return color(d.namespace); });
    ns.append("text")
        .datum(function(d) { 
          // Return null string if the last value was 0 (to avoid overlap)
          if (d.values[d.values.length -1].count == 0) {
            return { namespace: "", value: d.values[d.values.length - 1] };
          } else {
            return { namespace: d.namespace, value: d.values[d.values.length - 1]}; 
          }
        })
        .attr("transform", function(d) { 
          return "translate(" + x(d.value.date) + "," + y(d.value.count) + ")"; 
        })
        .attr("x", 3)
        .attr("dy", ".35em")
        .text(function(d) { return d.namespace; });
    // And then fix the labels on the axes (needed since the ticks and text are defined at the same time above)
    $("#vte-members-contribution-edits > svg text").css({"stroke": "none", "fill": "#000"});
  },

  // populateContent - draws the main content for the given tool/project
  populateContent: function(project, tool) {

  },

  //
  // HELPER FUNCTIONS
  //

  // getDateStr - Given a date object, returns a string like YYYY/mm/dd hh:mm:ss. If no date
  // is given will return the string for the current time.
  getDateStr: function(d) {
    if (typeof(d) === 'undefined') {
      d = new Date();
    }
    return String(d.getFullYear()) + "/" + String(vte.pad( parseInt(d.getMonth()) + 1, 2)) + "/" + String(vte.pad(d.getDate(), 2)) + " " + String(vte.pad(d.getHours(), 2)) + ":" + String(vte.pad(d.getMinutes(), 2)) + ":" + String(vte.pad(d.getSeconds(), 2));
  },
  // getWikiDateStr - Given a date object, returns a wiki-fied date string (the same
  // format that is saved if users enter 18:10, 16 October 2014 (UTC), ie, "13:15, 14 October 2014 (UTC)")
  getWikiDateStr: function(d) {
    if (typeof(d) === 'undefined') {
      d = new Date();
    }
    return String(vte.pad(d.getUTCHours(), 2)) + ":" + String(vte.pad(d.getUTCMinutes(), 2)) + ", " + String(d.getUTCDate()) + " " + vte.getMonthText(d.getUTCMonth() + 1) + " " + String(d.getUTCFullYear()) + " (UTC)";
  },

  getMonth: function(m) {
    var months = { "January": 1, "February": 2, "March": 3, "April": 4,
      "May": 5, "June": 6, "July": 7, "August": 8, "September": 9,
      "October": 10, "November": 11, "December": 12
    };
    if (! (m in months)) {
      console.error("Invalid month: " + m);
    }
    return months[m];
  },
  getMonthText: function(m) {
    var months = {1: "January", 2: "February", 3: "March", 4: "April",
      5: "May", 6: "June", 7: "July", 8: "August", 9: "September",
      10: "October", 11: "November", 12: "December"
    };
    if (! (m in months)) {
      console.error("Invalid month number: " + m);
    }
    return months[m];
  },

  // convertDateToWikiWeek - helper function to convert a date of the form YYYYmmdd to wikiweek
  convertDateToWikiWeek: function(d) {
    if (typeof(d) === 'undefined') {
      var date = new Date();
      d = String(date.getFullYear()) + String(vte.pad( parseInt(date.getMonth()) + 1, 2)) + String(vte.pad(date.getDate(), 2));
    }
    var ms = new Date(d.substring(0,4) + '/' + d.substring(4,6) + '/' + d.substring(6,8) + ' 00:00:00').getTime();
    var originMs = new Date('2001/01/01 00:00:00').getTime();
    var msDiff = ms - originMs;
    // milliseconds in a week
    var week = 7 * 24 * 60 * 60 * 1000;
    // weeks in the millisecond range
    return Math.floor(msDiff / week);
  },
  pad: function(n, width, z) {
    z = z || '0';
    n = n + '';
    return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
  },
  convertWikiWeekToDate: function(ww) {
    // milliseconds in wiki weeks
    var ms = ww * 7 * 24 * 60 * 60 * 1000;
    // Add milliseconds since the epoch to week ms value
    var mil = new Date('2001/01/01 00:00:00').getTime() + ms;
    var date = new Date(mil);

    // Date will be of form YYYYmmdd
    return String(date.getFullYear()) + String(vte.pad( parseInt(date.getMonth()) + 1, 2)) + String(vte.pad(date.getDate(), 2));
  },
  convertIdToNamespace: function(id) {
    var ns = {
      0: "Article", 1: "Article talk", 2: "User", 3: "User talk",
      4: "Wikipedia", 5: "Wikipedia talk", 6: "File", 7: "File talk",
      8: "MediaWiki", 9: "MediaWiki talk", 10: "Template", 11: "Template talk",
      12: "Help", 13: "Help talk", 14: "Category", 15: "Category talk",
      100: "Portal", 101: "Portal talk", 108: "Book", 109: "Book talk",
      118: "Draft", 119: "Draft talk"
    };
    return ns[id];
  },

};

/**** Styles ****/
var s_wpFont = 'Verdana, "Verdana Ref", Corbel, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", "DejaVu Sans", "Bitstream Vera Sans", "Liberation Sans", sans-serif';
var s_vteNavLink = {
  "margin": "5px 0px 0px 10px",
  "cursor": "pointer",
  "color": "#0B0B61"
};
var s_vteWindow = {
  "position": "fixed",
  "width": "80%",
  "height": "80%",
  "background-color": "#FFFFFF",
  "top": "50px",
  "left": "10%",
  "box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "-moz-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "-webkit-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "z-index": "5"
};
var s_vteMembersContribution = {
  "font-family": s_wpFont,
  "position": "absolute",
  "top": "5%",
  "left": "5%",
  "width": "80%",
  "height": "80%",
  "background-color": "rgba(255,255,255,.95)",
  "padding": "20px",
  "box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "-moz-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "-webkit-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "border-radius": "10px",
  "-moz-border-radius": "10px",
  "overflow-y": "auto",
};
var s_vteWindowRightContentTitle = {
  "font-family": s_wpFont,
  "font-size": "13px",
  "padding": "10px 0px 10px 0px",
  "color": "#151515",
};
var s_vteMembersActionsMessage = {
  "font-family": s_wpFont,
  "position": "absolute",
  "top": "100px",
  "left": "25%",
  "background-color": "rgba(255,255,255,.95)",
  "padding": "20px",
  "box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "-moz-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
  "-webkit-box-shadow": "0 1px 6px rgba(0, 0, 0, 0.2)",
};
var s_vteMembersActionsDiv = {
  "position": "absolute",
  "background-color": "#F5DA81", // yellow-orange
  "padding": "10px",
  "border-radius": "10px",
  "-moz-border-radius": "10px",
};
var s_vteMembersActionsAction = {
  "font-family": s_wpFont,
  "font-size": "10px",
  "color": "#424242",
  "text-align": "left",
  "cursor": "pointer",
  "margin": "3px 0px 3px 5px",
};
var s_vteWindowRightContentMembersAdd = {
  "font-family": s_wpFont,
  "font-size": "12px",
  "padding": "10px 0px 20px 0px",
  "color": "#424242",
};
var s_vteMembersName = {
  "display": "inline",
  "font-family": s_wpFont,
  "font-size": "12px",
  "color": "#424242",
  "float": "left",
  "width": "20%",
  "padding-top": "5px",
  "border-bottom": "1px solid #EEE",
};
var s_vteMembersDate = {
  "display": "inline",
  "font-family": s_wpFont,
  "font-size": "12px",
  "color": "#424242",
  "float": "left",
  "width": "20%",
  "padding-top": "5px",
  "border-bottom": "1px solid #EEE",
};
var s_vteMembersComment = {
  "display": "inline",
  "font-family": s_wpFont,
  "font-size": "12px",
  "color": "#424242",
  "float": "left",
  "width": "-moz-calc(60% - 90px)",    // Firefox
  "width": "-webkit-calc(60% - 90px)", // Webkit
  "width": "-o-calc(60% - 90px)",      // Opera
  "width": "calc(60% - 90px)",         // Standard
  "text-align": "center",
  "padding-top": "5px",
  "border-bottom": "1px solid #EEE",
};
var s_vteMembersAction = {
  "display": "inline",
  "font-family": s_wpFont,
  "font-size": "12px",
  "color": "#424242",
  "float": "left",
  "width": "80px",
  "text-align": "center",
  "cursor": "pointer",
  "padding-top": "5px",
  "border-bottom": "1px solid #EEE",
};
var s_vteMembersInactive = {
  "color": "#A4A4A4",
};
var s_vteWindowLeft = {
  "float": "left",
  "padding": "5px 5px 5px 5px",
  "border-right": "1px solid #EEE",
  // Set width as 20% minus padding, borders, etc
  "width": "-moz-calc(20% - 11px)",    // Firefox
  "width": "-webkit-calc(20% - 11px)", // Webkit
  "width": "-o-calc(20% - 11px)",      // Opera
  "width": "calc(20% - 11px)",         // Standard
  // Same as width, height is 100% minus border, padding, etc
  "height": "-moz-calc(100% - 11px)",   
  "height": "-webkit-calc(100% - 11px)",
  "height": "-o-calc(100% - 11px)",
  "height": "calc(100% - 11px)",
};
var s_vteWindowRight = {
  "float": "right",
  "padding": "5px 5px 5px 5px",
  // Set width as 80% minus padding, borders, etc
  "width": "79%",
  "width": "-moz-calc(80% - 10px)",    // Firefox
  "width": "-webkit-calc(80% - 10px)", // Webkit
  "width": "-o-calc(80% - 10px)",      // Opera
  "width": "calc(80% - 10px)",         // Standard
  // Same as width, height is 100% minus border, padding, etc
  "height": "-moz-calc(100% - 21px)",   
  "height": "-webkit-calc(100% - 21px)",
  "height": "-o-calc(100% - 21px)",
  "height": "calc(100% - 21px)",
};
var s_vteWindowLeftProject = {
  "float": "left",
  "width": "100%",
  "height": "15%",
  "background-color": "#F9F9F9"
};
var s_vteWindowLeftNav = {
  "float": "left",
  "width": "100%",
  "height": "80%",
  "background-color": "#FFFFFF",
  "font-family": s_wpFont,
  "font-size": "12px",
  "padding": "10px 0px",
};
var s_vteWindowRightTitle = {
  "float": "left",
  "width": "100%",
  "min-height": "10px",
  "background-color": "#F9F9F9",
  "border-bottom": "1px solid #EEE"
};
var s_vteWindowRightTool = {
  "float": "left",
  "width": "100%",
  "background-color": "#F9F9F9",
  "border-bottom": "1px solid #000"
};
var s_vteWindowRightContent = {
  "float": "left",
  "width": "100%",
  "height": "95%",
  "background-color": "#FFFFFF",
  "overflow-y": "auto",
  //"padding": "10px"
};
var s_vteWindowRightContentSummary = {
  "font-family": s_wpFont,
  "font-size": "13px",
};
var s_vteWindowRightContentSummaryGraph = {
  "font-family": s_wpFont,
  "margin": "8px",
  "border": "1px solid #EEEEEE",
  "height": "112px",
};
var s_vteWindowRightContentSummaryPages = {
  "font-family": s_wpFont,
  "margin": "8px",
  "border": "1px solid #EEEEEE",
  "height": "130px",
};
var s_vteWindowRightContentSummaryNew = {
  "font-family": s_wpFont,
  "margin": "8px",
  "border": "1px solid #EEEEEE",
};
var s_vteMembersContributionEdits = {
  "font-family": s_wpFont,
  "font-size": "10px",
  "margin": "8px",
  "border": "1px solid #EEEEEE",
  "height": "250px",
};
var s_vteLoadingText = {
  "margin-top": "20px",
  "color": "#848484",
  "-webkit-animation-name": "glow", 
  "-webkit-animation-duration": "1s", 
  "-webkit-animation-iteration-count": "infinite", 
  "-webkit-animation-direction": "alternate", 
  "-webkit-animation-timing-function": "ease-in-out", 
  "-moz-animation-name": "glow", 
  "-moz-animation-duration": "1s", 
  "-moz-animation-iteration-count": "infinite", 
  "-moz-animation-direction": "alternate", 
  "-moz-animation-timing-function": "ease-in-out", 
  "-o-animation-name": "glow", 
  "-o-animation-duration": "1s", 
  "-o-animation-iteration-count": "infinite", 
  "-o-animation-direction": "alternate", 
  "-o-animation-timing-function": "ease-in-out", 
  "animation-name": "glow", 
  "animation-duration": "1s", 
  "animation-iteration-count": "infinite", 
  "animation-direction": "alternate", 
  "animation-timing-function": "ease-in-out"
};
var s_vteTitle = {
  "font-family": s_wpFont,
  "font-size": "20px",
  "font-weight": "normal",
  "float": "left",
  "padding": "5px 0px 5px 10px"
};
var s_vteTitleActions = {
  "float": "right",
  "padding": "0px 10px 0px 0px",
};
var s_vteTitleAction = {
  "float": "left",
  "font-family": s_wpFont,
  "font-size": "12px",
  "cursor": "pointer",
  "padding": "5px 0px 0px 5px",
};
var s_vteProjectSelect = {
  "display": "block",
  "width": "-moz-calc(100% - 12px)",    // Firefox
  "width": "-webkit-calc(100% - 12px)", // Webkit
  "width": "-o-calc(100% - 12px)",      // Opera
  "width": "calc(100% - 12px)",         // Standard
  "height": "1.4em",
  "position": "relative",
  "border": "1px solid #AAA",
  "background-color": "#FFF",
  "margin": "5px 5px 0px 5px",
};
var s_vteProjectSelectLabel = {
  "font-family": s_wpFont,
  "font-size": "12px",
  "padding": "5px 0px 0px 7px",
};
var s_vteProjectSelectInput = {
  "position": "absolute",
  "margin": "0",
  "border": "0",
  "height": "1.4em",
  "background-color": "transparent",
  "color": "#000",
  "outline": "none",
  "font-family": s_wpFont,
  "font-size": "12px",
  "padding": "2px",
};
var s_vteProjectSelectMulti = {
  "position": "absolute",
  "width": "250px",
  "max-height": "80%",
  "overflow-y": "auto",
  "margin-left": "5px",
  "padding": "7px 10px",
  "background-color": "#EEE",
  "border-bottom-left-radius": "10px",
  "border-bottom-right-radius": "10px",
  "-moz-border-bottom-left-radius": "10px",
  "-moz-border-bottom-right-radius": "10px",
};
var s_vteProjectSelectMultiProj = {
  "font-family": s_wpFont,
  "font-size": "12px",
  "cursor": "pointer",
  "padding-left": "1em",
  "text-indent": "-1em",
  "padding-bottom": "4px",
};



// Only load vte on the Wikipedia namespace (may limit to project pages later)
//if (mw.config.get('wgCanonicalNamespace') === 'Wikipedia') {
  console.log("Loading VTE")
  mw.loader.using( ["mediawiki.api"], function() {
    $(vte.initialize);
  });
//}