<commit_before>{{### response.files.append("/curator/static/js/d3.v3.js") }}
{{ response.files.append("/curator/static/js/d3.v3.min.js") }}
{{extend 'layout.html'}}
{{import urllib}}
{{from pprint import pprint}}
{{### Add support scripts for parsing ISO-8601 dates
  #response.files.append(URL('static','js/moment.min.js'))
}}
<style type="text/css">
.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}
.bar {
  fill: steelblue;
}
.x.axis path {
  display: none;
}
.y.axis .tick line {
    stroke: #000;
    opacity: 0.15;
}
.y.axis path.domain {
    fill: none;
}
.highlight-details-box {
    stroke: #ccc;
    fill: #fff;
    opacity: 0.8;
}
.highlight-details-title {
    font-size: 12px;
    font-weight: bold;
    stroke: none;
    fill: #000;
}
.highlight-details-stat {
    font-size: 12px;
    font-weight: normal;
    fill: #333;
}

.event-markers .synth-release-marker,
.event-markers .taxo-release-marker {
    visibility: hidden;
}
circle.synth-release-marker {
    fill: #939;
}
circle.taxo-release-marker {
    fill: #39c;
}
.event-markers .date-with-synthesis circle.synth-release-marker,
.event-markers .date-with-synthesis text.synth-release-marker {
    visibility: visible;
}
.event-markers .date-with-phylesystem circle.taxo-release-marker,
.event-markers .date-with-phylesystem text.taxo-release-marker  {
    visibility: visible;
}
.event-markers text {
    fill: #fff;
    font-size: 8px;
    cursor: pointer;
}
.event-marker:hover circle {
    stroke: red;
    stroke-width: 2px;
    stroke-opacity: 0.5;
}
*/
</style>
<div class="container">
      <h1 id="main-title">Open Tree release history</h1>
      <div class="row">
          <div class="span10 offset1">
              <p>
              The Open Tree of Life project includes periodic releases of the
              <a href="/about/synthesis-release" target="_blank">OTT (Open Tree taxonomy)</a> and
              the <a href="/about/taxonomy-release" target="_blank">latest synthetic tree</a>.
              The timeline below shows these releases and the result&mdash;an 
              increase in OTUs (operating taxonomic units) in our synthesis.
              <br/>
              <a id="warnings-toggle" onclick="$('#warnings-panel').toggle(); return false;"
                 style="font-weight:bold; color:red; background-color: #fdd;" href="#">
                 &nbsp;CAUTION: FAKE DATA BELOW (click for details)&nbsp;
               </a>
              </p>
              <p id="highlight-prompt" style="font-style: italic;">&nbsp;</p>
          </div>
      </div>
      <div id="warnings-panel" 
           style="display: none; background-color: #fdd; padding: 1em; border: 1px solid #fcc;"></div>

</div><!-- end of .container -->

      <div id="stacked-bar-div" />
<script type="text/javascript">
// pass statistics as JSON to client for stepping, plotting, etc.
// NOTE that these are correctly date-sorted from the server!
var otu_stats = {{= XML(otu_stats) }}

// jimA: TEMPORARILY restricting this to just a few samples (for review)
//otu_stats = otu_stats.slice(0,4);

var sortedSynthesisProfileKeys = null;
var currentSynthesisProfileIndex = 0;
var profilesByDate = {};
var i;
for (i = 0; i < otu_stats.length ; ++i) {
    var profileDate = otu_stats[i]['Date'];
    profilesByDate[profileDate] = otu_stats[i];
}
var warnings = {{= XML(warnings)}};
for (i = 0; i < warnings.length; ++i) {
    $('#warnings-panel').append("<p style=\"font-weight:bold;color:red\">" + warnings[i] + "</p>");
}
/* svg based on http://bl.ocks.org/mbostock/3886208 */
var margin = {top: 10, right: 20, bottom: 90, left: 40},
    outerWidth = 960,
    outerHeight = 500,
    width = outerWidth - margin.left - margin.right,
    height = outerHeight - margin.top - margin.bottom;
var x = d3.time.scale.utc()
    .range([margin.left, width]);
var y = d3.scale.linear()
    .rangeRound([height, margin.top]);
// let's define two color ranges, with a common endpoint
var darkest = d3.rgb('#666666'),  // represents OTUs from studies in synthesis
    white = d3.rgb('#ffffff'),
    warm = d3.rgb('#cc9966'),
    cool = d3.rgb('#6677BB'),
    warmScale = d3.interpolateRgb(white, warm),
    coolScale = d3.interpolateRgb(cool, white);  // not currently used!


// create the final color set
var colors = d3.scale.ordinal()
    //.domain([0, 1, 2, 3])  // set this explicitly to avoid weirdness below!
    // Set domain to property names, to support area chart?
    .range([warmScale(0.85), warmScale(0.6), warmScale(0.25)]);

var colorCount = colors.range().length; // used below to get color index
var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");
var yAxis = d3.svg.axis()
    .scale(y)
    .orient("right")
    //.ticks(10)
    .innerTickSize(width)
    .tickFormat(d3.format(".2s"));
var svgpar = d3.select("#stacked-bar-div");
var svg = svgpar.append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var dup_otu_stats = [];
var i;
for (i = 0; i < otu_stats.length; ++i) {
    dup_otu_stats[2*i] = $.extend({}, otu_stats[i]);
    dup_otu_stats[2*i + 1] = $.extend({}, otu_stats[i]);
}

// allow "pinning" the highlight to a particular date (vs. following the mouse)
var highlightPinned = false;
function pinHighlight() {
    highlightPinned = true;
    // update the UI for this
    //$('#unpin-highlight-widget').css('opacity', 1.0);
    $('#highlight-prompt').html('Click the chart below to see details for any date. Click \
              <a id="unpin-highlight-widget" class="badge" style="Xbackground-color: #f88;" \
                 href="#" title="" onclick="unpinHighlight(); $(this).blur(); return false;"> \
                 <span class="widget-prompt">Unlock highlight</span> \
        </a> to follow the mouse.');
}
function unpinHighlight() {
    highlightPinned = false;
    //$('#unpin-highlight-widget').css('opacity', 0.25);
    $('#highlight-prompt').html("Move the mouse below to see details for any date. Click to keep the highlight there.");
}

var populateSVG = function(data) {
  // pre-fetch the highest Y to support alternate stacking orders
  var maxOTUs = d3.max(data, function(d) { return d['Unique OTUs in OTT']; });

  // Augment each profile with the properties we actually want to show, using the function for each
  var mutuallyExclusiveCategories = {
    'OTUs in synthesis': function(d) {
        return d['Unique OTUs in synthesis from studies'];
    },
    'OTUs in phylesystem, but not in synthesis': function(d) {
        return d['Unique OTUs in studies'] - d['Unique OTUs in synthesis from studies'];
    },
    'OTUs in taxonomy, but not in phylesystem or synthesis': function(d) {
        return d['Unique OTUs in OTT'] - d['Unique OTUs in studies'] - d['Unique OTUs in synthesis from studies'];
    }
  };
  // These property names should be the domain for our colors above
  colors.domain(d3.keys(mutuallyExclusiveCategories));

  data.forEach(function(d) {
    d3.keys(mutuallyExclusiveCategories).forEach(function(propName) {
        var func = mutuallyExclusiveCategories[propName];
        d[propName] = func(d);
    });
  });
  // N.B. These are PRE-sorted in chronological order!
  //data.sort(function(a, b) { return b.Date > a.Date; });

  // convert to stacked data format
  var stack = d3.layout.stack()
      .values(function(d) { return d.values; }); // unpack structure below

  // Gathers the [named] properties for each profile, by key
  var gatheredProperties = d3.keys(mutuallyExclusiveCategories).map(function(name) {
    return {
        name: name,
        values: data.map(function(d) {
            return { date: new Date(moment.utc(d.Date)), y: d[name] };
        })
    }
  });
  var otuStates = stack(gatheredProperties);

  // render X axis under data (to anchor dates)
  var earliestDate = new Date(moment.utc(data[0].Date));
  var latestDate = new Date(moment.utc(data[data.length - 1].Date));
  x.domain([earliestDate, latestDate]);
  y.domain([0, maxOTUs]);

  /*
  // render stacked data blocks
  var profile = svg.selectAll(".profile")
      .data(data)
    .enter().append("g")
      .attr("class", "g")
      .attr("transform", function(d) {
        return "translate(" + x(formatISODate(d.Date)) + ",0)";
      });
  profile.selectAll("rect")
      .data(function(d) { return d; })
    .enter().append("rect")
      .attr("x", function(d, i) {
              return x.rangeBand()/7.0;
       })
      .attr("width", x.rangeBand()*5.0/7.0)
      .attr("y", function(d) {
             return y(d.y0);
       })
      .attr("height", function(d) { return y(d.y1) - y(d.y0); })
      .style("fill", function(d, i) { 
          var nextColor = colors(i+1);
          console.log('big bar '+i+', color='+ nextColor); 
          return nextColor; 
       });
   */
  // render area chart
  var area = d3.svg.area()
      .x(function(d) { return x(d.date); })
      .y0(function(d) { return y(d.y0); })
      .y1(function(d) { return y(d.y0 + d.y); });
  var profile = svg.selectAll(".profile")
      .data(otuStates)
    .enter().append("path")
      .attr("class", "area")
      .attr("d", function(d) { return area(d.values); })
      .style("fill", function(d) { return colors(d.name); });

  // render axes on top of data
  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);
  var gy = svg.append("g")
      .attr("class", "y axis")
      .call(yAxis);
  gy.selectAll("text")
      .attr("x", 4)
      .attr("dy", -4);
  gy.append("text")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", "-1.25em")
      .style("text-anchor", "end")
      .text("OTUs in Open Tree Taxonomy");

  // add a moving highlight to spotlight single dates
  var highlight = svg.append("g")
      .attr("class", "highlight")
      .attr("transform", "translate(-1000, "+ margin.top +")");
  highlight.append("rect")
      .attr("x", -1)
      .attr("width", 2)
      .attr("height", height + margin.bottom)
      .attr("opacity", 0.5)
      .style("fill", "#f00");
  var highlightDetails = highlight.append("g")
      .attr("class", "highlight-details");
  var highlightDetailsBox = highlightDetails.append("rect")
      .attr("class", "highlight-details-box")
      .attr("height", 40) // reset below
      .attr("width", 420);
  var highlightDetailsTitle = highlightDetails.append("text")
      .attr("class", "highlight-details-title")
      .attr("x", 8)
      .attr("y", 16)
      .text('foo');
  var highlightDetailsStats = highlightDetails.append("g");
  // add a legend block and display text field for each property
  var currentY = 34;
  d3.keys(mutuallyExclusiveCategories).reverse().forEach(function(propName) {
      highlightDetailsStats.append("rect")
          .attr("x", 8)
          .attr("y", currentY - 9)
          .attr("width", 18)
          .attr("height", 18)
          .style("fill", colors(propName));
      highlightDetailsStats.append("text")
          .attr("class", "highlight-details-stat")
          .attr("x", 30)
          .attr("y", currentY)
          .attr("dy", ".35em")
          .style("text-anchor", "start")
          .text('This is a test');
      currentY += 24;
  });
  // add optional markers for synthesis and/or taxonomy release
  var markerRadius = 8;
  var highlightMarkerFirstRowY = currentY;
  var highlightMarkerSecondRowY = currentY + 20;
  highlightDetailsStats.append("circle")
      .attr("class", "synth-release-marker")
      .style("visibility", "visible")
      .attr("cx", 17)
      .attr("cy", highlightMarkerFirstRowY)
      .attr("r", markerRadius)
    .append("svg:title")
      .text("Synthesis release (click to see details in a new window).");
  highlightDetailsStats.append('text')
      .attr("class", "synth-release-marker")
      .style("fill", "#fff")
      .text("S")
      .attr("x", 12.5)
      .attr("y", highlightMarkerFirstRowY)
      .attr("dy", ".35em")
    .append("svg:title")
      .text("Synthesis release (click to see details in a new window).");
  highlightDetailsStats.append("text")
      .attr("class", "synth-release-marker highlight-details-stat")
      .style("visibility", "visible")
      .attr("x", 17 + markerRadius + 4)
      .attr("y", highlightMarkerFirstRowY)
      .attr("dy", ".32em")
      .text("Synthesis release (click marker below for details)");

  highlightDetailsStats.append("circle")
      .attr("class", "taxo-release-marker")
      .style("visibility", "visible")
      .attr("cx", 17)
      .attr("cy", highlightMarkerFirstRowY)
      .attr("r", markerRadius);
  highlightDetailsStats.append('text')
      .attr("class", "taxo-release-marker")
      .style("fill", "#fff")
      .text("T")
      .attr("x", 13)
      .attr("y", highlightMarkerFirstRowY)
      .attr("dy", ".4em")
    .append("svg:title")
      .text("Synthesis release (click to see details in a new window).");
  highlightDetailsStats.append("text")
      .attr("class", "taxo-release-marker highlight-details-stat")
      .style("visibility", "visible")
      .attr("x", 17 + markerRadius + 4)
      .attr("y", highlightMarkerFirstRowY)
      .attr("dy", ".32em")
      .text("Taxonomy release (click marker below for details)");

  // render event markers below X axis (for releases, updates, etc)
  var markers = svg.append("g")
      .attr("class", "event-markers")
  .selectAll(".event-marker")
    .data(data)
    .enter().append("g")
      .attr("class", function(d,i) {
          className = 'event-marker';
          if (d['Date has synthesis release'] === 'true') {
              className += ' date-with-synthesis';
          }
          if (d['Date has phylesystem info'] === 'true') {
              className += ' date-with-phylesystem';
          }
          return className;
      })
      .attr("transform", function(d,i) {
          var markerX = x( new Date(moment.utc( d.Date)) );
          var markerY = (height+30);
          var translate = ("translate("+ markerX +"," + markerY + ")");
          return translate;
      });

  var markerRadius = 5;

  markers.append("svg:a")
      .attr("target", "_blank")
      .attr("xlink:href", function(d){ return ("/about/synthesis-release/"+ d.Date); })
    .append("circle")
      .attr("class", "synth-release-marker")
      .attr("cx", 0)
      .attr("cy", 0)
      .attr("r", markerRadius)
    .append("svg:title")
      .text("Synthesis release (click to see details in a new window).");
  markers.append("svg:a")
      .attr("target", "_blank")
      .attr("xlink:href", function(d){ return ("/about/synthesis-release/"+ d.Date); })
    .append('text')
      .attr("class", "synth-release-marker")
      .text("S")
      .attr("x", -2.5)
      .attr("dy", 3)
    .append("svg:title")
      .text("Synthesis release (click to see details in a new window).");
  markers.append("svg:a")
      .attr("target", "_blank")
      .attr("xlink:href", function(d){ return ("/about/taxonomy-release/"+ d.Date); })
    .append("circle")
      .attr("class", "taxo-release-marker")
      .attr("cx", 0)
      .attr("cy", 15)
      .attr("r", markerRadius)
    .append("svg:title")
      .text("Taxonomy release (click to see details in a new window).");
  markers.append("svg:a")
      .attr("target", "_blank")
      .attr("xlink:href", function(d){ return ("/about/taxonomy-release/"+ d.Date); })
    .append("text")
      .attr("class", "taxo-release-marker")
      .text('T')
      .attr("x", -2.5)
      .attr("dy", 18)
    .append("svg:title")
      .text("Taxonomy release (click to see details in a new window).");

  // make a bisector that splits on any date (used below)
  var bisectDate = d3.bisector(function(d) { return new Date(moment.utc(d.Date)); }).right;

  // highlight should respond to mouse movement (or incoming date on query string)
  svg.on("click", function() {
    // clicking forces a pin in the new place
    unpinHighlight();

    // create a synthetic event on the highlight
    var mouse = d3.mouse(this);
    var evt = document.createEvent("MouseEvents");
    evt.initMouseEvent(
        "mousemove",
        true,       // bubbles?
        true,       // cancelable?
        window,     // view
        0,          // detail (unused)
        0,          // screenX (ignored)
        0,          // screenY (ignored)
        event.x,    // clientX (copy from click event)
        event.y,    // clientY (copy from click event)
        false,      // ctrlKey
        false,      // altKey
        false,      // shiftKey
        false,      // metaKey
        0,          // button
        svg[0][0]   // relatedTarget
    );

    // "unwrap" the funky d3 selection and dispatch the mousemove
    svg[0][0].dispatchEvent(evt);
    pinHighlight();
  });

  svg.on("mousemove", function() {
    if (highlightPinned) { 
        // pinning stops this behavior
        return;
    };

    var mouse = d3.mouse(this);
    var localX = mouse[0]; //  + margin.left;
    // find the nearest date and snap to its X coordinate
    var nearestDate, nearestProfileData;
    var mouseDate = x.invert(localX);
    var nextDatePos = bisectDate( data, mouseDate );
    if (nextDatePos === data.length) {
        // we're at the end of the date range
        var lastDatePos = nextDatePos - 1;
        nearestDate = new Date(moment.utc( data[lastDatePos].Date ));
        nearestProfileData = data[lastDatePos];
    } else {
        // split is somewhere inside the date range
        var nextDate = new Date(moment.utc( data[nextDatePos].Date ));
        var previousDatePos = Math.max(0, nextDatePos - 1);
        var previousDate = new Date(moment.utc( data[previousDatePos].Date ));
        // which is nearest?
        var previousSnapDistance = mouseDate - previousDate;
        var nextSnapDistance = nextDate - mouseDate;
        if (nextSnapDistance > previousSnapDistance) {
            nearestDate = previousDate;
            nearestProfileData = data[previousDatePos];
        } else {
            nearestDate = nextDate;
            nearestProfileData = data[nextDatePos];
        }
    }

    highlightDate( nearestDate, nearestProfileData );
  });

  // nested function, to use highlight and other instances
  function highlightDate( targetDate, profileData ) {
      if (typeof(targetDate) === 'string') {
          // fetch the expected Date and data
          if (!profileData) {
              // try to retrieve data
              var matchingProfiles = data.filter(function(d) {return d.Date === targetDate});
              if (matchingProfiles.length === 0) {
                alert("No data was found for this date: "+ targetDate);
                return;
              }
              profileData = matchingProfiles[0];
          }
          targetDate = new Date(moment.utc(targetDate));
      }

      var snapX = x(targetDate); // find X coordinates for the nearest date
      // move the highlight and update its displayed stats
      highlight.attr("transform", "translate("+ snapX +","+ margin.top +")");
      highlightDetailsTitle.text(targetDate);

      var statPosition = 0; 
      var statsFields = highlightDetailsStats.selectAll('text');
      d3.keys(mutuallyExclusiveCategories).reverse().forEach(function(propName) {
          var func = mutuallyExclusiveCategories[propName];
          var displayText = propName +": "+ func(profileData);
          var itsField = statsFields.filter(function(d,i) {return i === statPosition;});
          itsField.text(displayText);
          statPosition++;
      });

      // show markers for synthesis and/or taxonomy release, IF found for this date
      var dateHasSynthRelease = (profileData['Date has synthesis release'] === 'true');
      var dateHasTaxoRelease = (profileData['Date has phylesystem info'] === 'true');
      if (dateHasSynthRelease && dateHasTaxoRelease) {
          // shift the taxo-release markers down to second row
          highlightDetailsStats.select('circle.taxo-release-marker')
              .attr("cy", highlightMarkerSecondRowY);
          highlightDetailsStats.selectAll('text.taxo-release-marker')
              .attr("y", highlightMarkerSecondRowY);
      } else {
          // shift the taxo-release markers up to first row
          highlightDetailsStats.select('circle.taxo-release-marker')
              .attr("cy", highlightMarkerFirstRowY);
          highlightDetailsStats.selectAll('text.taxo-release-marker')
              .attr("y", highlightMarkerFirstRowY);
      }
      highlightDetailsStats.selectAll('.synth-release-marker')
          .style('visibility', function() {
              return (dateHasSynthRelease ? 'visible' : 'hidden');
          });
      highlightDetailsStats.selectAll('.taxo-release-marker')
          .style('visibility', function() {
              return (dateHasTaxoRelease ? 'visible' : 'hidden');
          });

      // set box height according to optional markers above
      highlightDetailsBox.attr('height', function() {
          if (dateHasSynthRelease && dateHasTaxoRelease) {
              return 140;
          }
          if (dateHasSynthRelease || dateHasTaxoRelease) {
              return 120;
          }
          return 100;
      });

      // should we put the details box on left or right (default)?
      var detailsWidth = highlightDetailsBox.attr('width');
      if (snapX > (width - detailsWidth)) {
          highlightDetails.attr("transform", "translate(-"+ (Number(detailsWidth) + 8) +", 100)");
      } else {
          highlightDetails.attr("transform", "translate(8,100)");
      }
  }

  // expose these methods for outside callers
  populateSVG.highlightDate = highlightDate;
};



$(document).ready(function() {
    populateSVG(otu_stats);

    // highlight incoming date/version, or the latest if not found
    var featuredDate = getParameterByName('highlight');
    var pinInitialHighlight = false;
    if (featuredDate) {
        // stick to this date until user clicks elsewhere
        pinInitialHighlight = true;
    } else {
        // set initial highlight on latest date, but allow easy exploration
        featuredDate = otu_stats[otu_stats.length - 1].Date;  // eg, '2014-11-05T19Z'
    }
    populateSVG.highlightDate( featuredDate );
    if (pinInitialHighlight) {
        pinHighlight();
    }
});

function getParameterByName(name) {
    // Retrieves query-string vars by name (or null)
    // from http://stackoverflow.com/a/5158301
    var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
    return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}

function formatISODate( dateString, options ) {
    options = options || {includeTime: true};
    var aDate = new moment(dateString);
    // see http://momentjs.com/docs/#/parsing/string/
    if (options.includeTime) {
        return aDate.format('MMMM Do YYYY, hA');
    } else {
        return aDate.format('MMMM Do YYYY');
    }
}

function showCurrentSynthesisProfile() {
    var currentSynthesisProfile = profilesByDate[currentSynthesisProfileDate];
    // Our stats JSON has friendly property names. Just format them!
    $('#otu-stats-panel table').empty();
    for (var prop in currentSynthesisProfile) {
        if (prop === 'Date' || prop == 'blocks' || prop == 'total') continue;
        var itsValue = currentSynthesisProfile[ prop ];
        $('#otu-stats-panel table').append(
            '<tr>'
              + '<th>'+ prop +'</th>'
              + '<td>'+ itsValue +'</td>'
              // for sparkline specs, see http://style.org/chartapi/sparklines/
              + '<td>'
                  + '<img src="http://chart.apis.google.com/chart?'
                    + 'cht=lc'
                    + '&chs=50x20'
                    + '&chd=t:'+ getSynthesisSparklineData(prop, currentSynthesisProfileDate)
                    + '&chco=336699'
                    + '&chls=1,1,0'
                    + '&chm=o,990000,0,20,4'
                    + '&chxt=r,x,y'
                    + '&chxs=0,990000,11,0,_|1,990000,1,0,_|2,990000,1,0,_'
                    + '&chxl=0:||1:||2:||'
                  + '" title="Previous history for this statistic">'
              +'</td>'
          + '</tr>'
        );
    }

    // Update the Next and Prev buttons (disable if no more)
    var nthProfile = sortedSynthesisProfileKeys.indexOf(currentSynthesisProfileDate);
    if (nthProfile === 0) {
        $('#prev-otu-profile').addClass('disabled');
    } else {
        $('#prev-otu-profile').removeClass('disabled');
    }
    if (nthProfile === (sortedSynthesisProfileKeys.length - 1)) {
        $('#next-otu-profile').addClass('disabled');
    } else {
        $('#next-otu-profile').removeClass('disabled');
    }

    // Update the date selector
    $('#otu-profile-date-chooser').val(currentSynthesisProfileDate);
}

function getSynthesisSparklineData(prop, lastProfileDate) {
    // return a comma-delimited data series (string) for plotting
    // NOTE that these should be decimal numbers between 0 and 100, so scale accordingly
    //   EXAMPLE: '5.3,26.5,15.9,31.7,42.3,21.2'
    var series = [];
    $.each(sortedSynthesisProfileKeys, function(i, profileDate) {
        // skip more recent profiles, if we're looking at something older
        if (profileDate <= lastProfileDate) {
             series.push( profilesByDate[profileDate][prop] );
        }
    });

    // normalize all values in the series to the range 0.0 to 100.0
    var minValue = Math.min.apply(Math, series);
    var maxValue = Math.max.apply(Math, series);
    // move all values above zero
    if (minValue < 0) {
        series = $.map(series, function(d) {
            return d - minValue;
        });
    }
    // scale all values to under 100 (using 90 to avoid cropping)
    var scaleToFit = 90.0 / maxValue;
    series = $.map(series, function(d) {
        return d * scaleToFit;
    });

    return series.join(',');
}

</script>
<commit_msg>Fix reversed links to synth+taxo release pages.<commit_after>{{### response.files.append("/curator/static/js/d3.v3.js") }}
{{ response.files.append("/curator/static/js/d3.v3.min.js") }}
{{extend 'layout.html'}}
{{import urllib}}
{{from pprint import pprint}}
{{### Add support scripts for parsing ISO-8601 dates
  #response.files.append(URL('static','js/moment.min.js'))
}}
<style type="text/css">
.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}
.bar {
  fill: steelblue;
}
.x.axis path {
  display: none;
}
.y.axis .tick line {
    stroke: #000;
    opacity: 0.15;
}
.y.axis path.domain {
    fill: none;
}
.highlight-details-box {
    stroke: #ccc;
    fill: #fff;
    opacity: 0.8;
}
.highlight-details-title {
    font-size: 12px;
    font-weight: bold;
    stroke: none;
    fill: #000;
}
.highlight-details-stat {
    font-size: 12px;
    font-weight: normal;
    fill: #333;
}

.event-markers .synth-release-marker,
.event-markers .taxo-release-marker {
    visibility: hidden;
}
circle.synth-release-marker {
    fill: #939;
}
circle.taxo-release-marker {
    fill: #39c;
}
.event-markers .date-with-synthesis circle.synth-release-marker,
.event-markers .date-with-synthesis text.synth-release-marker {
    visibility: visible;
}
.event-markers .date-with-phylesystem circle.taxo-release-marker,
.event-markers .date-with-phylesystem text.taxo-release-marker  {
    visibility: visible;
}
.event-markers text {
    fill: #fff;
    font-size: 8px;
    cursor: pointer;
}
.event-marker:hover circle {
    stroke: red;
    stroke-width: 2px;
    stroke-opacity: 0.5;
}
*/
</style>
<div class="container">
      <h1 id="main-title">Open Tree release history</h1>
      <div class="row">
          <div class="span10 offset1">
              <p>
              The Open Tree of Life project includes periodic releases of the
              <a href="/about/taxonomy-release" target="_blank">OTT (Open Tree taxonomy)</a> and
              the <a href="/about/synthesis-release" target="_blank">latest synthetic tree</a>.
              The timeline below shows these releases and the result&mdash;an 
              increase in OTUs (operating taxonomic units) in our synthesis.
              <br/>
              <a id="warnings-toggle" onclick="$('#warnings-panel').toggle(); return false;"
                 style="font-weight:bold; color:red; background-color: #fdd;" href="#">
                 &nbsp;CAUTION: FAKE DATA BELOW (click for details)&nbsp;
               </a>
              </p>
              <p id="highlight-prompt" style="font-style: italic;">&nbsp;</p>
          </div>
      </div>
      <div id="warnings-panel" 
           style="display: none; background-color: #fdd; padding: 1em; border: 1px solid #fcc;"></div>

</div><!-- end of .container -->

      <div id="stacked-bar-div" />
<script type="text/javascript">
// pass statistics as JSON to client for stepping, plotting, etc.
// NOTE that these are correctly date-sorted from the server!
var otu_stats = {{= XML(otu_stats) }}

// jimA: TEMPORARILY restricting this to just a few samples (for review)
//otu_stats = otu_stats.slice(0,4);

var sortedSynthesisProfileKeys = null;
var currentSynthesisProfileIndex = 0;
var profilesByDate = {};
var i;
for (i = 0; i < otu_stats.length ; ++i) {
    var profileDate = otu_stats[i]['Date'];
    profilesByDate[profileDate] = otu_stats[i];
}
var warnings = {{= XML(warnings)}};
for (i = 0; i < warnings.length; ++i) {
    $('#warnings-panel').append("<p style=\"font-weight:bold;color:red\">" + warnings[i] + "</p>");
}
/* svg based on http://bl.ocks.org/mbostock/3886208 */
var margin = {top: 10, right: 20, bottom: 90, left: 40},
    outerWidth = 960,
    outerHeight = 500,
    width = outerWidth - margin.left - margin.right,
    height = outerHeight - margin.top - margin.bottom;
var x = d3.time.scale.utc()
    .range([margin.left, width]);
var y = d3.scale.linear()
    .rangeRound([height, margin.top]);
// let's define two color ranges, with a common endpoint
var darkest = d3.rgb('#666666'),  // represents OTUs from studies in synthesis
    white = d3.rgb('#ffffff'),
    warm = d3.rgb('#cc9966'),
    cool = d3.rgb('#6677BB'),
    warmScale = d3.interpolateRgb(white, warm),
    coolScale = d3.interpolateRgb(cool, white);  // not currently used!


// create the final color set
var colors = d3.scale.ordinal()
    //.domain([0, 1, 2, 3])  // set this explicitly to avoid weirdness below!
    // Set domain to property names, to support area chart?
    .range([warmScale(0.85), warmScale(0.6), warmScale(0.25)]);

var colorCount = colors.range().length; // used below to get color index
var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");
var yAxis = d3.svg.axis()
    .scale(y)
    .orient("right")
    //.ticks(10)
    .innerTickSize(width)
    .tickFormat(d3.format(".2s"));
var svgpar = d3.select("#stacked-bar-div");
var svg = svgpar.append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var dup_otu_stats = [];
var i;
for (i = 0; i < otu_stats.length; ++i) {
    dup_otu_stats[2*i] = $.extend({}, otu_stats[i]);
    dup_otu_stats[2*i + 1] = $.extend({}, otu_stats[i]);
}

// allow "pinning" the highlight to a particular date (vs. following the mouse)
var highlightPinned = false;
function pinHighlight() {
    highlightPinned = true;
    // update the UI for this
    //$('#unpin-highlight-widget').css('opacity', 1.0);
    $('#highlight-prompt').html('Click the chart below to see details for any date. Click \
              <a id="unpin-highlight-widget" class="badge" style="Xbackground-color: #f88;" \
                 href="#" title="" onclick="unpinHighlight(); $(this).blur(); return false;"> \
                 <span class="widget-prompt">Unlock highlight</span> \
        </a> to follow the mouse.');
}
function unpinHighlight() {
    highlightPinned = false;
    //$('#unpin-highlight-widget').css('opacity', 0.25);
    $('#highlight-prompt').html("Move the mouse below to see details for any date. Click to keep the highlight there.");
}

var populateSVG = function(data) {
  // pre-fetch the highest Y to support alternate stacking orders
  var maxOTUs = d3.max(data, function(d) { return d['Unique OTUs in OTT']; });

  // Augment each profile with the properties we actually want to show, using the function for each
  var mutuallyExclusiveCategories = {
    'OTUs in synthesis': function(d) {
        return d['Unique OTUs in synthesis from studies'];
    },
    'OTUs in phylesystem, but not in synthesis': function(d) {
        return d['Unique OTUs in studies'] - d['Unique OTUs in synthesis from studies'];
    },
    'OTUs in taxonomy, but not in phylesystem or synthesis': function(d) {
        return d['Unique OTUs in OTT'] - d['Unique OTUs in studies'] - d['Unique OTUs in synthesis from studies'];
    }
  };
  // These property names should be the domain for our colors above
  colors.domain(d3.keys(mutuallyExclusiveCategories));

  data.forEach(function(d) {
    d3.keys(mutuallyExclusiveCategories).forEach(function(propName) {
        var func = mutuallyExclusiveCategories[propName];
        d[propName] = func(d);
    });
  });
  // N.B. These are PRE-sorted in chronological order!
  //data.sort(function(a, b) { return b.Date > a.Date; });

  // convert to stacked data format
  var stack = d3.layout.stack()
      .values(function(d) { return d.values; }); // unpack structure below

  // Gathers the [named] properties for each profile, by key
  var gatheredProperties = d3.keys(mutuallyExclusiveCategories).map(function(name) {
    return {
        name: name,
        values: data.map(function(d) {
            return { date: new Date(moment.utc(d.Date)), y: d[name] };
        })
    }
  });
  var otuStates = stack(gatheredProperties);

  // render X axis under data (to anchor dates)
  var earliestDate = new Date(moment.utc(data[0].Date));
  var latestDate = new Date(moment.utc(data[data.length - 1].Date));
  x.domain([earliestDate, latestDate]);
  y.domain([0, maxOTUs]);

  /*
  // render stacked data blocks
  var profile = svg.selectAll(".profile")
      .data(data)
    .enter().append("g")
      .attr("class", "g")
      .attr("transform", function(d) {
        return "translate(" + x(formatISODate(d.Date)) + ",0)";
      });
  profile.selectAll("rect")
      .data(function(d) { return d; })
    .enter().append("rect")
      .attr("x", function(d, i) {
              return x.rangeBand()/7.0;
       })
      .attr("width", x.rangeBand()*5.0/7.0)
      .attr("y", function(d) {
             return y(d.y0);
       })
      .attr("height", function(d) { return y(d.y1) - y(d.y0); })
      .style("fill", function(d, i) { 
          var nextColor = colors(i+1);
          console.log('big bar '+i+', color='+ nextColor); 
          return nextColor; 
       });
   */
  // render area chart
  var area = d3.svg.area()
      .x(function(d) { return x(d.date); })
      .y0(function(d) { return y(d.y0); })
      .y1(function(d) { return y(d.y0 + d.y); });
  var profile = svg.selectAll(".profile")
      .data(otuStates)
    .enter().append("path")
      .attr("class", "area")
      .attr("d", function(d) { return area(d.values); })
      .style("fill", function(d) { return colors(d.name); });

  // render axes on top of data
  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);
  var gy = svg.append("g")
      .attr("class", "y axis")
      .call(yAxis);
  gy.selectAll("text")
      .attr("x", 4)
      .attr("dy", -4);
  gy.append("text")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", "-1.25em")
      .style("text-anchor", "end")
      .text("OTUs in Open Tree Taxonomy");

  // add a moving highlight to spotlight single dates
  var highlight = svg.append("g")
      .attr("class", "highlight")
      .attr("transform", "translate(-1000, "+ margin.top +")");
  highlight.append("rect")
      .attr("x", -1)
      .attr("width", 2)
      .attr("height", height + margin.bottom)
      .attr("opacity", 0.5)
      .style("fill", "#f00");
  var highlightDetails = highlight.append("g")
      .attr("class", "highlight-details");
  var highlightDetailsBox = highlightDetails.append("rect")
      .attr("class", "highlight-details-box")
      .attr("height", 40) // reset below
      .attr("width", 420);
  var highlightDetailsTitle = highlightDetails.append("text")
      .attr("class", "highlight-details-title")
      .attr("x", 8)
      .attr("y", 16)
      .text('foo');
  var highlightDetailsStats = highlightDetails.append("g");
  // add a legend block and display text field for each property
  var currentY = 34;
  d3.keys(mutuallyExclusiveCategories).reverse().forEach(function(propName) {
      highlightDetailsStats.append("rect")
          .attr("x", 8)
          .attr("y", currentY - 9)
          .attr("width", 18)
          .attr("height", 18)
          .style("fill", colors(propName));
      highlightDetailsStats.append("text")
          .attr("class", "highlight-details-stat")
          .attr("x", 30)
          .attr("y", currentY)
          .attr("dy", ".35em")
          .style("text-anchor", "start")
          .text('This is a test');
      currentY += 24;
  });
  // add optional markers for synthesis and/or taxonomy release
  var markerRadius = 8;
  var highlightMarkerFirstRowY = currentY;
  var highlightMarkerSecondRowY = currentY + 20;
  highlightDetailsStats.append("circle")
      .attr("class", "synth-release-marker")
      .style("visibility", "visible")
      .attr("cx", 17)
      .attr("cy", highlightMarkerFirstRowY)
      .attr("r", markerRadius)
    .append("svg:title")
      .text("Synthesis release (click to see details in a new window).");
  highlightDetailsStats.append('text')
      .attr("class", "synth-release-marker")
      .style("fill", "#fff")
      .text("S")
      .attr("x", 12.5)
      .attr("y", highlightMarkerFirstRowY)
      .attr("dy", ".35em")
    .append("svg:title")
      .text("Synthesis release (click to see details in a new window).");
  highlightDetailsStats.append("text")
      .attr("class", "synth-release-marker highlight-details-stat")
      .style("visibility", "visible")
      .attr("x", 17 + markerRadius + 4)
      .attr("y", highlightMarkerFirstRowY)
      .attr("dy", ".32em")
      .text("Synthesis release (click marker below for details)");

  highlightDetailsStats.append("circle")
      .attr("class", "taxo-release-marker")
      .style("visibility", "visible")
      .attr("cx", 17)
      .attr("cy", highlightMarkerFirstRowY)
      .attr("r", markerRadius);
  highlightDetailsStats.append('text')
      .attr("class", "taxo-release-marker")
      .style("fill", "#fff")
      .text("T")
      .attr("x", 13)
      .attr("y", highlightMarkerFirstRowY)
      .attr("dy", ".4em")
    .append("svg:title")
      .text("Synthesis release (click to see details in a new window).");
  highlightDetailsStats.append("text")
      .attr("class", "taxo-release-marker highlight-details-stat")
      .style("visibility", "visible")
      .attr("x", 17 + markerRadius + 4)
      .attr("y", highlightMarkerFirstRowY)
      .attr("dy", ".32em")
      .text("Taxonomy release (click marker below for details)");

  // render event markers below X axis (for releases, updates, etc)
  var markers = svg.append("g")
      .attr("class", "event-markers")
  .selectAll(".event-marker")
    .data(data)
    .enter().append("g")
      .attr("class", function(d,i) {
          className = 'event-marker';
          if (d['Date has synthesis release'] === 'true') {
              className += ' date-with-synthesis';
          }
          if (d['Date has phylesystem info'] === 'true') {
              className += ' date-with-phylesystem';
          }
          return className;
      })
      .attr("transform", function(d,i) {
          var markerX = x( new Date(moment.utc( d.Date)) );
          var markerY = (height+30);
          var translate = ("translate("+ markerX +"," + markerY + ")");
          return translate;
      });

  var markerRadius = 5;

  markers.append("svg:a")
      .attr("target", "_blank")
      .attr("xlink:href", function(d){ return ("/about/synthesis-release/"+ d.Date); })
    .append("circle")
      .attr("class", "synth-release-marker")
      .attr("cx", 0)
      .attr("cy", 0)
      .attr("r", markerRadius)
    .append("svg:title")
      .text("Synthesis release (click to see details in a new window).");
  markers.append("svg:a")
      .attr("target", "_blank")
      .attr("xlink:href", function(d){ return ("/about/synthesis-release/"+ d.Date); })
    .append('text')
      .attr("class", "synth-release-marker")
      .text("S")
      .attr("x", -2.5)
      .attr("dy", 3)
    .append("svg:title")
      .text("Synthesis release (click to see details in a new window).");
  markers.append("svg:a")
      .attr("target", "_blank")
      .attr("xlink:href", function(d){ return ("/about/taxonomy-release/"+ d.Date); })
    .append("circle")
      .attr("class", "taxo-release-marker")
      .attr("cx", 0)
      .attr("cy", 15)
      .attr("r", markerRadius)
    .append("svg:title")
      .text("Taxonomy release (click to see details in a new window).");
  markers.append("svg:a")
      .attr("target", "_blank")
      .attr("xlink:href", function(d){ return ("/about/taxonomy-release/"+ d.Date); })
    .append("text")
      .attr("class", "taxo-release-marker")
      .text('T')
      .attr("x", -2.5)
      .attr("dy", 18)
    .append("svg:title")
      .text("Taxonomy release (click to see details in a new window).");

  // make a bisector that splits on any date (used below)
  var bisectDate = d3.bisector(function(d) { return new Date(moment.utc(d.Date)); }).right;

  // highlight should respond to mouse movement (or incoming date on query string)
  svg.on("click", function() {
    // clicking forces a pin in the new place
    unpinHighlight();

    // create a synthetic event on the highlight
    var mouse = d3.mouse(this);
    var evt = document.createEvent("MouseEvents");
    evt.initMouseEvent(
        "mousemove",
        true,       // bubbles?
        true,       // cancelable?
        window,     // view
        0,          // detail (unused)
        0,          // screenX (ignored)
        0,          // screenY (ignored)
        event.x,    // clientX (copy from click event)
        event.y,    // clientY (copy from click event)
        false,      // ctrlKey
        false,      // altKey
        false,      // shiftKey
        false,      // metaKey
        0,          // button
        svg[0][0]   // relatedTarget
    );

    // "unwrap" the funky d3 selection and dispatch the mousemove
    svg[0][0].dispatchEvent(evt);
    pinHighlight();
  });

  svg.on("mousemove", function() {
    if (highlightPinned) { 
        // pinning stops this behavior
        return;
    };

    var mouse = d3.mouse(this);
    var localX = mouse[0]; //  + margin.left;
    // find the nearest date and snap to its X coordinate
    var nearestDate, nearestProfileData;
    var mouseDate = x.invert(localX);
    var nextDatePos = bisectDate( data, mouseDate );
    if (nextDatePos === data.length) {
        // we're at the end of the date range
        var lastDatePos = nextDatePos - 1;
        nearestDate = new Date(moment.utc( data[lastDatePos].Date ));
        nearestProfileData = data[lastDatePos];
    } else {
        // split is somewhere inside the date range
        var nextDate = new Date(moment.utc( data[nextDatePos].Date ));
        var previousDatePos = Math.max(0, nextDatePos - 1);
        var previousDate = new Date(moment.utc( data[previousDatePos].Date ));
        // which is nearest?
        var previousSnapDistance = mouseDate - previousDate;
        var nextSnapDistance = nextDate - mouseDate;
        if (nextSnapDistance > previousSnapDistance) {
            nearestDate = previousDate;
            nearestProfileData = data[previousDatePos];
        } else {
            nearestDate = nextDate;
            nearestProfileData = data[nextDatePos];
        }
    }

    highlightDate( nearestDate, nearestProfileData );
  });

  // nested function, to use highlight and other instances
  function highlightDate( targetDate, profileData ) {
      if (typeof(targetDate) === 'string') {
          // fetch the expected Date and data
          if (!profileData) {
              // try to retrieve data
              var matchingProfiles = data.filter(function(d) {return d.Date === targetDate});
              if (matchingProfiles.length === 0) {
                alert("No data was found for this date: "+ targetDate);
                return;
              }
              profileData = matchingProfiles[0];
          }
          targetDate = new Date(moment.utc(targetDate));
      }

      var snapX = x(targetDate); // find X coordinates for the nearest date
      // move the highlight and update its displayed stats
      highlight.attr("transform", "translate("+ snapX +","+ margin.top +")");
      highlightDetailsTitle.text(targetDate);

      var statPosition = 0; 
      var statsFields = highlightDetailsStats.selectAll('text');
      d3.keys(mutuallyExclusiveCategories).reverse().forEach(function(propName) {
          var func = mutuallyExclusiveCategories[propName];
          var displayText = propName +": "+ func(profileData);
          var itsField = statsFields.filter(function(d,i) {return i === statPosition;});
          itsField.text(displayText);
          statPosition++;
      });

      // show markers for synthesis and/or taxonomy release, IF found for this date
      var dateHasSynthRelease = (profileData['Date has synthesis release'] === 'true');
      var dateHasTaxoRelease = (profileData['Date has phylesystem info'] === 'true');
      if (dateHasSynthRelease && dateHasTaxoRelease) {
          // shift the taxo-release markers down to second row
          highlightDetailsStats.select('circle.taxo-release-marker')
              .attr("cy", highlightMarkerSecondRowY);
          highlightDetailsStats.selectAll('text.taxo-release-marker')
              .attr("y", highlightMarkerSecondRowY);
      } else {
          // shift the taxo-release markers up to first row
          highlightDetailsStats.select('circle.taxo-release-marker')
              .attr("cy", highlightMarkerFirstRowY);
          highlightDetailsStats.selectAll('text.taxo-release-marker')
              .attr("y", highlightMarkerFirstRowY);
      }
      highlightDetailsStats.selectAll('.synth-release-marker')
          .style('visibility', function() {
              return (dateHasSynthRelease ? 'visible' : 'hidden');
          });
      highlightDetailsStats.selectAll('.taxo-release-marker')
          .style('visibility', function() {
              return (dateHasTaxoRelease ? 'visible' : 'hidden');
          });

      // set box height according to optional markers above
      highlightDetailsBox.attr('height', function() {
          if (dateHasSynthRelease && dateHasTaxoRelease) {
              return 140;
          }
          if (dateHasSynthRelease || dateHasTaxoRelease) {
              return 120;
          }
          return 100;
      });

      // should we put the details box on left or right (default)?
      var detailsWidth = highlightDetailsBox.attr('width');
      if (snapX > (width - detailsWidth)) {
          highlightDetails.attr("transform", "translate(-"+ (Number(detailsWidth) + 8) +", 100)");
      } else {
          highlightDetails.attr("transform", "translate(8,100)");
      }
  }

  // expose these methods for outside callers
  populateSVG.highlightDate = highlightDate;
};



$(document).ready(function() {
    populateSVG(otu_stats);

    // highlight incoming date/version, or the latest if not found
    var featuredDate = getParameterByName('highlight');
    var pinInitialHighlight = false;
    if (featuredDate) {
        // stick to this date until user clicks elsewhere
        pinInitialHighlight = true;
    } else {
        // set initial highlight on latest date, but allow easy exploration
        featuredDate = otu_stats[otu_stats.length - 1].Date;  // eg, '2014-11-05T19Z'
    }
    populateSVG.highlightDate( featuredDate );
    if (pinInitialHighlight) {
        pinHighlight();
    }
});

function getParameterByName(name) {
    // Retrieves query-string vars by name (or null)
    // from http://stackoverflow.com/a/5158301
    var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
    return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}

function formatISODate( dateString, options ) {
    options = options || {includeTime: true};
    var aDate = new moment(dateString);
    // see http://momentjs.com/docs/#/parsing/string/
    if (options.includeTime) {
        return aDate.format('MMMM Do YYYY, hA');
    } else {
        return aDate.format('MMMM Do YYYY');
    }
}

function showCurrentSynthesisProfile() {
    var currentSynthesisProfile = profilesByDate[currentSynthesisProfileDate];
    // Our stats JSON has friendly property names. Just format them!
    $('#otu-stats-panel table').empty();
    for (var prop in currentSynthesisProfile) {
        if (prop === 'Date' || prop == 'blocks' || prop == 'total') continue;
        var itsValue = currentSynthesisProfile[ prop ];
        $('#otu-stats-panel table').append(
            '<tr>'
              + '<th>'+ prop +'</th>'
              + '<td>'+ itsValue +'</td>'
              // for sparkline specs, see http://style.org/chartapi/sparklines/
              + '<td>'
                  + '<img src="http://chart.apis.google.com/chart?'
                    + 'cht=lc'
                    + '&chs=50x20'
                    + '&chd=t:'+ getSynthesisSparklineData(prop, currentSynthesisProfileDate)
                    + '&chco=336699'
                    + '&chls=1,1,0'
                    + '&chm=o,990000,0,20,4'
                    + '&chxt=r,x,y'
                    + '&chxs=0,990000,11,0,_|1,990000,1,0,_|2,990000,1,0,_'
                    + '&chxl=0:||1:||2:||'
                  + '" title="Previous history for this statistic">'
              +'</td>'
          + '</tr>'
        );
    }

    // Update the Next and Prev buttons (disable if no more)
    var nthProfile = sortedSynthesisProfileKeys.indexOf(currentSynthesisProfileDate);
    if (nthProfile === 0) {
        $('#prev-otu-profile').addClass('disabled');
    } else {
        $('#prev-otu-profile').removeClass('disabled');
    }
    if (nthProfile === (sortedSynthesisProfileKeys.length - 1)) {
        $('#next-otu-profile').addClass('disabled');
    } else {
        $('#next-otu-profile').removeClass('disabled');
    }

    // Update the date selector
    $('#otu-profile-date-chooser').val(currentSynthesisProfileDate);
}

function getSynthesisSparklineData(prop, lastProfileDate) {
    // return a comma-delimited data series (string) for plotting
    // NOTE that these should be decimal numbers between 0 and 100, so scale accordingly
    //   EXAMPLE: '5.3,26.5,15.9,31.7,42.3,21.2'
    var series = [];
    $.each(sortedSynthesisProfileKeys, function(i, profileDate) {
        // skip more recent profiles, if we're looking at something older
        if (profileDate <= lastProfileDate) {
             series.push( profilesByDate[profileDate][prop] );
        }
    });

    // normalize all values in the series to the range 0.0 to 100.0
    var minValue = Math.min.apply(Math, series);
    var maxValue = Math.max.apply(Math, series);
    // move all values above zero
    if (minValue < 0) {
        series = $.map(series, function(d) {
            return d - minValue;
        });
    }
    // scale all values to under 100 (using 90 to avoid cropping)
    var scaleToFit = 90.0 / maxValue;
    series = $.map(series, function(d) {
        return d * scaleToFit;
    });

    return series.join(',');
}

</script>

{'fraction_non_alphanumeric': 0.1572278748320215, 'fraction_numerical': 0.0145133422921866, 'mean_word_length': 2.597941704655339, 'pattern_counts': {'":': 2, '<': 97, '<?xml version=': 0, '>': 93, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5987753', 'n_tokens_mistral': 16670, 'n_tokens_neox': 15909, 'n_words': 3960}