Here’s a set of new plots, combined with my others.

The code

Fetch the data

Here I include the patchers from the previous posts, and I add a new one.

class ColumnStatesPatcher(object):
  def __init__(self):
    filename = 'column_states.%s.csv' % time.strftime('%Y%m%d-%H.%M.%S')
    self.outfile = open(filename, 'w')

  def patch(self, model):
    csvOutput = csv.writer(self.outfile)

    headerRow = [
      'n-unpredicted-active-columns',
      'n-predicted-inactive-columns',
      'n-predicted-active-columns',
    ]
    csvOutput.writerow(headerRow)

    runMethod = model.run
    def myRun(v):
      tp = model._getTPRegion().getSelf()._tfdr
      npPredictedCells = tp.getPredictedState().reshape(-1).nonzero()[0]
      predictedColumns = set(np.unique(npPredictedCells / tp.cellsPerColumn).tolist())

      runResult = runMethod(v)

      spOutput = model._getSPRegion().getSelf()._spatialPoolerOutput
      activeColumns = set(spOutput.nonzero()[0].tolist())

      row = (
        len(activeColumns - predictedColumns),
        len(predictedColumns - activeColumns),
        len(activeColumns & predictedColumns),
      )
      csvOutput.writerow(row)

      return runResult

    model.run = myRun

  def onFinished(self):
    self.outfile.close()

class TotalSegmentsPatcher(object):
  def __init__(self):
    filename = 'segments.%s.csv' % time.strftime('%Y%m%d-%H.%M.%S')
    self.outfile = open(filename, 'w')

  def patch(self, model):
    csvOutput = csv.writer(self.outfile)

    tp = model._getTPRegion().getSelf()._tfdr
    htmData = (tp.activationThreshold,)
    csvOutput.writerow(htmData)

    runMethod = model.run
    def myRun(v):
      runResult = runMethod(v)

      nSegmentsByConnectedCount = []
      if hasattr(tp, "connections"): # temporal_memory.py
        for segment, _ in tp.connections._segments.items():
          nConnected = 0
          for syn in tp.connections.synapsesForSegment(segment):
            synapseData = tp.connections.dataForSynapse(syn)
            if synapseData.permanence >= tp.connectedPermanence:
              nConnected += 1

          while len(nSegmentsByConnectedCount) <= nConnected:
            nSegmentsByConnectedCount.append(0)
          nSegmentsByConnectedCount[nConnected] += 1
      else: # tp.py
        for col in xrange(tp.numberOfCols):
          for cell in xrange(tp.cellsPerColumn):
            for segIdx in xrange(tp.getNumSegmentsInCell(col, cell)):
              nConnected = 0
              v = tp.getSegmentOnCell(col, cell, segIdx)
              segData = v[0]
              for _, _, perm in v[1:]:
                if perm >= tp.connectedPerm:
                  nConnected += 1

              while len(nSegmentsByConnectedCount) <= nConnected:
                nSegmentsByConnectedCount.append(0)
              nSegmentsByConnectedCount[nConnected] += 1

      csvOutput.writerow(nSegmentsByConnectedCount)

      return runResult

    model.run = myRun

  def onFinished(self):
    self.outfile.close()

# Does not work with tp.py.
class SegmentLearningPatcher(object):
  def __init__(self):
    filename = 'segment_learning.%s.csv' % time.strftime('%Y%m%d-%H.%M.%S')
    self.outfile = open(filename, 'w')
    self.clear()

  def clear(self):
    self.addedSegments = 0
    self.destroyedSegments = 0
    self.addedSynapses = 0
    self.destroyedSynapses = 0
    self.strengthenedSynapses = 0
    self.weakenedSynapses = 0
    self.newlyConnectedSynapses = 0
    self.newlyDisconnectedSynapses = 0

  def patch(self, model):
    csvOutput = csv.writer(self.outfile)

    headerRow = (
      'n-added-segments',
      'n-destroyed-segments',
      'n-added-synapses',
      'n-destroyed-synapses',
      'n-strengthened-synapses',
      'n-weakened-synapses',
      'n-newly-connected-synapses',
      'n-newly-disconnected-synapses',
    )
    csvOutput.writerow(headerRow)

    tp = model._getTPRegion().getSelf()._tfdr
    connections = tp.connections

    createSegmentMethod = connections.createSegment
    def myCreateSegment(*args, **kwargs):
      self.addedSegments += 1
      return createSegmentMethod(*args, **kwargs)
    connections.createSegment = myCreateSegment

    destroySegmentMethod = connections.destroySegment
    def myDestroySegment(*args, **kwargs):
      self.destroyedSegments += 1
      return destroySegmentMethod(*args, **kwargs)
    connections.destroySegment = myDestroySegment

    createSynapseMethod = connections.createSynapse
    def myCreateSynapse(*args, **kwargs):
      self.addedSynapses += 1
      return createSynapseMethod(*args, **kwargs)
    connections.createSynapse = myCreateSynapse

    destroySynapseMethod = connections.destroySynapse
    def myDestroySynapse(*args, **kwargs):
      self.destroyedSynapses += 1
      return destroySynapseMethod(*args, **kwargs)
    connections.destroySynapse = myDestroySynapse

    updateSynapsePermanenceMethod = connections.updateSynapsePermanence
    def myUpdateSynapsesPermanence(synapse, permanence):
      previous = connections.dataForSynapse(synapse).permanence
      if previous < permanence:
        self.strengthenedSynapses += 1
        if previous < tp.connectedPermanence and permanence >= tp.connectedPermanence:
          self.newlyConnectedSynapses += 1
      elif previous > permanence:
        self.weakenedSynapses += 1
        if previous >= tp.connectedPermanence and permanence < tp.connectedPermanence:
          self.newlyDisconnectedSynapses += 1
      return updateSynapsePermanenceMethod(synapse, permanence)
    connections.updateSynapsePermanence = myUpdateSynapsesPermanence

    runMethod = model.run
    def myRun(v):
      self.clear()
      runResult = runMethod(v)
      row = (
        self.addedSegments,
        self.destroyedSegments,
        self.addedSynapses,
        self.destroyedSynapses,
        self.strengthenedSynapses,
        self.weakenedSynapses,
        self.newlyConnectedSynapses,
        self.newlyDisconnectedSynapses,
      )
      csvOutput.writerow(row)
      return runResult
    model.run = myRun

  def onFinished(self):
    self.outfile.close()

## Then, somewhere in code...
patchers = (ColumnStatesPatcher(),
            TotalSegmentsPatcher(),
            SegmentLearningPatcher())
for p in patchers:
  p.patch(model)

# ...

for p in patchers:
  p.onFinished()

Draw the data

Use d3.js to draw the CSV data. The code is pretty good, though there’s some room for cleanup.

CSS

text {
    font: 10px sans-serif;
}

.axis path {
    fill: none;
    stroke: none;
    shape-rendering: crispEdges;
}

.axis line {
    stroke: none;
    shape-rendering: crispEdges;
}

.showAxis path {
    stroke: black;
}

.y.axis line {
    stroke: black;
}

.noselect {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

.clickable {
    cursor: pointer;
}

.draggable {
    cursor: -webkit-grab;
    cursor: -moz-grab;
    cursor: grab;
}

.dragging .draggable,
.dragging .clickable {
    cursor: -webkit-grabbing;
    cursor: -moz-grabbing;
    cursor: grabbing;
}

JavaScript

Here’s a code drop.

//
// SHARED STATE
//
var chartWidth = 600,
    chartLeft = 40,
    x = d3.scale.linear()
        .range([0, chartWidth]),
    onxscalechanged = [], // callbacks
    onZoomScaleExtentChanged = [], // callbacks
    timestepCount,
    zoom = d3.behavior.zoom()
        .on('zoom', function() {
            // Enforce a translateExtent
            if (x(0) > x.range()[0]) {
                zoom.translate([0, zoom.translate()[1]]);
            }
            else if (x(timestepCount) < x.range()[1]) {
                var xDomain = x.domain(),
                    domainWidth = xDomain[1] - xDomain[0],
                    leftMostInDataSpace = timestepCount - domainWidth;
                zoom.translate([-(leftMostInDataSpace * zoom.scale()),
                                zoom.translate()[1]]);
            }
            onxscalechanged.forEach(function(f) { f(true); });
        }),
    container = d3.select('#putItHere').append('div')
        .style('position', 'relative'),
    charts = container.append('div')
        .style('position', 'absolute')
        .style('left', chartLeft + 'px'),
    xSamplesDomain = [null, null],
    xSamples = [];

function onTimestepCountKnown(count) {
    if (!timestepCount || count > timestepCount) {
        timestepCount = count;
        // Set the domain to [0, count], but do this charade because the zoom's
        // scale is encapsulated and we can't change it without changing the
        // domain. A zoom scale of 1 means that the data space and pixel space
        // are equal.
        var scale = chartWidth / count;
        x.domain([0,chartWidth]);
        zoom.x(x);
        zoom.scale(scale);
        zoom.scaleExtent([chartWidth / count, Math.max(40, scale)]);
        onZoomScaleExtentChanged.forEach(function(f) { f(); });
    }
}

function xResample() {
    var extent = x.domain();
    if (xSamplesDomain[0] == extent[0] && xSamplesDomain[1] == extent[1]) {
        // No need to resample.
        return;
    }

    var xSamplesNew;
    if (extent[1] - extent[0] > chartWidth) {
        var bucketWidth = (extent[1] - extent[0]) / chartWidth,
            iPrevious = 0;
        xSamplesNew = d3.range(extent[0], extent[1], bucketWidth)
            .slice(0, chartWidth) // Floating point math can cause an extra.
            .map(function(x) {
                var data = {x0: x, x1: Math.min(x + bucketWidth, extent[1])};
                while (iPrevious < xSamples.length &&
                       xSamples[iPrevious].x < data.x0) {
                    iPrevious++;
                }

                if (iPrevious < xSamples.length &&
                    xSamples[iPrevious].x < data.x1) {
                    // When zooming / panning, the behavior is less
                    // jarring if we reuse samples rather than
                    // grabbing a new random sample.
                    data.x = xSamples[iPrevious].x;
                }
                else {
                    // Choose randomly from the interval.
                    // Otherwise with repeating patterns we'll have aliasing.
                    data.x = Math.random() * (data.x1 - data.x0) + data.x0;
                    data.x = Math.round(data.x);
                    if (data.x < data.x0) {
                        data.x++;
                    }
                    else if (data.x >= data.x1) {
                        data.x--;
                    }
                }

                return data;
            });
    }
    else {
        // No sampling needed.
        xSamplesNew = d3.range(Math.floor(extent[0]), extent[1])
            .map(function(x) { return {x0: x, x: x, x1: x + 1 };});
    }
    xSamples = xSamplesNew;
    xSamplesDomain = [extent[0], extent[1]];
}

//
// ZOOM WIDGET
//
(function() {
    function zoomTowardCenter(scale, drawImmediately) {
        var extent = x.domain(),
            center = ((extent[1] - extent[0]) / 2) + extent[0];
        zoom.scale(scale);
        var timestepsPerPixel = 1/scale,
            nTimesteps = chartWidth * timestepsPerPixel,
            newLeftmost = Math.max(0, center - (nTimesteps/2));
        zoom.translate([-newLeftmost * scale, 0]);
        onxscalechanged.forEach(function(f) { f(false); });
    }
    var zoomer = container.append('svg')
            .attr('width', 22)
            .attr('height', 102)
            .style('position', 'absolute')
            .style('top', '20px')
            .style('left', 0)
            .append('g')
            .attr('transform', 'translate(1,1)'),
        grooveHeight = 60,
        knobWidth = 20,
        knobHeight = 4,
        groove = zoomer.append('g')
            .attr('transform', 'translate(0, 20)'),
        grooveY = d3.scale.log()
            .domain([1, 5]) // default while waiting for csv
            .range([grooveHeight - knobHeight, 0]);
    onZoomScaleExtentChanged.push(function() {
        grooveY.domain(zoom.scaleExtent());
        placeKnob();
    });
    groove.append('rect')
        .attr('x', 8)
        .attr('y', 0)
        .attr('width', 3)
        .attr('height', 60)
        .attr('stroke', 'lightgray')
        .attr('fill', 'none');
    groove.append('rect')
        .attr('class', 'clickable')
        .attr('width', 20)
        .attr('height', 60)
        .attr('stroke', 'none')
        .attr('fill', 'transparent')
        .on('click', function () {
            var y = d3.event.clientY - d3.event.target.getBoundingClientRect().top;
            zoomTowardCenter(grooveY.invert(y), true);
        });

    [{text: '+',
      translateY: 0,
      onclick: function() {
          var y = Math.max(0, grooveY(zoom.scale()) - 5);
          zoomTowardCenter(grooveY.invert(y), true);
      }},
     {text: '-',
      translateY: grooveHeight + 20,
      onclick: function() {
          var y = Math.min(grooveHeight - knobHeight, grooveY(zoom.scale()) + 5);
          zoomTowardCenter(grooveY.invert(y), true);
      }}]
        .forEach(function(spec) {
            var button = zoomer.append('g')
                    .attr('transform', 'translate(0,' +
                          spec.translateY + ')');
            button.append('text')
                .attr('class', 'noselect')
                .attr('x', 10)
                .attr('y', 10)
                .attr('dy', '.26em')
                .attr('text-anchor', 'middle')
                .style('font', '15px sans-serif')
                .style('font-weight', 'bold')
                .style('fill', 'gray')
                .text(spec.text);
            button.append('rect')
                .attr('width', 20)
                .attr('height', 20)
                .attr('stroke-width', 1)
                .attr('stroke', 'gray')
                .attr('fill', 'transparent')
                .attr('class', 'clickable')
                .on('click', spec.onclick);
        });

    var knob = groove.append('g')
            .attr('class', 'draggable')
            .attr('transform', function(d) {
                return 'translate(0,' + grooveY(d) + ')';
            }),
        knobProgress = knob.append('rect')
            .attr('height', knobHeight)
            .attr('fill', 'black')
            .attr('stroke', 'none'),
        knobTitle = knob.append('title');
    knob.append('rect')
        .attr('width', knobWidth)
        .attr('height', knobHeight)
        .attr('fill', 'transparent')
        .attr('stroke', 'gray')
        .call(d3.behavior.drag()
              .on('dragstart', function() {
                  zoomer.classed('dragging', true);
              })
              .on('drag', function() {
                  var y = d3.event.sourceEvent.clientY -
                          groove.node().getBoundingClientRect().top;
                  y = Math.max(0, y);
                  y = Math.min(grooveHeight - knobHeight, y);
                  zoomTowardCenter(grooveY.invert(y));
              })
              .on('dragend', function() {
                  zoomer.classed('dragging', false);
              }));

    function placeKnob() {
        var scale = zoom.scale(),
            sampleRate = Math.min(scale, 1);
        knob.attr('transform', 'translate(0,' + grooveY(scale) + ')');
        knobProgress.attr('width', knobWidth * sampleRate);
        knobTitle.text(sampleRate == 1 ?
                       "Displaying every timestep in this interval." :
                       ("Due to limited pixels, only " +
                        Math.round(sampleRate*100) +
                        "% of timesteps in this interval are shown."));
    }

    onxscalechanged.push(placeKnob);
})();

//
// SHARED CHART CODE
//
function stackedTimeSeries() {
    var colorScale,
        x,
        y,
        xExtent;

    function stretch(selection) {
        selection.each(function(data) {
            var pixelsPerTimestep =
                    (x.range()[1] - x.range()[0]) /
                    (x.domain()[1] - x.domain()[0]);
            d3.select(this).selectAll('.stretchMe')
                .attr('transform', 'scale(' + pixelsPerTimestep +
                      ',1)translate(' + (-x.domain()[0]) + ',0)');
        });
    }

    var chart = function (selection) {
        selection.each(function(data) {
            var stretchMe = d3.select(this).selectAll('.stretchMe')
                    .data([data]);
            stretchMe.enter()
                .append('g')
                .attr('class', 'stretchMe');

            var layer = stretchMe.selectAll('.layer')
                    .data(function (d) {return d;});
            layer.enter()
                .append('path')
                .attr('class', 'layer')
                .attr('shape-rendering', 'crispEdges')
                .attr('fill', function(d, i) { return colorScale(i); })
                .attr('stroke', 'none');

            layer.attr('d', function (ds) {
                return ds.map(function(d) {
                    var x0 = d.x0,
                        x1 = d.x1,
                        y0 = y(d.y0 + d.y),
                        y1 = y(d.y0);
                    return ['M', x0, y0,
                            'L', x1, y0,
                            'L', x1, y1,
                            'L', x0, y1,
                            'Z'].join(' ');
                }).join(' ');
            });

            layer.exit().remove();
        }).call(stretch);
    };

    chart.stretch = stretch;

    chart.colorScale = function(_) {
        if (!arguments.length) return colorScale;
        colorScale = _;
        return chart;
    };

    chart.x = function(_) {
        if (!arguments.length) return x;
        x = _;
        return chart;
    };

    chart.y = function(_) {
        if (!arguments.length) return y;
        y = _;
        return chart;
    };

    return chart;
}

//
// COLUMN STATES PLOT
//
(function() {
    var margin = {top: 0, right: 300, bottom: 4, left: 0},
        height = 100 - margin.top - margin.bottom;

    var svg = charts.append('svg')
            .attr('width', chartWidth + margin.left + margin.right)
            .attr('height', height + margin.top + margin.bottom);

    var defs = svg.append('defs');
    defs.append('clipPath')
        .attr('id', 'clip1')
        .append('rect')
        .attr('width', chartWidth)
        .attr('height', height);

    d3.csv('/stuff/column_states.20160214-20.47.07.csv')
        .row(function(d) {
            var intKeys = ['n-unpredicted-active-columns',
                           'n-predicted-active-columns',
                           'n-predicted-inactive-columns'];
            intKeys.forEach(function(k) {
                d[k] = parseInt(d[k]);
            });
            return d;
        })
        .get(function(error, timesteps) {
            onTimestepCountKnown(timesteps.length);
            var stackOrder = [{key: 'n-unpredicted-active-columns',
                               color: 'hsl(0,100%,50%)',
                               activeText: 'active',
                               predictedText: 'not predicted'},
                              {key: 'n-predicted-active-columns',
                               color: 'hsl(270,100%,40%)',
                               activeText: 'active',
                               predictedText: 'predicted'},
                              {key: 'n-predicted-inactive-columns',
                               color: 'hsla(210,100%,50%,0.5)',
                               activeText: 'not active',
                               predictedText: 'predicted'}],
                yStackMax = d3.max(timesteps, function(d) {
                    return stackOrder.reduce(function(sum, o) {
                        return sum + d[o.key];
                    }, 0);
                }),
                chartNode = svg.append('g')
                    .attr('class', 'chart')
                    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'),
                stackColorScale = d3.scale.ordinal()
                    .domain(d3.range(stackOrder.length))
                    .range(stackOrder.map(function(d) { return d.color; })),
                help = chartNode.append('g')
                    .attr('transform', 'translate(' + (chartWidth+50) + ',10)');

            // Title and legend
            help.append('text')
                .attr('class', 'noselect')
                .attr('text-anchor', 'right')
                .attr('x', 5)
                .attr('y', 0)
                .text('active and predicted columns, stacked by:');

            var legend = help.append('g')
                    .attr('class', 'noselect')
                    .attr('transform', 'translate(0, 20)'),
                unitWidth = 80,
                color = legend.selectAll('g')
                    .data(stackOrder);
            stackOrder.forEach(function(o, i) {
                var g = legend.append('g')
                        .attr('transform', 'translate(' + i * unitWidth + ',0)');
                g.append('rect')
                    .attr('width', unitWidth)
                    .attr('height', 4)
                    .attr('fill', o.color);
                g.append('text')
                    .attr('x', unitWidth/2)
                    .attr('dy', '-0.24em')
                    .attr('text-anchor', 'middle')
                    .text(o.activeText);
                g.append('text')
                    .attr('x', unitWidth/2)
                    .attr('y', 14)
                    .attr('text-anchor', 'middle')
                    .text(o.predictedText);
            });

            // Chart
            var y = d3.scale.linear()
                    .domain([0, yStackMax])
                    .range([height, 0]),
                drawTimeout = null,
                chart = stackedTimeSeries()
                    .colorScale(stackColorScale)
                    .x(x)
                    .y(y),
                chartNodeInner = chartNode.append('g')
                    .style('clip-path', 'url(#clip1)');

            chartNode.append('rect')
                .attr('fill-opacity', 0)
                .attr('x', 0)
                .attr('y', 0)
                .attr('width', chartWidth)
                .attr('height', height)
                .call(zoom);

            function draw () {
                if (drawTimeout) {
                    clearTimeout(drawTimeout);
                    drawTimeout = null;
                }

                xResample();

                var layers = [];
                stackOrder.forEach(function(o) {
                    var layer = xSamples.map(function(data) {
                        var y = timesteps[data.x][o.key] || 0;
                        return {x: data.x, x0: data.x0, x1: data.x1, y: y};
                    });
                    layers.push(layer);
                });
                d3.layout.stack()(layers); // inserts y0 values
                chartNodeInner.datum(layers)
                    .call(chart);
            }

            onxscalechanged.push(function(maybeCoalesce) {
                chartNodeInner.call(chart.stretch);
                if (!drawTimeout) {
                    drawTimeout = setTimeout(draw, maybeCoalesce ? 250 : 1000/60);
                }
            });

            // y axis
            chartNode.append('g')
                .attr('transform', 'translate(' + chartWidth + ', 0)')
                .attr('class', 'y axis')
                .call(d3.svg.axis()
                      .scale(y)
                      .ticks(4)
                      .tickPadding(2)
                      .tickSize(4)
                      .outerTickSize(0)
                      .orient('right'));

            // Initial change
            draw();
            onxscalechanged.forEach(function (f) { f(); });
        });
})();

//
// SEGMENTS PLOT
//
(function() {
    var margin = {top: 20, right: 300, bottom: 20, left: 0},
        height = 100 - margin.top - margin.bottom;

    var svg = charts.append('svg')
            .attr('width', chartWidth + margin.left + margin.right)
            .attr('height', height + margin.top + margin.bottom);

    var defs = svg.append('defs');
    defs.append('clipPath')
        .attr('id', 'clip2')
        .append('rect')
        .attr('width', chartWidth)
        .attr('height', height);

    d3.text('/stuff/segments.20160214-20.47.07.csv', 'text/csv', function(error, contents) {
        var rows = d3.csv.parseRows(contents),
            htmData = rows[0],
            timesteps = rows.splice(1),
            activationThreshold = parseInt(htmData[0]),
            maxTotalSegments = 0,
            maxConnectedSynapsesInSegment = 0;

        onTimestepCountKnown(timesteps.length);

        timesteps.forEach(function(row) {
            var totalSegments = 0;
            for (var i = 0; i < row.length; i++) {
                row[i] = parseInt(row[i]);
                totalSegments += row[i];
                if (i > maxConnectedSynapsesInSegment && row[i] > 0) {
                    maxConnectedSynapsesInSegment = i;
                }
            }
            if (totalSegments > maxTotalSegments) {
                maxTotalSegments = totalSegments;
            }
        });

        var chartNode = svg.append('g')
                .attr('class', 'chart')
                .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'),
            stackOrder = d3.range(maxConnectedSynapsesInSegment + 1).reverse(),
            colorExtent = ['whitesmoke', 'black'],
            connectedSynapsesColorScale = d3.scale.linear()
                .domain([0, maxConnectedSynapsesInSegment])
                .range(colorExtent),
            stackColorScale = d3.scale.linear()
                .domain([maxConnectedSynapsesInSegment, 0])
                .range(colorExtent),
            help = chartNode.append('g')
                .attr('transform', 'translate(' + (chartWidth+50) + ',0)');

        // Title and legend
        help.append('text')
            .attr('class', 'noselect')
            .attr('text-anchor', 'right')
            .attr('x', 2)
            .attr('y', 0)
            .text('distal segments, stacked by:');

        var legend = help.append('g')
                .attr('class', 'noselect')
                .attr('transform', 'translate(0, 20)'),
            domainWidth = maxConnectedSynapsesInSegment + 1,
            legendWidth = 200,
            unitWidth = legendWidth / domainWidth,
            rect = legend.selectAll('rect')
                .data(d3.range(maxConnectedSynapsesInSegment + 1));
        rect.enter()
            .append('rect')
            .attr('x', function(d, i) { return i * unitWidth; })
            .attr('width', unitWidth)
            .attr('height', 4);
        rect.attr('fill', function(d, i) { return connectedSynapsesColorScale(d); });
        legend.append('text')
            .attr('x', legendWidth / 2)
            .attr('y', -4)
            .attr('text-anchor', 'middle')
            .text('number of connected synapses on segment');
        legend.append('text')
            .attr('y', 14)
            .attr('text-anchor', 'middle')
            .text(0);
        legend.append('text')
            .attr('x', legendWidth)
            .attr('text-anchor', 'middle')
            .attr('y', 14)
            .text(maxConnectedSynapsesInSegment);

        // Chart
        var y = d3.scale.linear()
                .domain([0, maxTotalSegments])
                .range([height, 0]),
            drawTimeout = null,
            chart = stackedTimeSeries()
                .colorScale(stackColorScale)
                .x(x)
                .y(y),
            chartNodeInner = chartNode.append('g')
                .style('clip-path', 'url(#clip2)');

        chartNode.append('rect')
            .attr('fill-opacity', 0)
            .attr('x', 0)
            .attr('y', 0)
            .attr('width', chartWidth)
            .attr('height', height)
            .call(zoom);

        function draw () {
            if (drawTimeout) {
                clearTimeout(drawTimeout);
                drawTimeout = null;
            }

            xResample();

            var layers = [];
            stackOrder.forEach(function(key) {
                var layer = xSamples.map(function(data) {
                    var y = timesteps[data.x][key] || 0;
                    return {x: data.x, x0: data.x0, x1: data.x1, y: y};
                });
                layers.push(layer);
            });
            d3.layout.stack()(layers); // inserts y0 values
            chartNodeInner.datum(layers)
                .call(chart);
        }

        onxscalechanged.push(function(maybeCoalesce) {
            chartNodeInner.call(chart.stretch);
            if (!drawTimeout) {
                drawTimeout = setTimeout(draw, maybeCoalesce ? 250 : 1000/60);
            }
        });

        // Axes
        var yLabels = chartNode.append('g')
                .attr('transform', 'translate(' + (chartWidth + 2) + ', 0)');

        onxscalechanged.push(function () {
            var finalRow = timesteps[Math.floor(x.domain()[1] - 1)],
                spikable = finalRow.slice(activationThreshold)
                    .reduce(function(sum, v) { return sum + v; }, 0),
                total = finalRow.reduce(function(sum, v) { return sum + v; }, 0);
            var groups = yLabels.selectAll('g')
                    .data([[spikable, 'spikable', '0.34em'],
                           [total, 'total', '0.34em']]);
            var enteringGroups = groups.enter()
                    .append('g');
            enteringGroups.append('line')
                .attr('x1', 0)
                .attr('x2', 4)
                .attr('y1', 0)
                .attr('y2', 0)
                .attr('stroke', 'black')
                .attr('stroke-width', 1);
            enteringGroups.append('text')
                .attr('class', 'noselect number')
                .attr('x', 6)
                .attr('dy', function(d, i) { return d[2]; });
            enteringGroups.append('text')
                .attr('class', 'noselect')
                .attr('x', 6)
                .attr('y', 15)
                .text(function(d) { return d[1]; });
            groups.attr('transform', function(d, i) {
                return 'translate(0,' + y(d[0]) + ')';
            })
                .select('.number')
                .text(function(d, i) { return d[0]; });
        });

        // Initial change
        draw();
        onxscalechanged.forEach(function (f) { f(); });
    });
})();

//
// SEGMENT LEARNING PLOTS
//
(function() {
    var margin = {top: 5, right: 300, bottom: 5, left: 0},
        height = 80 - margin.top - margin.bottom,
        specs = [{addKey: 'n-added-segments',
                  removeKey: 'n-destroyed-segments',
                  title: 'distal segment',
                  title2: 'addition / removal'},
                 {addKey: 'n-newly-connected-synapses',
                  removeKey: 'n-newly-disconnected-synapses',
                  title: 'distal connected synapse',
                  title2: 'addition / removal'},
                 {addKey: 'n-added-synapses',
                  removeKey: 'n-destroyed-synapses',
                  title: 'distal potential synapse',
                  title2: 'addition / removal'},
                 {addKey: 'n-strengthened-synapses',
                  removeKey: 'n-weakened-synapses',
                  title: 'distal synapse permanence',
                  title2: 'strengthening / weakening'},
                ];

    specs.forEach(function (spec) {
        spec.svg = charts.append('svg')
            .attr('width', chartWidth + margin.left + margin.right)
            .attr('height', height + margin.top + margin.bottom);

        spec.svg
            .append('defs')
            .append('clipPath')
            .attr('id', 'clip3')
            .append('rect')
            .attr('width', chartWidth)
            .attr('height', height);
    });

    d3.csv('/stuff/segment_learning.20160214-20.47.07.csv')
        .row(function(d) {
            var intKeys = ['n-added-segments',
                           'n-destroyed-segments',
                           'n-added-synapses',
                           'n-destroyed-synapses',
                           'n-strengthened-synapses',
                           'n-weakened-synapses',
                           'n-newly-connected-synapses',
                           'n-newly-disconnected-synapses'];
            intKeys.forEach(function(k) {
                d[k] = parseInt(d[k]);
            });
            return d;
        })
        .get(function(error, timesteps) {
            onTimestepCountKnown(timesteps.length);
            specs.forEach(function(spec) {
                var maxAdd = Math.max(10, d3.max(timesteps, function(d) {
                    return d[spec.addKey];
                    })),
                    maxDestroy = Math.max(10, d3.max(timesteps, function(d) {
                        return d[spec.removeKey];
                    })),
                    chartNode = spec.svg.append('g')
                        .attr('class', 'chart')
                        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'),
                    stackColorScale = d3.scale.ordinal()
                        .domain([0, 1])
                        .range(['green', 'lightgreen']),
                    help = chartNode.append('g')
                        .attr('transform', 'translate(' + (chartWidth+50) + ',5)');

                // Title
                help.append('text')
                    .attr('class', 'noselect')
                    .attr('text-anchor', 'right')
                    .attr('x', 5)
                    .attr('y', 0)
                    .text(spec.title);

                help.append('text')
                    .attr('class', 'noselect')
                    .attr('text-anchor', 'right')
                    .attr('x', 5)
                    .attr('y', '1.5em')
                    .text(spec.title2);

                // Chart
                // Using the stackedTimeSeries is a hack.
                // It's useful because it's already stretchable.
                var y = d3.scale.linear()
                        .domain([-maxDestroy, maxAdd])
                        .range([height, 0]),
                    drawTimeout = null,
                    chart = stackedTimeSeries()
                        .colorScale(stackColorScale)
                        .x(x)
                        .y(y),
                    chartNodeInner = chartNode.append('g')
                        .style('clip-path', 'url(#clip3)');

                chartNode.append('rect')
                    .attr('fill-opacity', 0)
                    .attr('x', 0)
                    .attr('y', 0)
                    .attr('width', chartWidth)
                    .attr('height', height)
                    .call(zoom);

                function draw () {
                    if (drawTimeout) {
                        clearTimeout(drawTimeout);
                        drawTimeout = null;
                    }

                    xResample();

                    var layers = [
                        xSamples.map(function(data) {
                            return {
                                x0: data.x0,
                                x: data.x,
                                x1: data.x1,
                                y0: 0,
                                y: timesteps[data.x][spec.addKey]
                            };
                        }),
                        xSamples.map(function(data) {
                            return {
                                x0: data.x0,
                                x: data.x,
                                x1: data.x1,
                                y0: 0,
                                y: -timesteps[data.x][spec.removeKey]
                            };
                        }),
                    ];

                    // d3.layout.stack()(layers); // inserts y0 values
                    chartNodeInner.datum(layers)
                        .call(chart);
                }

                onxscalechanged.push(function(maybeCoalesce) {
                    chartNodeInner.call(chart.stretch);
                    if (!drawTimeout) {
                        drawTimeout = setTimeout(draw, maybeCoalesce ? 250 : 1000/60);
                    }
                });

                // y axis
                chartNode.append('g')
                    .attr('transform', 'translate(' + chartWidth + ', 0)')
                    .attr('class', 'y axis showAxis')
                    .call(d3.svg.axis()
                          .scale(y)
                          .ticks(4)
                          .tickPadding(2)
                          .tickSize(4)
                          .outerTickSize(0)
                          .tickFormat(d3.format('d'))
                          .orient('right'));

                // Initial change
                draw();
                onxscalechanged.forEach(function (f) { f(); });
            });

        });
})();

//
// SHARED X AXIS
//
(function() {
    var xAxis = d3.svg.axis()
            .scale(x)
            .tickPadding(3)
            .tickSize(0)
            .outerTickSize(0)
            .orient('bottom')
            .tickFormat(d3.format('d')),
        marginLeft = 3,
        svg = charts.append('svg')
            .attr('width', chartWidth + 10)
            .attr('height', 30)
            .style('position', 'relative')
            .style('left', -marginLeft + 'px'),
        xAxisNode = svg.append('g')
            .attr('class', 'x axis noselect')
            .attr('transform', 'translate(' + marginLeft + ',0)')
            .append('g');
    onxscalechanged.push(function () {
        var extent = x.domain(),
            domainWidth = extent[1] - extent[0],
            pixelsPerTimestep = chartWidth / domainWidth,
            tickShift = pixelsPerTimestep / 2;
        xAxis.ticks(Math.min(domainWidth, 15));
        xAxisNode.call(xAxis);
        xAxisNode.attr('transform', 'translate(' + tickShift + ',' + '0)');
    });

    svg.append('text')
        .attr('class', 'x label noselect')
        .attr('x', marginLeft)
        .attr('y', 25)
        .text('timestep');
})();