Merge branch 'next'

This commit is contained in:
Sebastian Grewe 2013-10-18 07:31:09 +02:00
commit 1437a9eae1
13 changed files with 586 additions and 827 deletions

View File

@ -34,7 +34,7 @@ if ( $bitcoin->can_connect() !== true ) {
} }
// Fetch all unconfirmed blocks // Fetch all unconfirmed blocks
$aAllBlocks = $block->getAllUnconfirmed($config['confirmations']); $aAllBlocks = $block->getAllUnconfirmed(max($config['network_confirmations'],$config['confirmations']));
$log->logInfo("ID\tHeight\tBlockhash\tConfirmations"); $log->logInfo("ID\tHeight\tBlockhash\tConfirmations");
foreach ($aAllBlocks as $iIndex => $aBlock) { foreach ($aAllBlocks as $iIndex => $aBlock) {

View File

@ -51,7 +51,7 @@ foreach ($aAllBlocks as $iIndex => $aBlock) {
$pplns_target = $config['pplns']['shares']['default']; $pplns_target = $config['pplns']['shares']['default'];
} }
if (!$aBlock['accounted']) { if (!$aBlock['accounted'] && $aBlock['height'] > $setting->getValue('last_accounted_block_height')) {
$iPreviousShareId = @$aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0; $iPreviousShareId = @$aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0;
$iCurrentUpstreamId = $aBlock['share_id']; $iCurrentUpstreamId = $aBlock['share_id'];
if (!is_numeric($iCurrentUpstreamId)) { if (!is_numeric($iCurrentUpstreamId)) {
@ -175,6 +175,9 @@ foreach ($aAllBlocks as $iIndex => $aBlock) {
$log->logFatal('Failed to insert new Donation transaction to database for ' . $aData['username']); $log->logFatal('Failed to insert new Donation transaction to database for ' . $aData['username']);
} }
// Store this blocks height as last accounted for
$setting->setValue('last_accounted_block_height', $aBlock['height']);
// Move counted shares to archive before this blockhash upstream share // Move counted shares to archive before this blockhash upstream share
if (!$share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId)) if (!$share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId))
$log->logError('Failed to copy shares to archive table'); $log->logError('Failed to copy shares to archive table');
@ -191,6 +194,22 @@ foreach ($aAllBlocks as $iIndex => $aBlock) {
$monitoring->setStatus($cron_name . "_status", "okerror", 1); $monitoring->setStatus($cron_name . "_status", "okerror", 1);
exit(1); exit(1);
} }
} else {
$aMailData = array(
'email' => $setting->getValue('website_email'),
'subject' => 'Payout processing aborted',
'Error' => 'Potential double payout detected. All payouts halted until fixed!',
'BlockID' => $aBlock['id'],
'Block Height' => $aBlock['height'],
'Block Share ID' => $aBlock['share_id']
);
if (!$mail->sendMail('notifications/error', $aMailData))
$log->logError(" Failed sending notifications: " . $notification->getError() . "\n");
$log->logFatal('Potential double payout detected. Aborted.');
$monitoring->setStatus($cron_name . "_active", "yesno", 0);
$monitoring->setStatus($cron_name . "_message", "message", "Block height for block too low! Potential double payout detected.");
$monitoring->setStatus($cron_name . "_status", "okerror", 1);
exit(1);
} }
} }

View File

@ -45,7 +45,7 @@ $count = 0;
// Table header for account shares // Table header for account shares
$log->logInfo("ID\tUsername\tValid\tInvalid\tPercentage\tPayout\t\tDonation\tFee"); $log->logInfo("ID\tUsername\tValid\tInvalid\tPercentage\tPayout\t\tDonation\tFee");
foreach ($aAllBlocks as $iIndex => $aBlock) { foreach ($aAllBlocks as $iIndex => $aBlock) {
if (!$aBlock['accounted']) { if (!$aBlock['accounted'] && $aBlock['height'] > $setting->getValue('last_accounted_block_height')) {
$iPreviousShareId = @$aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0; $iPreviousShareId = @$aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0;
$iCurrentUpstreamId = $aBlock['share_id']; $iCurrentUpstreamId = $aBlock['share_id'];
$aAccountShares = $share->getSharesForAccounts($iPreviousShareId, $aBlock['share_id']); $aAccountShares = $share->getSharesForAccounts($iPreviousShareId, $aBlock['share_id']);
@ -82,7 +82,7 @@ foreach ($aAllBlocks as $iIndex => $aBlock) {
number_format($aData['percentage'], 8) . "\t" . number_format($aData['percentage'], 8) . "\t" .
number_format($aData['payout'], 8) . "\t" . number_format($aData['payout'], 8) . "\t" .
number_format($aData['donation'], 8) . "\t" . number_format($aData['donation'], 8) . "\t" .
number_format($aData['fee']), 8); number_format($aData['fee'], 8));
// Update user share statistics // Update user share statistics
if (!$statistics->updateShareStatistics($aData, $aBlock['id'])) if (!$statistics->updateShareStatistics($aData, $aBlock['id']))
@ -100,6 +100,9 @@ foreach ($aAllBlocks as $iIndex => $aBlock) {
$log->logFatal('Failed to insert new Donation transaction to database for ' . $aData['username']); $log->logFatal('Failed to insert new Donation transaction to database for ' . $aData['username']);
} }
// Add block as accounted for into settings table
$setting->setValue('last_accounted_block_height', $aBlock['height']);
// Move counted shares to archive before this blockhash upstream share // Move counted shares to archive before this blockhash upstream share
if (!$share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId)) if (!$share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId))
$log->logError('Failed to copy shares to archive'); $log->logError('Failed to copy shares to archive');
@ -119,6 +122,22 @@ foreach ($aAllBlocks as $iIndex => $aBlock) {
$monitoring->setStatus($cron_name . "_status", "okerror", 1); $monitoring->setStatus($cron_name . "_status", "okerror", 1);
exit(1); exit(1);
} }
} else {
$log->logFatal('Possible double payout detected. Aborted.');
$aMailData = array(
'email' => $setting->getValue('website_email'),
'subject' => 'Payout Failure: Double Payout',
'Error' => 'Possible double payout detected',
'BlockID' => $aBlock['id'],
'Block Height' => $aBlock['height'],
'Block Share ID' => $aBlock['share_id']
);
if (!$mail->sendMail('notifications/error', $aMailData))
$log->logError(" Failed sending notifications: " . $notification->getError() . "\n");
$monitoring->setStatus($cron_name . "_active", "yesno", 0);
$monitoring->setStatus($cron_name . "_message", "message", 'Possible double payout detected. Aborted.');
$monitoring->setStatus($cron_name . "_status", "okerror", 1);
exit(1);
} }
} }

View File

@ -242,7 +242,12 @@ class Statistics {
public function getUserShares($account_id) { public function getUserShares($account_id) {
$this->debug->append("STA " . __METHOD__, 4); $this->debug->append("STA " . __METHOD__, 4);
// Dual-caching, try statistics cron first, then fallback to local, then fallbock to SQL // Dual-caching, try statistics cron first, then fallback to local, then fallbock to SQL
if ($data = $this->memcache->get(STATISTICS_ALL_USER_SHARES)) return @$data['data'][$account_id]; if ($data = $this->memcache->get(STATISTICS_ALL_USER_SHARES)) {
if (array_key_exists($account_id, $data['data']))
return $data['data'][$account_id];
// We have no cached value, we return defaults
return array('valid' => 0, 'invalid' => 0, 'donate_percent' => 0, 'is_anonymous' => 0);
}
if ($data = $this->memcache->get(__FUNCTION__ . $account_id)) return $data; if ($data = $this->memcache->get(__FUNCTION__ . $account_id)) return $data;
$stmt = $this->mysqli->prepare(" $stmt = $this->mysqli->prepare("
SELECT SELECT
@ -529,6 +534,31 @@ class Statistics {
$this->debug->append("Failed to fetch hourly hashrate: " . $this->mysqli->error); $this->debug->append("Failed to fetch hourly hashrate: " . $this->mysqli->error);
return false; return false;
} }
/**
* get user estimated payouts based on share counts
* @param aRoundShares array Round shares
* @param aUserShares array User shares
* @param dDonate double User donation setting
* @param bNoFees bool User no-fees option setting
* @return aEstimates array User estimations
**/
public function getUserEstimates($aRoundShares, $aUserShares, $dDonate, $bNoFees) {
$this->debug->append("STA " . __METHOD__, 4);
// Fetch some user information that we need
if (@$aRoundShares['valid'] > 0 && @$aUserShares['valid'] > 0) {
$aEstimates['block'] = round(( (int)$aUserShares['valid'] / (int)$aRoundShares['valid'] ) * (float)$this->config['reward'], 8);
$bNoFees == 0 ? $aEstimates['fee'] = round(((float)$this->config['fees'] / 100) * (float)$aEstimates['block'], 8) : $aEstimates['fee'] = 0;
$aEstimates['donation'] = round((( (float)$dDonate / 100) * ((float)$aEstimates['block'] - (float)$aEstimates['fee'])), 8);
$aEstimates['payout'] = round((float)$aEstimates['block'] - (float)$aEstimates['donation'] - (float)$aEstimates['fee'], 8);
} else {
$aEstimates['block'] = 0;
$aEstimates['fee'] = 0;
$aEstimates['donation'] = 0;
$aEstimates['payout'] = 0;
}
return $aEstimates;
}
} }
$statistics = new Statistics($debug, $mysqli, $config, $share, $user, $block, $memcache); $statistics = new Statistics($debug, $mysqli, $config, $share, $user, $block, $memcache);

View File

@ -46,15 +46,18 @@ class User {
public function getUserNameByEmail($email) { public function getUserNameByEmail($email) {
return $this->getSingle($email, 'username', 'email', 's'); return $this->getSingle($email, 'username', 'email', 's');
} }
public function getUserId($username) { public function getUserId($username, $lower=false) {
return $this->getSingle($username, 'id', 'username', 's'); return $this->getSingle($username, 'id', 'username', 's', $lower);
} }
public function getUserEmail($username) { public function getUserEmail($username, $lower=false) {
return $this->getSingle($username, 'email', 'username', 's'); return $this->getSingle($username, 'email', 'username', 's', $lower);
} }
public function getUserNoFee($id) { public function getUserNoFee($id) {
return $this->getSingle($id, 'no_fees', 'id'); return $this->getSingle($id, 'no_fees', 'id');
} }
public function getUserDonatePercent($id) {
return $this->getDonatePercent($id);
}
public function getUserAdmin($id) { public function getUserAdmin($id) {
return $this->getSingle($id, 'is_admin', 'id'); return $this->getSingle($id, 'is_admin', 'id');
} }
@ -130,7 +133,7 @@ class User {
return false; return false;
} }
if (filter_var($username, FILTER_VALIDATE_EMAIL)) { if (filter_var($username, FILTER_VALIDATE_EMAIL)) {
$this->debug->append("Username is an e-mail", 2); $this->debug->append("Username is an e-mail: $username", 2);
if (!$username = $this->getUserNameByEmail($username)) { if (!$username = $this->getUserNameByEmail($username)) {
$this->setErrorMessage("Invalid username or password."); $this->setErrorMessage("Invalid username or password.");
return false; return false;
@ -179,9 +182,12 @@ class User {
* @param type string Type of value * @param type string Type of value
* @return array Return result * @return array Return result
**/ **/
private function getSingle($value, $search='id', $field='id', $type="i") { private function getSingle($value, $search='id', $field='id', $type="i", $lower=false) {
$this->debug->append("STA " . __METHOD__, 4); $this->debug->append("STA " . __METHOD__, 4);
$stmt = $this->mysqli->prepare("SELECT $search FROM $this->table WHERE $field = ? LIMIT 1"); $sql = "SELECT $search FROM $this->table WHERE";
$lower ? $sql .= " LOWER($field) = LOWER(?)" : $sql .= " $field = ?";
$sql .= " LIMIT 1";
$stmt = $this->mysqli->prepare($sql);
if ($this->checkStmt($stmt)) { if ($this->checkStmt($stmt)) {
$stmt->bind_param($type, $value); $stmt->bind_param($type, $value);
$stmt->execute(); $stmt->execute();
@ -388,16 +394,13 @@ class User {
$this->debug->append("STA " . __METHOD__, 4); $this->debug->append("STA " . __METHOD__, 4);
$user = array(); $user = array();
$password_hash = $this->getHash($password); $password_hash = $this->getHash($password);
$stmt = $this->mysqli->prepare("SELECT username, id, is_admin FROM $this->table WHERE username=? AND pass=? LIMIT 1"); $stmt = $this->mysqli->prepare("SELECT username, id, is_admin FROM $this->table WHERE LOWER(username) = LOWER(?) AND pass = ? LIMIT 1");
if ($this->checkStmt($stmt)) { if ($this->checkStmt($stmt) && $stmt->bind_param('ss', $username, $password_hash) && $stmt->execute() && $stmt->bind_result($row_username, $row_id, $row_admin)) {
$stmt->bind_param('ss', $username, $password_hash);
$stmt->execute();
$stmt->bind_result($row_username, $row_id, $row_admin);
$stmt->fetch(); $stmt->fetch();
$stmt->close(); $stmt->close();
// Store the basic login information // Store the basic login information
$this->user = array('username' => $row_username, 'id' => $row_id, 'is_admin' => $row_admin); $this->user = array('username' => $row_username, 'id' => $row_id, 'is_admin' => $row_admin);
return $username === $row_username; return strtolower($username) === strtolower($row_username);
} }
return false; return false;
} }
@ -562,7 +565,7 @@ class User {
$username_clean = strip_tags($username); $username_clean = strip_tags($username);
if ($this->checkStmt($stmt) && $stmt->bind_param('sssssi', $username_clean, $password_hash, $email1, $pin_hash, $apikey_hash, $is_locked) && $stmt->execute()) { if ($this->checkStmt($stmt) && $stmt->bind_param('sssssi', $username_clean, $password_hash, $email1, $pin_hash, $apikey_hash, $is_locked) && $stmt->execute()) {
if (! $this->setting->getValue('accounts_confirm_email_enabled') && $is_admin != 1) { if (! $this->setting->getValue('accounts_confirm_email_disabled') && $is_admin != 1) {
if ($token = $this->token->createToken('confirm_email', $stmt->insert_id)) { if ($token = $this->token->createToken('confirm_email', $stmt->insert_id)) {
$aData['username'] = $username_clean; $aData['username'] = $username_clean;
$aData['token'] = $token; $aData['token'] = $token;
@ -638,20 +641,27 @@ class User {
$this->serErrorMessage("Username must not be empty"); $this->serErrorMessage("Username must not be empty");
return false; return false;
} }
if (!$aData['email'] = $this->getUserEmail($username)) { if (filter_var($username, FILTER_VALIDATE_EMAIL)) {
$this->debug->append("Username is an e-mail: $username", 2);
if (!$username = $this->getUserNameByEmail($username)) {
$this->setErrorMessage("Invalid username or password.");
return false;
}
}
if (!$aData['email'] = $this->getUserEmail($username, true)) {
$this->setErrorMessage("Unable to find a mail address for user $username"); $this->setErrorMessage("Unable to find a mail address for user $username");
return false; return false;
} }
if (!$aData['token'] = $this->token->createToken('password_reset', $this->getUserId($username))) { if (!$aData['token'] = $this->token->createToken('password_reset', $this->getUserId($username, true))) {
$this->setErrorMessage('Unable to setup token for password reset'); $this->setErrorMessage('Unable to setup token for password reset');
return false; return false;
} }
$aData['username'] = $username; $aData['username'] = $this->getUserName($this->getUserId($username, true));
$aData['subject'] = 'Password Reset Request'; $aData['subject'] = 'Password Reset Request';
if ($this->mail->sendMail('password/reset', $aData)) { if ($this->mail->sendMail('password/reset', $aData)) {
return true; return true;
} else { } else {
$this->setErrorMessage("Unable to send mail to your address"); $this->setErrorMessage('Unable to send mail to your address');
return false; return false;
} }
return false; return false;

View File

@ -37,6 +37,7 @@ $statistics->setGetCache(true);
// Use caches for this one // Use caches for this one
$aUserRoundShares = $statistics->getUserShares($user_id); $aUserRoundShares = $statistics->getUserShares($user_id);
$aRoundShares = $statistics->getRoundShares(); $aRoundShares = $statistics->getRoundShares();
$aEstimates = $statistics->getUserEstimates($aRoundShares, $aUserRoundShares, $user->getUserDonatePercent($user_id), $user->getUserNoFee($user_id));
// Apply pool modifiers // Apply pool modifiers
$dPersonalHashrateAdjusted = $dPersonalHashrate * $dPersonalHashrateModifier; $dPersonalHashrateAdjusted = $dPersonalHashrate * $dPersonalHashrateModifier;
@ -46,7 +47,7 @@ $dNetworkHashrateAdjusted = $dNetworkHashrate / 1000 * $dNetworkHashrateModifier
// Output JSON format // Output JSON format
$data = array( $data = array(
'raw' => array( 'personal' => array( 'hashrate' => $dPersonalHashrate ), 'pool' => array( 'hashrate' => $dPoolHashrate ), 'network' => array( 'hashrate' => $dNetworkHashrate / 1000 ) ), 'raw' => array( 'personal' => array( 'hashrate' => $dPersonalHashrate ), 'pool' => array( 'hashrate' => $dPoolHashrate ), 'network' => array( 'hashrate' => $dNetworkHashrate / 1000 ) ),
'personal' => array ( 'hashrate' => $dPersonalHashrateAdjusted, 'sharerate' => $dPersonalSharerate, 'shares' => $aUserRoundShares, 'balance' => $transaction->getBalance($user_id)), 'personal' => array ( 'hashrate' => $dPersonalHashrateAdjusted, 'sharerate' => $dPersonalSharerate, 'shares' => $aUserRoundShares, 'balance' => $transaction->getBalance($user_id), 'estimates' => $aEstimates),
'pool' => array( 'hashrate' => $dPoolHashrateAdjusted, 'shares' => $aRoundShares ), 'pool' => array( 'hashrate' => $dPoolHashrateAdjusted, 'shares' => $aRoundShares ),
'network' => array( 'hashrate' => $dNetworkHashrateAdjusted, 'difficulty' => $dDifficulty, 'block' => $iBlock ), 'network' => array( 'hashrate' => $dNetworkHashrateAdjusted, 'difficulty' => $dDifficulty, 'block' => $iBlock ),
); );

View File

@ -128,17 +128,11 @@ if (@$_SESSION['USERDATA']['id']) {
switch ($config['payout_system']) { switch ($config['payout_system']) {
case 'prop' || 'pplns': case 'prop' || 'pplns':
// Some estimations // Some estimations
if (@$aRoundShares['valid'] > 0) { $aEstimates = $statistics->getUserEstimates($aRoundShares, $aGlobal['userdata']['shares'], $aGlobal['userdata']['donate_percent'], $aGlobal['userdata']['no_fees']);
$aGlobal['userdata']['est_block'] = round(( (int)$aGlobal['userdata']['shares']['valid'] / (int)$aRoundShares['valid'] ) * (float)$config['reward'], 8); $aGlobal['userdata']['est_block'] = $aEstimates['block'];
$aGlobal['userdata']['no_fees'] == 0 ? $aGlobal['userdata']['est_fee'] = round(((float)$config['fees'] / 100) * (float)$aGlobal['userdata']['est_block'], 8) : $aGlobal['userdata']['est_fee'] = 0; $aGlobal['userdata']['est_fee'] = $aEstimates['fee'];
$aGlobal['userdata']['est_donation'] = round((( (float)$aGlobal['userdata']['donate_percent'] / 100) * ((float)$aGlobal['userdata']['est_block'] - (float)$aGlobal['userdata']['est_fee'])), 8); $aGlobal['userdata']['est_donation'] = $aEstimates['donation'];
$aGlobal['userdata']['est_payout'] = round((float)$aGlobal['userdata']['est_block'] - (float)$aGlobal['userdata']['est_donation'] - (float)$aGlobal['userdata']['est_fee'], 8); $aGlobal['userdata']['est_payout'] = $aEstimates['payout'];
} else {
$aGlobal['userdata']['est_block'] = 0;
$aGlobal['userdata']['est_fee'] = 0;
$aGlobal['userdata']['est_donation'] = 0;
$aGlobal['userdata']['est_payout'] = 0;
}
case 'pplns': case 'pplns':
$aGlobal['pplns']['target'] = $config['pplns']['shares']['default']; $aGlobal['pplns']['target'] = $config['pplns']['shares']['default'];
if ($aLastBlock = $block->getLast()) { if ($aLastBlock = $block->getLast()) {

View File

@ -40,11 +40,13 @@ background: #222222 url(../images/header_bg.png) repeat-x;
} }
header#header h1.site_title, header#header h2.section_title { header#header h1.site_title, header#header h2.section_title {
white-space: nowrap;
float: left; float: left;
margin: 0; margin: 0;
margin-right: 2.8%;
padding-right: 1.8%;
font-size: 22px; font-size: 22px;
display: block; display: inline-block;
width: 23%;
height: 55px; height: 55px;
font-weight: normal; font-weight: normal;
text-align: left; text-align: left;
@ -63,7 +65,6 @@ header#header h2.section_title {
text-align: left; text-align: left;
text-indent: 4.5%; text-indent: 4.5%;
text-transform: uppercase; text-transform: uppercase;
width: 68%;
background: url(../images/header_shadow.png) no-repeat left top; background: url(../images/header_shadow.png) no-repeat left top;
} }
@ -346,7 +347,7 @@ background: #ffffff;
#main .module header h3 { #main .module header h3 {
display: block; display: block;
width: 60%; width: 90%;
float: left; float: left;
} }
@ -649,6 +650,15 @@ padding: 1% 0%;
margin: 10px 0; margin: 10px 0;
} }
fieldset a {
height: 20px;
padding-left: 10px;
display: block;
float: left;
width: 92%;
margin: 0 10px;
}
fieldset label { fieldset label {
display: block; display: block;
float: left; float: left;

View File

@ -1,11 +1,9 @@
/** /*
* -------------------------------------------------------------------- * --------------------------------------------------------------------
* jQuery-Plugin "visualize" * jQuery inputToButton plugin
* by Scott Jehl, scott@filamentgroup.com * Author: Scott Jehl, scott@filamentgroup.com
* http://www.filamentgroup.com
* Copyright (c) 2009 Filament Group * Copyright (c) 2009 Filament Group
* Dual licensed under the MIT (filamentgroup.com/examples/mit-license.txt) and GPL (filamentgroup.com/examples/gpl-license.txt) licenses. * licensed under MIT (filamentgroup.com/examples/mit-license.txt)
*
* -------------------------------------------------------------------- * --------------------------------------------------------------------
*/ */
(function($) { (function($) {
@ -19,239 +17,182 @@ $.fn.visualize = function(options, container){
appendTitle: true, //table caption text is added to chart appendTitle: true, //table caption text is added to chart
title: null, //grabs from table caption if null title: null, //grabs from table caption if null
appendKey: true, //color key is added to chart appendKey: true, //color key is added to chart
rowFilter: '*',
colFilter: '*',
colors: ['#be1e2d','#666699','#92d5ea','#ee8310','#8d10ee','#5a3b16','#26a4ed','#f45a90','#e9e744'], colors: ['#be1e2d','#666699','#92d5ea','#ee8310','#8d10ee','#5a3b16','#26a4ed','#f45a90','#e9e744'],
textColors: [], //corresponds with colors array. null/undefined items will fall back to CSS textColors: [], //corresponds with colors array. null/undefined items will fall back to CSS
parseDirection: 'x', //which direction to parse the table data parseDirection: 'x', //which direction to parse the table data
pieMargin: 10, //pie charts only - spacing around pie pieMargin: 20, //pie charts only - spacing around pie
pieLabelsAsPercent: true, pieLabelsAsPercent: true,
pieLabelPos: 'inside', pieLabelPos: 'inside',
lineWeight: 4, //for line and area - stroke weight lineWeight: 4, //for line and area - stroke weight
lineDots: false, //also available: 'single', 'double'
dotInnerColor: "#ffffff", // only used for lineDots:'double'
lineMargin: (options.lineDots?15:0), //for line and area - spacing around lines
barGroupMargin: 10, barGroupMargin: 10,
chartId: '',
xLabelParser: null, // function to parse labels as values
valueParser: null, // function to parse values. must return a Number
chartId: '',
chartClass: '',
barMargin: 1, //space around bars in bar chart (added to both sides of bar) barMargin: 1, //space around bars in bar chart (added to both sides of bar)
yLabelInterval: 30, //distance between y labels yLabelInterval: 30 //distance between y labels
interaction: false // only used for lineDots != false -- triggers mouseover and mouseout on original table
},options); },options);
//reset width, height to numbers //reset width, height to numbers
o.width = parseFloat(o.width); o.width = parseFloat(o.width);
o.height = parseFloat(o.height); o.height = parseFloat(o.height);
// reset padding if graph is not lines
if(o.type != 'line' && o.type != 'area' ) {
o.lineMargin = 0;
}
var self = $(this); var self = $(this);
// scrape data from html table //function to scrape data from html table
var tableData = {}; function scrapeTable(){
var colors = o.colors; var colors = o.colors;
var textColors = o.textColors; var textColors = o.textColors;
var tableData = {
dataGroups: function(){
var parseLabels = function(direction){ var dataGroups = [];
var labels = [];
if(direction == 'x'){
self.find('thead tr').each(function(i){
$(this).find('th').each(function(j){
if(!labels[j]) {
labels[j] = [];
}
labels[j][i] = $(this).text()
})
});
}
else {
self.find('tbody tr').each(function(i){
$(this).find('th').each(function(j) {
if(!labels[i]) {
labels[i] = [];
}
labels[i][j] = $(this).text()
});
});
}
return labels;
};
var fnParse = o.valueParser || parseFloat;
var dataGroups = tableData.dataGroups = [];
if(o.parseDirection == 'x'){ if(o.parseDirection == 'x'){
self.find('tbody tr').each(function(i,tr){ self.find('tr:gt(0)').filter(o.rowFilter).each(function(i){
dataGroups[i] = {}; dataGroups[i] = {};
dataGroups[i].points = []; dataGroups[i].points = [];
dataGroups[i].color = colors[i]; dataGroups[i].color = colors[i];
if(textColors[i]){ dataGroups[i].textColor = textColors[i]; } if(textColors[i]){ dataGroups[i].textColor = textColors[i]; }
$(tr).find('td').each(function(j,td){ $(this).find('td').filter(o.colFilter).each(function(){
dataGroups[i].points.push( { dataGroups[i].points.push( parseFloat($(this).text()) );
value: fnParse($(td).text()),
elem: td,
tableCords: [i,j]
}); });
}); });
}); }
} else { else {
var cols = self.find('tbody tr:eq(0) td').size(); var cols = self.find('tr:eq(1) td').filter(o.colFilter).size();
for(var i=0; i<cols; i++){ for(var i=0; i<cols; i++){
dataGroups[i] = {}; dataGroups[i] = {};
dataGroups[i].points = []; dataGroups[i].points = [];
dataGroups[i].color = colors[i]; dataGroups[i].color = colors[i];
if(textColors[i]){ dataGroups[i].textColor = textColors[i]; } if(textColors[i]){ dataGroups[i].textColor = textColors[i]; }
self.find('tbody tr').each(function(j){ self.find('tr:gt(0)').filter(o.rowFilter).each(function(){
dataGroups[i].points.push( { dataGroups[i].points.push( $(this).find('td').filter(o.colFilter).eq(i).text()*1 );
value: $(this).find('td').eq(i).text()*1,
elem: this,
tableCords: [i,j]
} );
}); });
}; };
} }
return dataGroups;
var allItems = tableData.allItems = [];
$(dataGroups).each(function(i,row){
var count = 0;
$.each(row.points,function(j,point){
allItems.push(point);
count += point.value;
});
row.groupTotal = count;
});
tableData.dataSum = 0;
tableData.topValue = 0;
tableData.bottomValue = Infinity;
$.each(allItems,function(i,item){
tableData.dataSum += fnParse(item.value);
if(fnParse(item.value,10)>tableData.topValue) {
tableData.topValue = fnParse(item.value,10);
}
if(item.value<tableData.bottomValue) {
tableData.bottomValue = fnParse(item.value);
}
});
var dataSum = tableData.dataSum;
var topValue = tableData.topValue;
var bottomValue = tableData.bottomValue;
var xAllLabels = tableData.xAllLabels = parseLabels(o.parseDirection);
var yAllLabels = tableData.yAllLabels = parseLabels(o.parseDirection==='x'?'y':'x');
var xLabels = tableData.xLabels = [];
$.each(tableData.xAllLabels,function(i,labels) {
tableData.xLabels.push(labels[0]);
});
var totalYRange = tableData.totalYRange = tableData.topValue - tableData.bottomValue;
var zeroLocX = tableData.zeroLocX = 0;
if($.isFunction(o.xLabelParser)) {
var xTopValue = null;
var xBottomValue = null;
$.each(xLabels,function(i,label) {
label = xLabels[i] = o.xLabelParser(label);
if(i === 0) {
xTopValue = label;
xBottomValue = label;
}
if(label>xTopValue) {
xTopValue = label;
}
if(label<xBottomValue) {
xBottomValue = label;
}
});
var totalXRange = tableData.totalXRange = xTopValue - xBottomValue;
var xScale = tableData.xScale = (o.width -2*o.lineMargin) / totalXRange;
var marginDiffX = 0;
if(o.lineMargin) {
var marginDiffX = -2*xScale-o.lineMargin;
}
zeroLocX = tableData.zeroLocX = xBottomValue + o.lineMargin;
tableData.xBottomValue = xBottomValue;
tableData.xTopValue = xTopValue;
tableData.totalXRange = totalXRange;
}
var yScale = tableData.yScale = (o.height - 2*o.lineMargin) / totalYRange;
var zeroLocY = tableData.zeroLocY = (o.height-2*o.lineMargin) * (tableData.topValue/tableData.totalYRange) + o.lineMargin;
var yLabels = tableData.yLabels = [];
var numLabels = Math.floor((o.height - 2*o.lineMargin) / 30);
var loopInterval = tableData.totalYRange / numLabels; //fix provided from lab
loopInterval = Math.round(parseFloat(loopInterval)/5)*5;
loopInterval = Math.max(loopInterval, 1);
// var start =
for(var j=Math.round(parseInt(tableData.bottomValue)/5)*5; j<=tableData.topValue+loopInterval; j+=loopInterval){
yLabels.push(j);
}
if(yLabels[yLabels.length-1] > tableData.topValue+loopInterval) {
yLabels.pop();
} else if (yLabels[yLabels.length-1] <= tableData.topValue-10) {
yLabels.push(tableData.topValue);
}
// populate some data
$.each(dataGroups,function(i,row){
row.yLabels = tableData.yAllLabels[i];
$.each(row.points, function(j,point){
point.zeroLocY = tableData.zeroLocY;
point.zeroLocX = tableData.zeroLocX;
point.xLabels = tableData.xAllLabels[j];
point.yLabels = tableData.yAllLabels[i];
point.color = row.color;
});
});
try{console.log(tableData);}catch(e){}
var charts = {};
charts.pie = {
interactionPoints: dataGroups,
setup: function() {
charts.pie.draw(true);
}, },
draw: function(drawHtml){ allData: function(){
var allData = [];
$(this.dataGroups()).each(function(){
allData.push(this.points);
});
return allData;
},
dataSum: function(){
var dataSum = 0;
var allData = this.allData().join(',').split(',');
$(allData).each(function(){
dataSum += parseFloat(this);
});
return dataSum
},
topValue: function(){
var topValue = 0;
var allData = this.allData().join(',').split(',');
$(allData).each(function(){
if(parseFloat(this,10)>topValue) topValue = parseFloat(this);
});
return topValue;
},
bottomValue: function(){
var bottomValue = 0;
var allData = this.allData().join(',').split(',');
$(allData).each(function(){
if(this<bottomValue) bottomValue = parseFloat(this);
});
return bottomValue;
},
memberTotals: function(){
var memberTotals = [];
var dataGroups = this.dataGroups();
$(dataGroups).each(function(l){
var count = 0;
$(dataGroups[l].points).each(function(m){
count +=dataGroups[l].points[m];
});
memberTotals.push(count);
});
return memberTotals;
},
yTotals: function(){
var yTotals = [];
var dataGroups = this.dataGroups();
var loopLength = this.xLabels().length;
for(var i = 0; i<loopLength; i++){
yTotals[i] =[];
var thisTotal = 0;
$(dataGroups).each(function(l){
yTotals[i].push(this.points[i]);
});
yTotals[i].join(',').split(',');
$(yTotals[i]).each(function(){
thisTotal += parseFloat(this);
});
yTotals[i] = thisTotal;
}
return yTotals;
},
topYtotal: function(){
var topYtotal = 0;
var yTotals = this.yTotals().join(',').split(',');
$(yTotals).each(function(){
if(parseFloat(this,10)>topYtotal) topYtotal = parseFloat(this);
});
return topYtotal;
},
totalYRange: function(){
return this.topValue() - this.bottomValue();
},
xLabels: function(){
var xLabels = [];
if(o.parseDirection == 'x'){
self.find('tr:eq(0) th').filter(o.colFilter).each(function(){
xLabels.push($(this).html());
});
}
else {
self.find('tr:gt(0) th').filter(o.rowFilter).each(function(){
xLabels.push($(this).html());
});
}
return xLabels;
},
yLabels: function(){
var yLabels = [];
yLabels.push(bottomValue);
var numLabels = Math.round(o.height / o.yLabelInterval);
var loopInterval = Math.ceil(totalYRange / numLabels) || 1;
while( yLabels[yLabels.length-1] < topValue - loopInterval){
yLabels.push(yLabels[yLabels.length-1] + loopInterval);
}
yLabels.push(topValue);
return yLabels;
}
};
return tableData;
};
//function to create a chart
var createChart = {
pie: function(){
canvasContain.addClass('visualize-pie');
if(o.pieLabelPos == 'outside'){ canvasContain.addClass('visualize-pie-outside'); }
var centerx = Math.round(canvas.width()/2); var centerx = Math.round(canvas.width()/2);
var centery = Math.round(canvas.height()/2); var centery = Math.round(canvas.height()/2);
var radius = centery - o.pieMargin; var radius = centery - o.pieMargin;
var counter = 0.0; var counter = 0.0;
if(drawHtml) {
canvasContain.addClass('visualize-pie');
if(o.pieLabelPos == 'outside'){ canvasContain.addClass('visualize-pie-outside'); }
var toRad = function(integer){ return (Math.PI/180)*integer; }; var toRad = function(integer){ return (Math.PI/180)*integer; };
var labels = $('<ul class="visualize-labels"></ul>') var labels = $('<ul class="visualize-labels"></ul>')
.insertAfter(canvas); .insertAfter(canvas);
}
//draw the pie pieces //draw the pie pieces
$.each(dataGroups, function(i,row){ $.each(memberTotals, function(i){
var fraction = row.groupTotal / dataSum; var fraction = (this <= 0 || isNaN(this))? 0 : this / dataSum;
if (fraction <= 0 || isNaN(fraction))
return;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(centerx, centery); ctx.moveTo(centerx, centery);
ctx.arc(centerx, centery, radius, ctx.arc(centerx, centery, radius,
@ -263,7 +204,6 @@ $.fn.visualize = function(options, container){
ctx.fillStyle = dataGroups[i].color; ctx.fillStyle = dataGroups[i].color;
ctx.fill(); ctx.fill();
// draw labels // draw labels
if(drawHtml) {
var sliceMiddle = (counter + fraction/2); var sliceMiddle = (counter + fraction/2);
var distance = o.pieLabelPos == 'inside' ? radius/1.5 : radius + radius / 5; var distance = o.pieLabelPos == 'inside' ? radius/1.5 : radius + radius / 5;
var labelx = Math.round(centerx + Math.sin(sliceMiddle * Math.PI * 2) * (distance)); var labelx = Math.round(centerx + Math.sin(sliceMiddle * Math.PI * 2) * (distance));
@ -272,15 +212,8 @@ $.fn.visualize = function(options, container){
var topBottom = (labely > centery) ? 'bottom' : 'top'; var topBottom = (labely > centery) ? 'bottom' : 'top';
var percentage = parseFloat((fraction*100).toFixed(2)); var percentage = parseFloat((fraction*100).toFixed(2));
// interaction variables
row.canvasCords = [labelx,labely];
row.zeroLocY = tableData.zeroLocY = 0; // related to zeroLocY and plugin API
row.zeroLocX = tableData.zeroLocX = 0; // related to zeroLocX and plugin API
row.value = row.groupTotal;
if(percentage){ if(percentage){
var labelval = (o.pieLabelsAsPercent) ? percentage + '%' : row.groupTotal; var labelval = (o.pieLabelsAsPercent) ? percentage + '%' : this;
var labeltext = $('<span class="visualize-label">' + labelval +'</span>') var labeltext = $('<span class="visualize-label">' + labelval +'</span>')
.css(leftRight, 0) .css(leftRight, 0)
.css(topBottom, 0); .css(topBottom, 0);
@ -295,48 +228,26 @@ $.fn.visualize = function(options, container){
.css('margin-'+topBottom, -labeltext.outerHeight()/2); .css('margin-'+topBottom, -labeltext.outerHeight()/2);
if(dataGroups[i].textColor){ labeltext.css('color', dataGroups[i].textColor); } if(dataGroups[i].textColor){ labeltext.css('color', dataGroups[i].textColor); }
}
} }
counter+=fraction; counter+=fraction;
}); });
} },
};
(function(){ line: function(area){
var xInterval;
var drawPoint = function (ctx,x,y,color,size) {
ctx.moveTo(x,y);
ctx.beginPath();
ctx.arc(x,y,size/2,0,2*Math.PI,false);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
};
charts.line = {
interactionPoints: allItems,
setup: function(area){
if(area){ canvasContain.addClass('visualize-area'); } if(area){ canvasContain.addClass('visualize-area'); }
else{ canvasContain.addClass('visualize-line'); } else{ canvasContain.addClass('visualize-line'); }
//write X labels //write X labels
var xInterval = canvas.width() / (xLabels.length -1);
var xlabelsUL = $('<ul class="visualize-labels-x"></ul>') var xlabelsUL = $('<ul class="visualize-labels-x"></ul>')
.width(canvas.width()) .width(canvas.width())
.height(canvas.height()) .height(canvas.height())
.insertBefore(canvas); .insertBefore(canvas);
if(!o.customXLabels) {
xInterval = (canvas.width() - 2*o.lineMargin) / (xLabels.length -1);
$.each(xLabels, function(i){ $.each(xLabels, function(i){
var thisLi = $('<li><span>'+this+'</span></li>') var thisLi = $('<li><span>'+this+'</span></li>')
.prepend('<span class="line" />') .prepend('<span class="line" />')
.css('left', o.lineMargin + xInterval * i) .css('left', xInterval * i)
.appendTo(xlabelsUL); .appendTo(xlabelsUL);
var label = thisLi.find('span:not(.line)'); var label = thisLi.find('span:not(.line)');
var leftOffset = label.width()/-2; var leftOffset = label.width()/-2;
@ -346,96 +257,48 @@ $.fn.visualize = function(options, container){
.css('margin-left', leftOffset) .css('margin-left', leftOffset)
.addClass('label'); .addClass('label');
}); });
} else {
o.customXLabels(tableData,xlabelsUL);
}
//write Y labels //write Y labels
var liBottom = (canvas.height() - 2*o.lineMargin) / (yLabels.length-1); var yScale = canvas.height() / totalYRange;
var liBottom = canvas.height() / (yLabels.length-1);
var ylabelsUL = $('<ul class="visualize-labels-y"></ul>') var ylabelsUL = $('<ul class="visualize-labels-y"></ul>')
.width(canvas.width()) .width(canvas.width())
.height(canvas.height()) .height(canvas.height())
// .css('margin-top',-o.lineMargin) .insertBefore(canvas);
.insertBefore(scroller);
$.each(yLabels, function(i){ $.each(yLabels, function(i){
var value = Math.floor(this); var thisLi = $('<li><span>'+this+'</span></li>')
var posB = (value-bottomValue)*yScale + o.lineMargin; .prepend('<span class="line" />')
if(posB >= o.height-1 || posB < 0) { .css('bottom',liBottom*i)
return; .prependTo(ylabelsUL);
}
var thisLi = $('<li><span>'+value+'</span></li>')
.css('bottom', posB);
if(Math.abs(posB) < o.height-1) {
thisLi.prepend('<span class="line" />');
}
thisLi.prependTo(ylabelsUL);
var label = thisLi.find('span:not(.line)'); var label = thisLi.find('span:not(.line)');
var topOffset = label.height()/-2; var topOffset = label.height()/-2;
if(!o.lineMargin) {
if(i == 0){ topOffset = -label.height(); } if(i == 0){ topOffset = -label.height(); }
else if(i== yLabels.length-1){ topOffset = 0; } else if(i== yLabels.length-1){ topOffset = 0; }
}
label label
.css('margin-top', topOffset) .css('margin-top', topOffset)
.addClass('label'); .addClass('label');
}); });
//start from the bottom left //start from the bottom left
ctx.translate(zeroLocX,zeroLocY); ctx.translate(0,zeroLoc);
//iterate and draw
charts.line.draw(area);
},
draw: function(area) {
// prevent drawing on top of previous draw
ctx.clearRect(-zeroLocX,-zeroLocY,o.width,o.height);
// Calculate each point properties before hand
var integer;
$.each(dataGroups,function(i,row){
integer = o.lineMargin; // the current offset
$.each(row.points, function(j,point){
if(o.xLabelParser) {
point.canvasCords = [(xLabels[j]-zeroLocX)*xScale - xBottomValue,-(point.value*yScale)];
} else {
point.canvasCords = [integer,-(point.value*yScale)];
}
if(o.lineDots) {
point.dotSize = o.dotSize||o.lineWeight*Math.PI;
point.dotInnerSize = o.dotInnerSize||o.lineWeight*Math.PI/2;
if(o.lineDots == 'double') {
point.innerColor = o.dotInnerColor;
}
}
integer+=xInterval;
});
});
// fire custom event so we can enable rich interaction
self.trigger('vizualizeBeforeDraw',{options:o,table:self,canvasContain:canvasContain,tableData:tableData});
// draw lines and areas
$.each(dataGroups,function(h){ $.each(dataGroups,function(h){
// Draw lines
ctx.beginPath(); ctx.beginPath();
ctx.lineWidth = o.lineWeight; ctx.lineWidth = o.lineWeight;
ctx.lineJoin = 'round'; ctx.lineJoin = 'round';
$.each(this.points, function(g){ var points = this.points;
var loc = this.canvasCords; var integer = 0;
if(g == 0) { ctx.moveTo(0,-(points[0]*yScale));
ctx.moveTo(loc[0],loc[1]); $.each(points, function(){
} ctx.lineTo(integer,-(this*yScale));
ctx.lineTo(loc[0],loc[1]); integer+=xInterval;
}); });
ctx.strokeStyle = this.color; ctx.strokeStyle = this.color;
ctx.stroke(); ctx.stroke();
// Draw fills
if(area){ if(area){
var integer = this.points[this.points.length-1].canvasCords[0];
if (isFinite(integer))
ctx.lineTo(integer,0); ctx.lineTo(integer,0);
ctx.lineTo(o.lineMargin,0); ctx.lineTo(0,0);
ctx.closePath(); ctx.closePath();
ctx.fillStyle = this.color; ctx.fillStyle = this.color;
ctx.globalAlpha = .3; ctx.globalAlpha = .3;
@ -444,213 +307,106 @@ $.fn.visualize = function(options, container){
} }
else {ctx.closePath();} else {ctx.closePath();}
}); });
// draw points
if(o.lineDots) {
$.each(dataGroups,function(h){
$.each(this.points, function(g){
drawPoint(ctx,this.canvasCords[0],this.canvasCords[1],this.color,this.dotSize);
if(o.lineDots === 'double') {
drawPoint(ctx,this.canvasCords[0],this.canvasCords[1],this.innerColor,this.dotInnerSize);
}
});
});
}
}
};
})();
charts.area = {
setup: function() {
charts.line.setup(true);
}, },
draw: charts.line.draw
};
(function(){ area: function(){
createChart.line(true);
},
var horizontal,bottomLabels; bar: function(){
charts.bar = {
setup:function(){
/**
* We can draw horizontal or vertical bars depending on the
* value of the 'barDirection' option (which may be 'vertical' or
* 'horizontal').
*/
horizontal = (o.barDirection == 'horizontal');
canvasContain.addClass('visualize-bar'); canvasContain.addClass('visualize-bar');
/** //write X labels
* Write labels along the bottom of the chart. If we're drawing var xInterval = canvas.width() / (xLabels.length);
* horizontal bars, these will be the yLabels, otherwise they
* will be the xLabels. The positioning also varies slightly:
* yLabels are values, hence they will span the whole width of
* the canvas, whereas xLabels are supposed to line up with the
* bars.
*/
bottomLabels = horizontal ? yLabels : xLabels;
var xInterval = canvas.width() / (bottomLabels.length - (horizontal ? 1 : 0));
var xlabelsUL = $('<ul class="visualize-labels-x"></ul>') var xlabelsUL = $('<ul class="visualize-labels-x"></ul>')
.width(canvas.width()) .width(canvas.width())
.height(canvas.height()) .height(canvas.height())
.insertBefore(canvas); .insertBefore(canvas);
$.each(xLabels, function(i){
$.each(bottomLabels, function(i){
var thisLi = $('<li><span class="label">'+this+'</span></li>') var thisLi = $('<li><span class="label">'+this+'</span></li>')
.prepend('<span class="line" />') .prepend('<span class="line" />')
.css('left', xInterval * i) .css('left', xInterval * i)
.width(xInterval) .width(xInterval)
.appendTo(xlabelsUL); .appendTo(xlabelsUL);
if (horizontal) {
var label = thisLi.find('span.label'); var label = thisLi.find('span.label');
label.css("margin-left", -label.width() / 2); label.addClass('label');
}
}); });
/** //write Y labels
* Write labels along the left of the chart. Follows the same idea var yScale = canvas.height() / totalYRange;
* as the bottom labels. var liBottom = canvas.height() / (yLabels.length-1);
*/
var leftLabels = horizontal ? xLabels : yLabels;
var liBottom = canvas.height() / (leftLabels.length - (horizontal ? 0 : 1));
var ylabelsUL = $('<ul class="visualize-labels-y"></ul>') var ylabelsUL = $('<ul class="visualize-labels-y"></ul>')
.width(canvas.width()) .width(canvas.width())
.height(canvas.height()) .height(canvas.height())
.insertBefore(canvas); .insertBefore(canvas);
$.each(yLabels, function(i){
$.each(leftLabels, function(i){ var thisLi = $('<li><span>'+this+'</span></li>')
var thisLi = $('<li><span>'+this+'</span></li>').prependTo(ylabelsUL); .prepend('<span class="line" />')
.css('bottom',liBottom*i)
var label = thisLi.find('span:not(.line)').addClass('label'); .prependTo(ylabelsUL);
var label = thisLi.find('span:not(.line)');
if (horizontal) { var topOffset = label.height()/-2;
/** if(i == 0){ topOffset = -label.height(); }
* For left labels, we want to vertically align the text else if(i== yLabels.length-1){ topOffset = 0; }
* to the middle of its container, but we don't know how label
* many lines of text we will have, since the labels could .css('margin-top', topOffset)
* be very long. .addClass('label');
*
* So we set a min-height of liBottom, and a max-height
* of liBottom + 1, so we can then check the label's actual
* height to determine if it spans one line or more lines.
*/
label.css({
'min-height': liBottom,
'max-height': liBottom + 1,
'vertical-align': 'middle'
});
thisLi.css({'top': liBottom * i, 'min-height': liBottom});
var r = label[0].getClientRects()[0];
if (r.bottom - r.top == liBottom) {
/* This means we have only one line of text; hence
* we can centre the text vertically by setting the line-height,
* as described at:
* http://www.ampsoft.net/webdesign-l/vertical-aligned-nav-list.html
*
* (Although firefox has .height on the rectangle, IE doesn't,
* so we use r.bottom - r.top rather than r.height.)
*/
label.css('line-height', parseInt(liBottom) + 'px');
}
else {
/*
* If there is more than one line of text, then we shouldn't
* touch the line height, but we should make sure the text
* doesn't overflow the container.
*/
label.css("overflow", "hidden");
}
}
else {
thisLi.css('bottom', liBottom * i).prepend('<span class="line" />');
label.css('margin-top', -label.height() / 2)
}
}); });
charts.bar.draw(); //start from the bottom left
ctx.translate(0,zeroLoc);
}, //iterate and draw
draw: function() {
// Draw bars
if (horizontal) {
// for horizontal, keep the same code, but rotate everything 90 degrees
// clockwise.
ctx.rotate(Math.PI / 2);
}
else {
// for vertical, translate to the top left corner.
ctx.translate(0, zeroLocY);
}
// Don't attempt to draw anything if all the values are zero,
// otherwise we will get weird exceptions from the canvas methods.
if (totalYRange <= 0)
return;
var yScale = (horizontal ? canvas.width() : canvas.height()) / totalYRange;
var barWidth = horizontal ? (canvas.height() / xLabels.length) : (canvas.width() / (bottomLabels.length));
var linewidth = (barWidth - o.barGroupMargin*2) / dataGroups.length;
for(var h=0; h<dataGroups.length; h++){ for(var h=0; h<dataGroups.length; h++){
ctx.beginPath(); ctx.beginPath();
var linewidth = (xInterval-o.barGroupMargin*2) / dataGroups.length; //removed +1
var strokeWidth = linewidth - (o.barMargin*2); var strokeWidth = linewidth - (o.barMargin*2);
ctx.lineWidth = strokeWidth; ctx.lineWidth = strokeWidth;
var points = dataGroups[h].points; var points = dataGroups[h].points;
var integer = 0; var integer = 0;
for(var i=0; i<points.length; i++){ for(var i=0; i<points.length; i++){
// If the last value is zero, IE will go nuts and not draw anything,
// so don't try to draw zero values at all.
if (points[i].value != 0) {
var xVal = (integer-o.barGroupMargin)+(h*linewidth)+linewidth/2; var xVal = (integer-o.barGroupMargin)+(h*linewidth)+linewidth/2;
xVal += o.barGroupMargin*2; xVal += o.barGroupMargin*2;
ctx.moveTo(xVal, 0); ctx.moveTo(xVal, 0);
ctx.lineTo(xVal, Math.round(-points[i].value*yScale)); ctx.lineTo(xVal, Math.round(-points[i]*yScale));
} integer+=xInterval;
integer+=barWidth;
} }
ctx.strokeStyle = dataGroups[h].color; ctx.strokeStyle = dataGroups[h].color;
ctx.stroke(); ctx.stroke();
ctx.closePath(); ctx.closePath();
} }
} }
}; };
})();
//create new canvas, set w&h attrs (not inline styles) //create new canvas, set w&h attrs (not inline styles)
var canvasNode = document.createElement("canvas"); var canvasNode = document.createElement("canvas");
var canvas = $(canvasNode) canvasNode.setAttribute('height',o.height);
.attr({ canvasNode.setAttribute('width',o.width);
'height': o.height, var canvas = $(canvasNode);
'width': o.width
});
//get title for chart //get title for chart
var title = o.title || self.find('caption').text(); var title = o.title || self.find('caption').text();
//create canvas wrapper div, set inline w&h, append //create canvas wrapper div, set inline w&h, append
var canvasContain = (container || $('<div '+(o.chartId?'id="'+o.chartId+'" ':'')+'class="visualize '+o.chartClass+'" role="img" aria-label="Chart representing data from the table: '+ title +'" />')) var canvasContain = (container || $('<div class="visualize" role="img" aria-label="Chart representing data from the table: '+ title +'" />'))
.height(o.height) .height(o.height)
.width(o.width); .width(o.width)
var scroller = $('<div class="visualize-scroller"></div>')
.appendTo(canvasContain)
.append(canvas); .append(canvas);
//scrape table (this should be cleaned up into an obj)
var tableData = scrapeTable();
var dataGroups = tableData.dataGroups();
var allData = tableData.allData();
var dataSum = tableData.dataSum();
var topValue = tableData.topValue();
var bottomValue = tableData.bottomValue();
var memberTotals = tableData.memberTotals();
var totalYRange = tableData.totalYRange();
var zeroLoc = o.height * (topValue/totalYRange);
var xLabels = tableData.xLabels();
var yLabels = tableData.yLabels();
//title/key container //title/key container
if(o.appendTitle || o.appendKey){ if(o.appendTitle || o.appendKey){
var infoContain = $('<div class="visualize-info"></div>') var infoContain = $('<div class="visualize-info"></div>')
@ -666,130 +422,40 @@ $.fn.visualize = function(options, container){
//append key //append key
if(o.appendKey){ if(o.appendKey){
var newKey = $('<ul class="visualize-key"></ul>'); var newKey = $('<ul class="visualize-key"></ul>');
$.each(yAllLabels, function(i,label){ var selector;
$('<li><span class="visualize-key-color" style="background: '+dataGroups[i].color+'"></span><span class="visualize-key-label">'+ label +'</span></li>') if(o.parseDirection == 'x'){
selector = self.find('tr:gt(0) th').filter(o.rowFilter);
}
else{
selector = self.find('tr:eq(0) th').filter(o.colFilter);
}
selector.each(function(i){
$('<li><span class="visualize-key-color" style="background: '+dataGroups[i].color+'"></span><span class="visualize-key-label">'+ $(this).text() +'</span></li>')
.appendTo(newKey); .appendTo(newKey);
}); });
newKey.appendTo(infoContain); newKey.appendTo(infoContain);
}; };
// init interaction
if(o.interaction) {
// sets the canvas to track interaction
// IE needs one div on top of the canvas since the VML shapes prevent mousemove from triggering correctly.
// Pie charts needs tracker because labels goes on top of the canvas and also messes up with mousemove
var tracker = $('<div class="visualize-interaction-tracker"/>')
.css({
'height': o.height + 'px',
'width': o.width + 'px',
'position':'relative',
'z-index': 200
})
.insertAfter(canvas);
var triggerInteraction = function(overOut,data) {
var data = $.extend({
canvasContain:canvasContain,
tableData:tableData
},data);
self.trigger('vizualize'+overOut,data);
};
var over=false, last=false, started=false;
tracker.mousemove(function(e){
var x,y,x1,y1,data,dist,i,current,selector,zLabel,elem,color,minDist,found,ev=e.originalEvent;
// get mouse position relative to the tracker/canvas
x = ev.layerX || ev.offsetX || 0;
y = ev.layerY || ev.offsetY || 0;
found = false;
minDist = started?30000:(o.type=='pie'?(Math.round(canvas.height()/2)-o.pieMargin)/3:o.lineWeight*4);
// iterate datagroups to find points with matching
$.each(charts[o.type].interactionPoints,function(i,current){
x1 = current.canvasCords[0] + zeroLocX;
y1 = current.canvasCords[1] + (o.type=="pie"?0:zeroLocY);
dist = Math.sqrt( (x1 - x)*(x1 - x) + (y1 - y)*(y1 - y) );
if(dist < minDist) {
found = current;
minDist = dist;
}
});
if(o.multiHover && found) {
x = found.canvasCords[0] + zeroLocX;
y = found.canvasCords[1] + (o.type=="pie"?0:zeroLocY);
found = [found];
$.each(charts[o.type].interactionPoints,function(i,current){
if(current == found[0]) {return;}
x1 = current.canvasCords[0] + zeroLocX;
y1 = current.canvasCords[1] + zeroLocY;
dist = Math.sqrt( (x1 - x)*(x1 - x) + (y1 - y)*(y1 - y) );
if(dist <= o.multiHover) {
found.push(current);
}
});
}
// trigger over and out only when state changes, instead of on every mousemove
over = found;
if(over != last) {
if(over) {
if(last) {
triggerInteraction('Out',{point:last});
}
triggerInteraction('Over',{point:over});
last = over;
}
if(last && !over) {
triggerInteraction('Out',{point:last});
last=false;
}
started=true;
}
});
tracker.mouseleave(function(){
triggerInteraction('Out',{
point:last,
mouseOutGraph:true
});
over = (last = false);
});
}
//append new canvas to page //append new canvas to page
if(!container){canvasContain.insertAfter(this); } if(!container){canvasContain.insertAfter(this); }
if( typeof(G_vmlCanvasManager) != 'undefined' ){ G_vmlCanvasManager.init(); G_vmlCanvasManager.initElement(canvas[0]); } if( typeof(G_vmlCanvasManager) != 'undefined' ){ G_vmlCanvasManager.init(); G_vmlCanvasManager.initElement(canvas[0]); }
//set up the drawing board //set up the drawing board
var ctx = canvas[0].getContext('2d'); var ctx = canvas[0].getContext('2d');
// Scroll graphs
scroller.scrollLeft(o.width-scroller.width());
// init plugins
$.each($.visualizePlugins,function(i,plugin){
plugin.call(self,o,tableData);
});
//create chart //create chart
charts[o.type].setup(); createChart[o.type]();
//clean up some doubled lines that sit on top of canvas borders (done via JS due to IE)
$('.visualize-line li:first-child span.line, .visualize-line li:last-child span.line, .visualize-area li:first-child span.line, .visualize-area li:last-child span.line, .visualize-bar li:first-child span.line,.visualize-bar .visualize-labels-y li:last-child span.line').css('border','none');
if(!container){ if(!container){
//add event for updating //add event for updating
self.bind('visualizeRefresh', function(){ canvasContain.bind('visualizeRefresh', function(){
self.visualize(o, $(this).empty()); self.visualize(o, $(this).empty());
}); });
//add event for redraw
self.bind('visualizeRedraw', function(){
charts[o.type].draw();
});
} }
}).next(); //returns canvas(es) }).next(); //returns canvas(es)
}; };
// create array for plugins. if you wish to make a plugin,
// just push your init funcion into this array
$.visualizePlugins = [];
})(jQuery); })(jQuery);

View File

@ -0,0 +1,10 @@
<html>
<body>
<h1>An error occured!</h1>
<p>This should never happen. Please review the error output below.</p>
{foreach from=$DATA key=text item=message}
{if $text != 'email' && $text != 'subject'}
<p>{$text}: {$message}</p>
{/if}
{/foreach}

View File

@ -2,7 +2,7 @@
<form action="" method="POST"> <form action="" method="POST">
<input type="hidden" name="page" value="password"> <input type="hidden" name="page" value="password">
<input type="hidden" name="action" value="reset"> <input type="hidden" name="action" value="reset">
<p>If you have an email set for your account, enter your username to get your password reset</p> <p>If you have an email set for your account, enter your username or email address to get your password reset</p>
<p><input type="text" value="{$smarty.post.username|default:""}" name="username" required><input class="submit small" type="submit" value="Reset"></p> <p><input type="text" value="{$smarty.post.username|default:""}" name="username" required><input class="submit small" type="submit" value="Reset"></p>
</form> </form>
{include file="global/block_footer.tpl"} {include file="global/block_footer.tpl"}

View File

@ -12,7 +12,7 @@
<td class="leftheader">Current Active Workers</td> <td class="leftheader">Current Active Workers</td>
<td>{$GLOBAL.workers}</td> <td>{$GLOBAL.workers}</td>
</tr> </tr>
{if $GLOBAL.website.blockexplorer.url} {if ! $GLOBAL.website.blockexplorer.disabled}
<tr> <tr>
<td class="leftheader">Next Network Block</td> <td class="leftheader">Next Network Block</td>
<td>{$CURRENTBLOCK + 1} &nbsp;&nbsp;<font size="1"> (Current: <a href="{$GLOBAL.website.blockexplorer.url}{$CURRENTBLOCKHASH}" target="_new">{$CURRENTBLOCK})</a></font></td> <td>{$CURRENTBLOCK + 1} &nbsp;&nbsp;<font size="1"> (Current: <a href="{$GLOBAL.website.blockexplorer.url}{$CURRENTBLOCKHASH}" target="_new">{$CURRENTBLOCK})</a></font></td>

View File

@ -6,7 +6,7 @@
<div class="module_content"> <div class="module_content">
<p>If you have an email set for your account, enter your username to get your password reset</p> <p>If you have an email set for your account, enter your username to get your password reset</p>
<fieldset> <fieldset>
<label>Username</label> <label>Username or E-Mail</label>
<input type="text" name="username" value="{$smarty.post.username|default:""}" size="22" maxlength="20" required> <input type="text" name="username" value="{$smarty.post.username|default:""}" size="22" maxlength="20" required>
</fieldset> </fieldset>
<div class="clear"></div> <div class="clear"></div>