MonthlyStats.php #2

  • //
  • guest/
  • matt_attaway/
  • mediawiki/
  • extensions/
  • MonthlyStats/
  • MonthlyStats.php
  • View
  • Commits
  • Open Download .zip Download (18 KB)
<?php
/**
 * A special page extension to display monthly edit statistics
 *
 * @package MediaWiki
 * @subpackage Extensions
 *
 * @author Sam Stafford
 * @copyright Copyright  2009, Sam Stafford
 * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
 * Some bits adapted from Matt Grinberg's Usage Statistics extension.
 */

# Alert the user that this is not a valid entry point to MediaWiki
# if they try to access the extension file directly.
if (!defined('MEDIAWIKI')) {
        echo <<<EOT
To install the MonthlyStats extension, put the following line in LocalSettings.php:
require_once( "$IP/extensions/MonthlyStats/MonthlyStats.php" );
EOT;
        exit( 1 );
}

$dir = dirname(__FILE__) . '/';


$wgAutoloadClasses['MonthlyStats'] = $dir . 'MonthlyStats.php';
$wgExtensionMessagesFiles['MonthlyStats'] = $dir . 'MonthlyStats.msg.php';
$wgSpecialPages['MonthlyStats'] = 'MonthlyStats';

$version = '$Change: 8385 $';
$version = substr( $version, 9, -2 );

$wgExtensionCredits['specialpage'][] =
array
(
	'name'   => 'MonthlyStats (@'.$version.')',
	'url'    => 'http://public.perforce.com/wiki/Monthly_Stats',
	'author' => 'Sam Stafford',
	'description' => 'Special page with monthly activity statistics'
);

class MonthlyStats extends SpecialPage
{
	function MonthlyStats()
	{
	    SpecialPage::SpecialPage('MonthlyStats');
	}
	function execute( $param )
	{
	    global $wgOut;
	    global $wgScriptPath;
	    $this->setHeaders();
	    $wgOut->setPagetitle( 'Monthly Stats' );

	    $args = explode( '@@', $param );
	    $par = $args[0];
	    if ( $par == "" ) $par = 1;
	    if ( $par < 1 ) $par = 1;
	    if ( $par > 10 ) $par = 10;
	    $path = $args[1];

	    $maxUsers = 18;  # number of users that can fit in the chart

	    $dates = array();   # n => date
	    $d_cur = getdate();
	    $d_end = $d_cur;
	    $d_cur["year"] = $d_cur["year"] - $par;
	    $t_cur = mktime( 0, 0, 0, $d_cur["mon"], 1, $d_cur["year"] );
	    $t_end = mktime( 0, 0, 0, $d_end["mon"], 1, $d_end["year"] );
	    $t_begin = $t_cur;
	    $s_begin = date( 'Ym00000000', $t_begin );
	    $i = 0;
	    while ( $t_cur < $t_end )
	    {
	        $dates[$i] = $t_cur;
	        $i++;

	        $d_cur["mon"] = $d_cur["mon"] + 1;
	        if ( $d_cur["mon"] > 12 )
	        {
	            $d_cur["year"] = $d_cur["year"] + 1;
	            $d_cur["mon"] = 1;
	        }
	        $t_cur = mktime( 0, 0, 0, $d_cur["mon"], 1, $d_cur["year"] );
	    }
	    $dates[$i] = $t_cur;
	    $i_max = $i;  # This is the current month; $i_max-1 is last month.

	    $db = wfGetDB( DB_SLAVE );
	    $sql = "SELECT rev_user_text,rev_timestamp FROM ".
	           $db->tableName('revision').
	           " WHERE rev_timestamp>$s_begin".
                   " ORDER BY rev_timestamp";
	    $res = $db->query($sql, __METHOD__);

	    $userPages = array();   # user => ( n => count )
	    $totalPages = array();   # n => counts
	    $i = 0;
	    $maxPages = 0;
	    for ($j=0; $j<$db->numRows($res); $j++)
	    {
	        $row = $db->fetchRow($res);

	        $u = $row[0];

	        # Figure out which date bucket this row falls into.
	        while ( $i < $i_max && date( 'Ym00000000', $dates[$i+1] ) <  $row[1] )
	        {
	            $i++;
	        }

	        if ( !isset( $userPages[$u] ) ) $userPages[$u] = array();
	        if ( !isset( $userPages[$u][$i] ) ) $userPages[$u][$i] = 0;
	        if ( !isset( $totalPages[$i] ) ) $totalPages[$i] = 0;
	        $userPages[$u][$i]++;
	        $totalPages[$i]++;
	        if ( $totalPages[$i] > $maxPages ) $maxPages = $totalPages[$i];
	    }
	    ksort( $userPages );
	    
	    # Figure out which users are worth showing.
	    $tempUsers = array();   # temp user list
	    $tempCounts = array();  # corresponding edit counts
	    foreach( $userPages as $u => $a )
	    {
	        $tempUsers[] = $u;
	        $tempCounts[] = max($a);
	    }
	    array_multisort( $tempCounts, $tempUsers );
	    foreach( $tempUsers as $i => $u )
	    {
	        if ( count( $tempUsers ) <= $maxUsers ) break;
	        if ( $userPages[$u][$i_max] || $userPages[$u][$i_max-1] ) continue;
	        unset( $tempUsers[$i] );
	    }
	    $tempUsers = array_slice( $tempUsers, -$maxUsers );
	    # If it's in tempUsers it's worth showing.

	    foreach( $userPages as $u => $a )
	    {
	        if ( !in_array( $u, $tempUsers ) )
	        {
	            if ( !isset( $userPages['(other)'] ) )
	                $userPages['(other)'] = array_fill( 0, $i_max + 1, 0 );
	            $userPages['(other)'] = 
	                array_sum_values( $userPages['(other)'], $a );
	            unset( $userPages[$u] );
	        }
	    }

	    global $wgP4EXEC;
	    global $wgP4PORT;
	    global $wgP4USER;
	    global $wgP4PASSWD;	
	    global $wgP4WEBURL;
	    if ( $wgP4EXEC != "" )
	    {
	    $cmdline = $wgP4EXEC .
	        " -u " . $wgP4USER .
	        " -p " . $wgP4PORT ;
	    if ( $wgP4PASSWD != "" )
	        $cmdline .= " -P " . $wgP4PASSWD ;
	    $cmdline .= ' changes ';
	    if ( $path != '' ) $cmdline .= '-i ';
	    $cmdline .= $path.'@'.date( 'Y/m/01', $dates[0] ).',@now';
	    $cmdline .= ' 2>&1';

	    $userChanges = array();  # user => ( n => count )
	    $totalChanges = array(); # n => counts
	    $i = 0;
	    $maxChanges = 0;

	    $changes = array();
	    exec( $cmdline, $changes );
	    $changes = array_reverse( $changes );
	    foreach( $changes as $c )
	    {
	        $cmatch = array();
	        if ( !preg_match( '/Change \d+ on (\d+)\/(\d+)\/(\d+) by (\S+)@\S+ /',
	                          $c, $cmatch ) ) { continue; }

	        $u = $cmatch[4];

	        #Find the date bucket.
	        $dc = $cmatch[1].$cmatch[2].$cmatch[3];
	        if ( $dc < date( 'Ym00', $dates[0] ) ) continue;
	        while( $i < $i_max && date( 'Ym00', $dates[$i+1] ) < $dc )
	        {
	            $i++;
	        }

	        if ( !isset( $userChanges[$u] ) ) $userChanges[$u] = array();
	        if ( !isset( $userChanges[$u][$i] ) ) $userChanges[$u][$i] = 0;
	        if ( !isset( $totalChanges[$i] ) ) $totalChanges[$i] = 0;
	        $userChanges[$u][$i]++;
	        $totalChanges[$i]++;
	        if ( $totalChanges[$i] > $maxChanges ) $maxChanges = $totalChanges[$i];
	    }
	    ksort( $userChanges );

	    # Figure out which users are worth showing.
	    $tempUsers = array();   # temp user list
	    $tempCounts = array();  # corresponding edit counts
	    foreach( $userChanges as $u => $a )
	    {
	        $tempUsers[] = $u;
	        $tempCounts[] = max($a);
	    }
	    array_multisort( $tempCounts, $tempUsers );
	    foreach( $tempUsers as $i => $u )
	    {
	        if ( count( $tempUsers ) <= $maxUsers ) break;
	        if ( $userChanges[$u][$i_max] || $userChanges[$u][$i_max-1] ) continue;
	        unset( $tempUsers[$i] );
	    }
	    $tempUsers = array_slice( $tempUsers, -$maxUsers );
	    # If it's in tempUsers it's worth showing.

	    foreach( $userChanges as $u => $a )
	    {
	        if ( !in_array( $u, $tempUsers ) )
	        {
	            if ( !isset( $userChanges['(other)'] ) )
	                $userChanges['(other)'] = array_fill( 0, $i_max + 1, 0 );
	            $userChanges['(other)'] = 
	                array_sum_values( $userChanges['(other)'], $a );
	            unset( $userChanges[$u] );
	        }
	    }
	    } #end wgP4EXEC block

	    # Generate colors for users.
	    $userColors = array();
	    foreach ( $userPages as $u => $a )
	        $userColors[unorm($u)] = '000000';
	    if ( isset( $wgP4EXEC ) )
	        foreach ( $userChanges as $u => $a )
	            $userColors[unorm($u)] = '000000';
	    if ( isset( $userColors['(other)'] ) )
	    {
	        $other = 'DDDDDD';
	        unset($userColors['(other)']);
	    }
	    ksort( $userColors );
	    $i = 0;
	    $colorCount = count( $userColors );
	    
	    foreach( $userColors as $u => $c )
	    {
	        $userColors[$u] = HSV_TO_RGB
	           ( 0.5 + ( $i * 1.0 / count( $userColors ) ), 0.4, 0.9 );
	        $i++;
	    }
	    if ( isset( $other ) ) $userColors['(other)'] = $other;

	    # Wiki stats chart.
	    $height = 200;
	    if ( count( $userPages ) * 20 > $height )
	        $height = count( $userPages ) * 20;
	    if ( $height > 360 ) $height = 360;
	    $url = 'http://chart.apis.google.com/chart?';
	    $url .= 'chs=800x'.$height.'&amp;';
	    $url .= 'cht=bvs&amp;chbh=a&amp;';
	    $url .= 'chds=0,'.$maxPages.'&amp;';
	    $url .= 'chxt=y,x&amp;';
	    $url .= 'chma=9,150,9,9&amp;chldp=r&amp;';

	    $url .= 'chxr=0,0,'.$maxPages.'&amp;';
	    $url .= 'chxl=1:|';
	    $interval = $i_max / 12;
	    if ( $interval == 0 ) $interval = 1;
	    for( $i = 0 ; $i < $i_max ; $i++ )
	    {
	        if ( ( $i + 1 ) % ( $interval ) == 0 )
	            $url .= date( 'M y', $dates[$i] );
	        $url .= '|';
	    }
	    $url = substr( $url, 0, -1 );
	    $url .= '&amp;';

	    $data = 'chd=t:';
	    foreach ( $userPages as $u => $a )
	    {
	        for ( $i = 0 ; $i < $i_max ; $i++ )
	        {
	            if ( !isset( $a[$i] ) ) $a[$i] = 0;
	            $data .= "$a[$i],";
	        }
	        $data = substr( $data, 0, -1 );
	        $data .= '|';
	    }
	    $data = substr( $data, 0, -1 );
	    $data .= '&amp;';

	    $data .= 'chco=';
	    foreach ( $userPages as $u => $a )
	    {
	        $data .= $userColors[unorm($u)];
	        $data .= ',';
	    }
	    $data = substr( $data, 0, -1 );
	    $data .= '&amp;';

	    $data .= 'chdl=';
	    foreach( $userPages as $u => $a )
	    {
	        $data .= $u;
	        $data .= '|';
	    }
	    $data = substr( $data, 0, -1 );

	    $data = str_replace( ' ', '%20', $data );
	    if ( strlen( $url ) + strlen( $data ) > 2048 )
	    {
	        $data = 'chd=t:';
	        for( $i = 0 ; $i < $i_max ; $i++ )
	        {
	            if ( !isset( $totalPages[$i] ) )
	                $totalPages[$i] = 0;
	            $data .= "$totalPages[$i],";
	        }
	        $data = substr( $data, 0, -1 );
	        $data .= '&amp;';
	        $data .= 'chdl=All Users';
	        $url = str_replace( 'chs=800x'.$height.'&amp;', 'chs=800x200&amp;', $url );
	    }
	    $mwa = $url.$data;

	    #Last month's wiki edits (pie)
	    $i = $i_max - 1;
	    $mwl = 'http://chart.apis.google.com/chart?';
	    $mwl .= 'chs=390x100&amp;';
	    $mwl .= 'cht=p3&amp;';
	    $mwl .= 'chtt='.date('M y',$dates[$i]).'&amp;';
	    $dat = "";
	    $col = "";
	    $lab = "";
	    foreach ( $userPages as $u => $a )
	    {
	        if ( !isset( $a[$i] ) || !$a[$i] ) continue;
	        $dat .= "$a[$i],";
	        $col .= $userColors[unorm($u)].",";
	        $lab .= "$u ($a[$i])|";
	    }
	    if ( strlen( $dat ) )
	    {
	        $dat = substr( $dat, 0, -1 );
	        $col = substr( $col, 0, -1 );
	        $lab = substr( $lab, 0, -1 );
	    }
	    else
	    {
	        $dat = '1';
	        $col = 'EEEEEE';
	        $lab = 'No pages edited';
	    }
	    $mwl .= 'chd=t:'.$dat.'&amp;';
	    $mwl .= 'chco='.$col.'&amp;';
	    $mwl .= 'chl='.$lab;

	    $mwlu = $wgScriptPath.'/index.php?title=Special:Recentchanges';
	    $mwlu .= '&amp;from='.date( 'Ym01000000', $dates[$i] );
	    $mwlu .= '&amp;days=60&amp;limit=500';

	    #This month's wiki edits to date (pie)
	    $i = $i_max;
	    $mwt = 'http://chart.apis.google.com/chart?';
	    $mwt .= 'chs=390x100&amp;';
	    $mwt .= 'cht=p3&amp;';
	    $mwt .= 'chtt='.date('M y',$dates[$i]).' to date&amp;';
	    $dat = "";
	    $col = "";
	    $lab = "";
	    foreach ( $userPages as $u => $a )
	    {
	        if ( !isset( $a[$i] ) || !$a[$i] ) continue;
	        $dat .= "$a[$i],";
	        $col .= $userColors[unorm($u)].",";
	        $lab .= "$u ($a[$i])|";
	    }
	    if ( strlen( $dat ) )
	    {
	        $dat = substr( $dat, 0, -1 );
	        $col = substr( $col, 0, -1 );
	        $lab = substr( $lab, 0, -1 );
	    }
	    else
	    {
	        $dat = '1';
	        $col = 'EEEEEE';
	        $lab = 'No pages edited yet';
	    }
	    $mwt .= 'chd=t:'.$dat.'&amp;';
	    $mwt .= 'chco='.$col.'&amp;';
	    $mwt .= 'chl='.$lab;

	    $mwtu = $wgScriptPath.'/index.php?title=Special:Recentchanges';
	    $mwtu .= '&amp;from='.date( 'Ym01000000', $dates[$i] );
	    $mwtu .= '&amp;days=30&amp;limit=500';

	    if ( $wgP4EXEC != "" )
	    {
	    # Perforce stats chart.
	    $height = 200;
	    if ( count( $userChanges ) * 20 > $height ) 
	        $height = count( $userChanges ) * 20;
	    if ( $height > 360 ) $height = 360;
	    $url = 'http://chart.apis.google.com/chart?';
	    $url .= 'chs=800x'.$height.'&amp;';
	    $url .= 'cht=bvs&amp;chbh=a&amp;';
	    $url .= 'chds=0,'.$maxChanges.'&amp;';
	    $url .= 'chxt=y,x&amp;';
	    $url .= 'chma=9,150,9,9&amp;chldp=r&amp;';

	    $url .= 'chxr=0,0,'.$maxChanges.'&amp;';
	    $url .= 'chxl=1:|';
	    $interval = $i_max / 12;
	    if ( $interval == 0 ) $interval = 1;
	    for( $i = 0 ; $i < $i_max ; $i++ )
	    {
	        if ( ( $i + 1 ) % ( $interval ) == 0 )
	            $url .= date( 'M y', $dates[$i] );
	        $url .= '|';
	    }
	    $url = substr( $url, 0, -1 );
	    $url .= '&amp;';

	    $data = 'chd=t:';
	    foreach ( $userChanges as $u => $a )
	    {
	        for ( $i = 0 ; $i < $i_max ; $i++ )
	        {
	            if ( !isset( $a[$i] ) ) $a[$i] = 0;
	            $data .= "$a[$i],";
	        }
	        $data = substr( $data, 0, -1 );
	        $data .= '|';
	    }
	    $data = substr( $data, 0, -1 );
	    $data .= '&amp;';

	    $data .= 'chco=';
	    foreach ( $userChanges as $u => $a )
	    {
	        $data .= $userColors[unorm($u)];
	        $data .= ',';
	    }
	    $data = substr( $data, 0, -1 );
	    $data .= '&amp;';

	    $data .= 'chdl=';
	    foreach( $userChanges as $u => $a )
	    {
	        $data .= $u;
	        $data .= '|';
	    }
	    $data = substr( $data, 0, -1 );

	    $data = str_replace( ' ', '%20', $data );
	    if ( strlen( $url ) + strlen( $data ) > 2048 )
	    {
	        $data = 'chd=t:';
	        for( $i = 0 ; $i < $i_max ; $i++ )
	        {
	            if ( !isset( $totalChanges[$i] ) )
	                $totalChanges[$i] = 0;
	            $data .= "$totalChanges[$i],";
	        }
	        $data = substr( $data, 0, -1 );
	        $data .= '&amp;';
	        $data .= 'chdl=All Users';
	        $url = str_replace( 'chs=800x'.$height.'&amp;', 'chs=800x200&amp;', $url );
	    }
	    $p4a = $url.$data;

	    #Last month's Perforce changes (pie)
	    $i = $i_max - 1;
	    $p4l = 'http://chart.apis.google.com/chart?';
	    $p4l .= 'chs=390x100&amp;';
	    $p4l .= 'cht=p3&amp;';
	    $p4l .= 'chtt='.date('M y',$dates[$i]).'&amp;';
	    $dat = "";
	    $col = "";
	    $lab = "";
	    foreach ( $userChanges as $u => $a )
	    {
	        if ( !isset( $a[$i] ) || !$a[$i] ) continue;
	        $dat .= "$a[$i],";
	        $col .= $userColors[unorm($u)].",";
	        $lab .= "$u ($a[$i])|";
	    }
	    if ( strlen( $dat ) )
	    {
	        $dat = substr( $dat, 0, -1 );
	        $col = substr( $col, 0, -1 );
	        $lab = substr( $lab, 0, -1 );
	    }
	    else
	    {
	        $dat = '1';
	        $col = 'EEEEEE';
	        $lab = 'No changes submitted';
	    }
	    $p4l .= 'chd=t:'.$dat.'&amp;';
	    $p4l .= 'chco='.$col.'&amp;';
	    $p4l .= 'chl='.$lab;

	    $p4lu = $wgP4WEBURL . '?ac=43&amp;sr=';
	    $p4lu .= date( 'Y/m/01', $dates[$i] ) . '&amp;sr2=';
	    $p4lu .= date( 'Y/m/00', $dates[$i+1] );
	    $p4lu .= '&amp;al=y';

	    #This month's Perforce changes (pie)
	    $i = $i_max;
	    $p4t = 'http://chart.apis.google.com/chart?';
	    $p4t .= 'chs=390x100&amp;';
	    $p4t .= 'cht=p3&amp;';
	    $p4t .= 'chtt='.date('M y',$dates[$i]).' to date&amp;';
	    $dat = "";
	    $col = "";
	    $lab = "";
	    foreach ( $userChanges as $u => $a )
	    {
	        if ( !isset( $a[$i] ) || !$a[$i] ) continue;
	        $dat .= "$a[$i],";
	        $col .= $userColors[unorm($u)].",";
	        $lab .= "$u ($a[$i])|";
	    }
	    if ( strlen( $dat ) )
	    {
	        $dat = substr( $dat, 0, -1 );
	        $col = substr( $col, 0, -1 );
	        $lab = substr( $lab, 0, -1 );
	    }
	    else
	    {
	        $dat = '1';
	        $col = 'EEEEEE';
	        $lab = 'No changes submitted yet';
	    }
	    $p4t .= 'chd=t:'.$dat.'&amp;';
	    $p4t .= 'chco='.$col.'&amp;';
	    $p4t .= 'chl='.$lab;

	    $p4tu = $wgP4WEBURL . '?ac=43&amp;sr=';
	    $p4tu .= date( 'Y/m/01', $dates[$i] ) . '&amp;sr2=now';
	    $p4tu .= '&amp;al=y';
	    } #end P4EXEC check

	    $wgOut->addHTML('<h2>Wiki Page Edits by All Users</h2>'."\n");
	    $wgOut->addHTML('<p><a href="'.$mwtu.'"><img src="'.$mwt.'"/></a>');
	    $wgOut->addHTML('<a href="'.$mwlu.'"><img src="'.$mwl.'"/></a></p>'."\n");
	    $wgOut->addHTML('<p><img src="'.$mwa.'"/></p>'."\n");
	    if ( $wgP4EXEC != "" )
	    {
	    $wgOut->addHTML('<p><br/><br/></p>'."\n");
	    $wgOut->addHTML('<h2>Perforce Changelist Submissions by All Users</h2>'."\n");
	    $wgOut->addHTML('<p><a href="'.$p4tu.'"><img src="'.$p4t.'"/></a>');
	    $wgOut->addHTML('<a href="'.$p4lu.'"><img src="'.$p4l.'"/></a></p>'."\n");
	    $wgOut->addHTML('<p><img src="'.$p4a.'"/></p>'."\n");
	    }
	}
}

function unorm( $u )
# Normalization function for folding wiki and Perforce usernames together.
# This works nicely for the Perforce Public Depot; your mileage may vary.
{
	return strtr( strtolower( $u ), '_', ' ' );
}

# Slightly tweaked function from actionscript.org forum poster petefs.
function HSV_TO_RGB ($H, $S, $V) // HSV Values:Number 0-1
{

while ( $H < 0 ) { $H++; }
while ( $H > 1 ) { $H--; }

if($S == 0)
{
$R = $G = $B = $V * 255;
}
else
{
$var_H = $H * 6;
$var_i = floor( $var_H );
$var_1 = $V * ( 1 - $S );
$var_2 = $V * ( 1 - $S * ( $var_H - $var_i ) );
$var_3 = $V * ( 1 - $S * (1 - ( $var_H - $var_i ) ) );

if ($var_i == 0) { $var_R = $V ; $var_G = $var_3 ; $var_B = $var_1 ; }
else if ($var_i == 1) { $var_R = $var_2 ; $var_G = $V ; $var_B = $var_1 ; }
else if ($var_i == 2) { $var_R = $var_1 ; $var_G = $V ; $var_B = $var_3 ; }
else if ($var_i == 3) { $var_R = $var_1 ; $var_G = $var_2 ; $var_B = $V ; }
else if ($var_i == 4) { $var_R = $var_3 ; $var_G = $var_1 ; $var_B = $V ; }
else { $var_R = $V ; $var_G = $var_1 ; $var_B = $var_2 ; }

$R = $var_R * 255;
$G = $var_G * 255;
$B = $var_B * 255;
}

return strtoupper( dechex($R).dechex($G).dechex($B) );
}

#From Tobias Schlemmer at php.net.
/**
 * Sums the values of the arrays be there keys (PHP 4, PHP 5) 
 * array array_sum_values ( array array1 [, array array2 [, array ...]] )
 */
function array_sum_values() {
    $return = array();
    $intArgs = func_num_args();
    $arrArgs = func_get_args();
    if($intArgs < 1) trigger_error('Warning: Wrong parameter count for array_sum_values()', E_USER_WARNING);
    
    foreach($arrArgs as $arrItem) {
        if(!is_array($arrItem)) trigger_error('Warning: Wrong parameter values for array_sum_values()', E_USER_WARNING);
        foreach($arrItem as $k => $v) {
            $return[$k] += $v;
        }
    }
    return $return;
}
# Change User Description Committed
#2 8385 Matt Attaway Yet another require_once we don't need with modern Mediawiki installs
#1 8318 Matt Attaway Get the latest changes from @sam_stafford's version of the Mediawiki plugin
//guest/sam_stafford/mediawiki/extensions/MonthlyStats/MonthlyStats.php
#10 7308 Sam Stafford Just for fun, added a way to specify a path for the changelist query in
Special:MonthlyStats.  With the "-i" option this actually generates
some interesting pictures of overall contributions to a project branch.
 Might break this out into a distinct parser function at some point so
changelist bar charts can be inserted into project or user pages.
#9 7303 Sam Stafford Replace URL encoding with more selective " "/"%20" replacement.
 Full
URL encoding seems a little more aggressive than what is necessary and
causes the character limit to get overrun faster than it should.
#8 7302 Sam Stafford URL-encode chart data before checking it for the 2048-character limit.
#7 7301 Sam Stafford Fine-tune the logic for which users are color-coded and listed in the
graphs and which aren't.  Rather than a fixed percent, users are now
sorted by greatest number of submissions in a month and the top N are
selected over two passes (the first with exemptions for users active in
the pie chart months, the second without if the first wasn't enough to
whittle it down to N). 

N is currently set to 18 since that seems to be just enough to fill up
the bar chart legend.
#6 7299 Sam Stafford Have Monthly Stats default to a 1 year period instead of 2.
#5 7122 Sam Stafford Fix off-by-one error that was messing up the stats for this month.
Not sure why I didn't see this problem last month.  Need to keep an eye
on it.
#4 7111 Sam Stafford Clump users with relatively low counts into a single "(other)" bucket.
 
This potentially cuts down the number of data sets for the bar graph
way down, and therefore increases the odds that it'll be possible to
show the per-user bar graph.

Currently the criteria for clumping a user are:
1) No changes/edits in the last or current month (so pie charts will
never show "other").
2) No bar in the chart would be more than 1% of the overall chart
height.

1% is pretty low, so another pass at this might be to raise the bar
iteratively until only a certain number of users (TBD) are left
standing.
#3 7110 Sam Stafford Add links to appropriate Special:Recentchanges and P4Web pages from
the pie charts.
#2 7109 Sam Stafford Add a bit of logic to scale the height of the bar chart by the number of users
in the legend.
#1 7108 Sam Stafford New Monthly Stats extension.