This part hasn’t changed. Look at the Python sections in these posts:

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 = 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 = 150 - 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/hotgym.column_states.2016-01-16.1505.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 ( stackOrder . map ( function ( d ) { return d . key ; }))
. range ( stackOrder . map ( function ( d ) { return d . color ; })),
help = chartNode . append ( ' g ' )
. attr ( ' transform ' , ' translate( ' + ( chartWidth + 50 ) + ' ,40) ' );
// 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 = 200 - 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/hotgym.segments.2016-01-31.1400.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 ) + ' ,40) ' );
// 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 (); });
});
})();
//
// 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 ' )
. style ( ' top ' , ' -20px ' ),
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 ' );
})();