summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xusr/local/www/diag_logs_filter.php81
-rwxr-xr-xusr/local/www/diag_logs_filter_dynamic.php5
-rw-r--r--usr/local/www/diag_logs_filter_summary.php227
-rw-r--r--usr/local/www/easyrule.inc260
-rw-r--r--usr/local/www/easyrule.php132
-rw-r--r--usr/local/www/filter_log.inc82
-rw-r--r--usr/local/www/filterparser.php4
-rw-r--r--usr/local/www/protochart/ProtoChart.js2653
-rw-r--r--usr/local/www/protochart/excanvas-compressed.js19
-rw-r--r--usr/local/www/protochart/excanvas.js785
-rw-r--r--usr/local/www/themes/nervecenter/images/icons/icon_block_add.gifbin0 -> 192 bytes
-rw-r--r--usr/local/www/themes/nervecenter/images/icons/icon_pass_add.gifbin0 -> 183 bytes
12 files changed, 4217 insertions, 31 deletions
diff --git a/usr/local/www/diag_logs_filter.php b/usr/local/www/diag_logs_filter.php
index 960956f..fa25c95 100755
--- a/usr/local/www/diag_logs_filter.php
+++ b/usr/local/www/diag_logs_filter.php
@@ -5,7 +5,8 @@
part of pfSesne by Scott Ullrich
originally based on m0n0wall (http://m0n0.ch/wall)
- Copyright (C) 2003-2004 Manuel Kasper <mk@neon1.net>.
+ Copyright (C) 2003-2009 Manuel Kasper <mk@neon1.net>,
+ Jim Pingle <myfirstname>@<mylastname>.org
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -51,13 +52,34 @@ if($_GET['getrulenum'] or $_POST['getrulenum']) {
exit;
}
+if($_GET['dnsip'] or $_POST['dnsip']) {
+ if($_GET['dnsip'])
+ $dnsip = $_GET['dnsip'];
+ if($_POST['dnsip'])
+ $dnsip = $_POST['dnsip'];
+ $host = get_reverse_dns($dnsip);
+ if ($host == $ip) {
+ $host = "No PTR Record";
+ }
+ echo "IP: {$dnsip}\nHost: {$host}";
+ exit;
+}
+
+$filtertext = "";
+if($_GET['filtertext'] or $_POST['filtertext']) {
+ if($_GET['filtertext'])
+ $filtertext = $_GET['filtertext'];
+ if($_POST['filtertext'])
+ $filtertext = $_POST['filtertext'];
+}
+
$filter_logfile = "{$g['varlog_path']}/filter.log";
$nentries = $config['syslog']['nentries'];
if (!$nentries)
$nentries = 50;
-if ($_POST['clear'])
+if ($_POST['clear'])
clear_log_file($filter_logfile);
$pgtitle = array("Status","System logs","Firewall");
@@ -88,12 +110,20 @@ include("head.inc");
<td>
<div id="mainarea">
<table class="tabcont" width="100%" border="0" cellpadding="0" cellspacing="0">
+ <tr><td colspan="6" align="left">
+ Normal View | <a href="diag_logs_filter_dynamic.php">Dynamic View</a> | <a href="diag_logs_filter_summary.php">Summary View</a><br/><br/>
+ </td></tr>
<?php if (!isset($config['syslog']['rawfilter'])):
- $filterlog = conv_log_filter($filter_logfile, $nentries, $nentries + 100);
+ $filterlog = conv_log_filter($filter_logfile, $nentries, $nentries + 100, $filtertext);
?>
<tr>
<td colspan="6" class="listtopic">
- Last <?php echo $nentries;?> firewall log entries &nbsp;&nbsp;&nbsp;(<a href='/diag_logs_filter_dynamic.php'>switch</a> to dynamic view)</td>
+ <?php if (!$filtertext) { ?>
+ Last <?php echo count($filterlog);?> firewall log entries.
+ <?php } else { ?>
+ <?php echo count($filterlog);?> matched log entries.
+ <?php } ?>
+ (Max <?php echo $nentries;?>)
</tr>
<tr>
<td width="10%" class="listhdrr">Act</td>
@@ -111,8 +141,23 @@ include("head.inc");
<?php if ($filterent['count']) echo $filterent['count'];?></td>
<td class="listr" nowrap><?php echo htmlspecialchars($filterent['time']);?></td>
<td class="listr" nowrap><?php echo htmlspecialchars($filterent['interface']);?></td>
- <td class="listr" nowrap><?php echo htmlspecialchars($filterent['src']);?></td>
- <td class="listr" nowrap><?php echo htmlspecialchars($filterent['dst']);?></td>
+ <?php
+ $int = strtolower($filterent['interface']);
+ $proto = strtolower($filterent['proto']);
+
+ $srcstr = $filterent['srcip'] . get_port_with_service($filterent['srcport'], $proto);
+ $dststr = $filterent['dstip'] . get_port_with_service($filterent['dstport'], $proto);
+ ?>
+ <td class="listr" nowrap>
+ <a href="diag_dns.php?host=<?php echo $filterent['srcip']; ?>" title="Reverse Resolve with DNS"><img border="0" src="/themes/nervecenter/images/icons/icon_log.gif"></a>
+ <a href="easyrule.php?<?php echo "action=block&int={$int}&src={$filterent['srcip']}"; ?>" title="Easy Rule: Add to Block List" onclick="return confirm('Do you really want to add this BLOCK rule?\n\nEasy Rule is still experimental.\nContinue at risk of your own peril.\nBackups are also nice.')"><img border="0" src="/themes/nervecenter/images/icons/icon_block_add.gif"></a>
+ <?php echo $srcstr;?>
+ </td>
+ <td class="listr" nowrap>
+ <a href="diag_dns.php?host=<?php echo $filterent['dstip']; ?>" title="Reverse Resolve with DNS"><img border="0" src="/themes/nervecenter/images/icons/icon_log.gif"></a>
+ <a href="easyrule.php?<?php echo "action=pass&int={$int}&proto={$proto}&src={$filterent['srcip']}&dst={$filterent['dstip']}&dstport={$filterent['dstport']}"; ?>" title="Easy Rule: Pass this traffic" onclick="return confirm('Do you really want to add this PASS rule?\n\nEasy Rule is still experimental.\nContinue at risk of your own peril.\nBackups are also nice.')"><img border="0" src="/themes/nervecenter/images/icons/icon_pass_add.gif"></a>
+ <?php echo $dststr;?>
+ </td>
<?php
if ($filterent['proto'] == "TCP")
$filterent['proto'] .= ":{$filterent['tcpflags']}";
@@ -124,11 +169,27 @@ include("head.inc");
<td colspan="2" class="listtopic">
Last <?php echo $nentries;?> firewall log entries</td>
</tr>
- <?php dump_clog($filter_logfile, $nentries); ?>
+ <?php
+ if($filtertext)
+ dump_clog($filter_logfile, $nentries, true, array("$filtertext"));
+ else
+ dump_clog($filter_logfile, $nentries);
+ ?>
<?php endif; ?>
- <tr><td><br /><form action="diag_logs_filter.php" method="post">
-<input name="clear" type="submit" class="formbtn" value="Clear log" /></td></tr>
-</form>
+ <tr>
+ <td align="left" valign="top" colspan="3">
+ <form id="clearform" name="clearform" action="diag_logs_filter.php" method="post" style="margin-top: 14px;">
+ <input id="submit" name="clear" type="submit" class="formbtn" value="<?=gettext("Clear log");?>" />
+ </form>
+ </td>
+ <td align="right" valign="top" colspan="3">
+ <form id="filterform" name="filterform" action="diag_logs_filter.php" method="post" style="margin-top: 14px;">
+ <input id="filtertext" name="filtertext" value="<?=gettext($filtertext);?>" />
+ <input id="filtersubmit" name="filtersubmit" type="submit" class="formbtn" value="<?=gettext("Filter");?>" />
+ </form>
+ </td>
+
+ </tr>
</table>
</div>
</td>
diff --git a/usr/local/www/diag_logs_filter_dynamic.php b/usr/local/www/diag_logs_filter_dynamic.php
index 3e4cae3..3bcd9b0 100755
--- a/usr/local/www/diag_logs_filter_dynamic.php
+++ b/usr/local/www/diag_logs_filter_dynamic.php
@@ -108,8 +108,11 @@ include("head.inc");
<tr>
<td>
<div id="mainarea">
+ <div class="tabcont">
+ <a href="diag_logs_filter.php">Normal View</a> | Dynamic View | <a href="diag_logs_filter_summary.php">Summary View</a>
+ </div>
<div class="listtopic">
- Last <?php echo $nentries; ?> records; (Switch to <a href="diag_logs_filter.php">regular</a> view) Pause:<input valign="middle" type="checkbox" onClick="javascript:toggle_pause();">
+ Last <?php echo $nentries; ?> records; Pause:<input valign="middle" type="checkbox" onClick="javascript:toggle_pause();">
</div>
<div id="log">
<div class="log-header">
diff --git a/usr/local/www/diag_logs_filter_summary.php b/usr/local/www/diag_logs_filter_summary.php
new file mode 100644
index 0000000..565cac5
--- /dev/null
+++ b/usr/local/www/diag_logs_filter_summary.php
@@ -0,0 +1,227 @@
+<?php
+/*
+ diag_logs_filter_summary.php
+
+ Copyright (C) 2009 Jim Pingle (jpingle@gmail.com)
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+*/
+
+$pgtitle = "Diagnostics: System logs: Firewall Log Summary";
+require_once("guiconfig.inc");
+include_once("includes/log.inc.php");
+
+$filter_logfile = "{$g['varlog_path']}/filter.log";
+$lines = 5000;
+$entriesperblock = 5;
+
+$filterlog = conv_clog_filter($filter_logfile, $lines, $lines);
+$gotlines = count($filterlog);
+
+$descr = array();
+$descr['actions'] = "Actions";
+$descr['ints'] = "Interfaces";
+$descr['protos'] = "Protocols";
+$descr['srcips'] = "Source IPs";
+$descr['dstips'] = "Destination IPs";
+$descr['srcports'] = "Source Ports";
+$descr['dstports'] = "Destination Ports";
+
+$summary['actions'] = array();
+$summary['ints'] = array();
+$summary['protos'] = array();
+$summary['srcips'] = array();
+$summary['dstips'] = array();
+$summary['srcports'] = array();
+$summary['dstports'] = array();
+
+$totals = array();
+
+function cmp($a, $b) {
+ if ($a == $b) {
+ return 0;
+ }
+ return ($a < $b) ? 1 : -1;
+}
+
+function stat_block($summary, $stat, $num) {
+ global $gotlines, $descr;
+ uasort($summary[$stat] , 'cmp');
+ print '<table width="200px" cellpadding="3" cellspacing="0" border="1">';
+ print "<tr><th colspan='2'>{$descr[$stat]} data</th></tr>";
+ $k = array_keys($summary[$stat]);
+ $total = 0;
+ $numentries = 0;
+ for ($i=0; $i < $num; $i++) {
+ if ($k[$i]) {
+ $total += $summary[$stat][$k[$i]];
+ $numentries++;
+ $outstr = $k[$i];
+ if (is_ipaddr($outstr)) {
+ $outstr = "<a href=\"diag_dns.php?host={$outstr}\" title=\"Reverse Resolve with DNS\"><img border=\"0\" src=\"/themes/nervecenter/images/icons/icon_log.gif\"></a> {$outstr}";
+ } elseif (substr_count($outstr, '/') == 1) {
+ list($proto, $port) = explode('/', $outstr);
+ $service = getservbyport($port, strtolower($proto));
+ if ($service)
+ $outstr .= ": {$service}";
+ }
+ print "<tr><td>{$outstr}</td><td width='50px' align='right'>{$summary[$stat][$k[$i]]}</td></tr>\n";
+ }
+ }
+ $leftover = $gotlines - $total;
+ if ($leftover > 0) {
+ print "<tr><td>Other</td><td width='50px' align='right'>{$leftover}</td></tr>\n";
+ }
+ print '</table>';
+}
+
+function pie_block($summary, $stat, $num) {
+ global $gotlines, $descr;
+ uasort($summary[$stat] , 'cmp');
+ $k = array_keys($summary[$stat]);
+ $total = 0;
+ $numentries = 0;
+ print "\n<script language=\"javascript\" type=\"text/javascript\">\n";
+ print "function lblfmt(lbl) {\n";
+ print " return '<font size=\"-2\">' + lbl + '</font>'\n";
+ print "}\n";
+ for ($i=0; $i < $num; $i++) {
+ if ($k[$i]) {
+ $total += $summary[$stat][$k[$i]];
+ $numentries++;
+ print "var d{$i} = [];\n";
+ print "d{$i}.push([1, {$summary[$stat][$k[$i]]}]);\n";
+ }
+ }
+ $leftover = $gotlines - $total;
+ if ($leftover > 0) {
+ print "var d{$num} = [];\n";
+ print "d{$num}.push([1, {$leftover}]);\n";
+ }
+
+ print "Event.observe(window, 'load', function() {\n";
+ print " new Proto.Chart($('piechart{$stat}'), \n";
+ print " [\n";
+ for ($i=0; $i < $num; $i++) {
+ if ($k[$i]) {
+ print " { data: d{$i}, label: \"{$k[$i]}\"}";
+ if (!(($i == ($numentries - 1)) && ($leftover <= 0)))
+ print ",\n";
+ else
+ print "\n";
+ }
+ }
+ if ($leftover > 0)
+ print " { data: d{$i}, label: \"Other\"}\n";
+ print " ],\n";
+ print " {\n";
+ print " pies: {show: true, autoScale: true},\n";
+ print " legend: {show: true, labelFormatter: lblfmt}\n";
+ print " });\n";
+ print "});\n";
+
+ print "</script>";
+ print "<table cellpadding=\"3\" cellspacing=\"0\" border=\"0\">";
+ print "<tr><th><font size=\"+1\">{$descr[$stat]}</font></th></tr>";
+ print "<tr><td><div id=\"piechart{$stat}\" style=\"width:450px;height:300px\"></div>\n";
+ print "</table>";
+}
+
+foreach ($filterlog as $fe) {
+ $summary['actions'][$fe['act']]++;
+ $summary['ints'][$fe['interface']]++;
+ $summary['protos'][$fe['proto']]++;
+ $summary['srcips'][$fe['srcip']]++;
+ $summary['dstips'][$fe['dstip']]++;
+ if ($fe['srcport'])
+ $summary['srcports'][$fe['proto'].'/'.$fe['srcport']]++;
+ else
+ $summary['srcports'][$fe['srcport']]++;
+ if ($fe['dstport'])
+ $summary['dstports'][$fe['proto'].'/'.$fe['dstport']]++;
+ else
+ $summary['dstports'][$fe['dstport']]++;
+}
+
+include("head.inc"); ?>
+<body link="#000000" vlink="#000000" alink="#000000">
+<script src="/javascript/filter_log.js" type="text/javascript"></script>
+<script language="javascript" type="text/javascript" src="/protochart/prototype.js"></script>
+<script language="javascript" type="text/javascript" src="/protochart/ProtoChart.js"></script>
+<!--[if IE]>
+<script language="javascript" type="text/javascript" src="/protochart/excanvas.js">
+</script>
+<![endif]-->
+
+<? include("fbegin.inc"); ?>
+<p class="pgtitle"><?=$pgtitle?></p>
+<table width="100%" border="0" cellpadding="0" cellspacing="0">
+ <tr><td>
+<?php
+ $tab_array = array();
+ $tab_array[] = array("System", false, "diag_logs.php");
+ $tab_array[] = array("Firewall", true, "diag_logs_filter.php");
+ $tab_array[] = array("DHCP", false, "diag_logs_dhcp.php");
+ $tab_array[] = array("Portal Auth", false, "diag_logs_auth.php");
+ $tab_array[] = array("IPSEC VPN", false, "diag_logs_ipsec.php");
+ $tab_array[] = array("PPTP VPN", false, "diag_logs_vpn.php");
+ $tab_array[] = array("Load Balancer", false, "diag_logs_slbd.php");
+ $tab_array[] = array("OpenVPN", false, "diag_logs_openvpn.php");
+ $tab_array[] = array("OpenNTPD", false, "diag_logs_ntpd.php");
+ $tab_array[] = array("Settings", false, "diag_logs_settings.php");
+ display_top_tabs($tab_array);
+?>
+ </td></tr>
+ <tr>
+ <td>
+ <div id="mainarea">
+ <table class="tabcont" width="100%" border="0" cellpadding="0" cellspacing="0" align="center">
+ <tr><td colspan="6" align="left">
+ <a href="diag_logs_filter.php">Normal View</a> | <a href="diag_logs_filter_dynamic.php">Dynamic View</a> | Summary View<br/><br/>
+ </td></tr>
+ <tr><td align="center">
+
+This is a firewall log summary, of the last <?php echo $gotlines; ?> lines of the firewall log (Max <?php echo $lines; ?>).<br />
+NOTE: IE8 users must enable compatibility view.
+
+<?php pie_block($summary, 'actions' , $entriesperblock); ?><br /><br />
+<?php stat_block($summary, 'actions' , $entriesperblock); ?><br /><br />
+<?php pie_block($summary, 'ints' , $entriesperblock); ?><br /><br />
+<?php stat_block($summary, 'ints' , $entriesperblock); ?><br /><br />
+<?php pie_block($summary, 'protos' , $entriesperblock); ?><br /><br />
+<?php stat_block($summary, 'protos' , $entriesperblock); ?><br /><br />
+<?php pie_block($summary, 'srcips' , $entriesperblock); ?><br /><br />
+<?php stat_block($summary, 'srcips' , $entriesperblock); ?><br /><br />
+<?php pie_block($summary, 'dstips' , $entriesperblock); ?><br /><br />
+<?php stat_block($summary, 'dstips' , $entriesperblock); ?><br /><br />
+<?php pie_block($summary, 'srcports', $entriesperblock); ?><br /><br />
+<?php stat_block($summary, 'srcports', $entriesperblock); ?><br /><br />
+<?php pie_block($summary, 'dstports', $entriesperblock); ?><br /><br />
+<?php stat_block($summary, 'dstports', $entriesperblock); ?><br /><br />
+
+ </td></tr></table>
+ </div>
+ </td>
+ </tr>
+</table>
+<?php include("fend.inc"); ?>
diff --git a/usr/local/www/easyrule.inc b/usr/local/www/easyrule.inc
new file mode 100644
index 0000000..f49e791
--- /dev/null
+++ b/usr/local/www/easyrule.inc
@@ -0,0 +1,260 @@
+<?php
+/*
+ easyrule.inc.php
+
+ Copyright (C) 2009 Jim Pingle (jpingle@gmail.com)
+ Sponsored By Anathematic @ pfSense Forums
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+*/
+
+$blockaliasname = 'EasyRuleBlockHosts';
+$specialsrcdst = explode(" ", "any wanip lanip lan pptp pppoe");
+
+function easyrule_find_rule_interface($int) {
+ global $config;
+ /* Borrowed from firewall_rules.php */
+ $iflist = array("lan" => "LAN", "wan" => "WAN");
+
+ for ($i = 1; isset($config['interfaces']['opt' . $i]); $i++) {
+ $iflist['opt' . $i] = $config['interfaces']['opt' . $i]['descr'];
+ }
+
+ if ($config['pptpd']['mode'] == "server")
+ $iflist['pptp'] = "PPTP VPN";
+
+ if ($config['pppoe']['mode'] == "server")
+ $iflist['pppoe'] = "PPPoE VPN";
+
+ /* add ipsec interfaces */
+ if (isset($config['ipsec']['enable']) || isset($config['ipsec']['mobileclients']['enable'])){
+ $iflist["enc0"] = "IPSEC";
+ }
+
+ for ($i = 1; isset($config['interfaces']['opt' . $i]); $i++) {
+ $iflist['opt' . $i] = $config['interfaces']['opt' . $i]['descr'];
+ }
+
+ if (isset($iflist[$int]))
+ return $int;
+
+ foreach ($iflist as $if => $ifd) {
+ if (strtolower($int) == strtolower($ifd))
+ return $if;
+ }
+
+ return false;
+}
+
+function easyrule_block_rule_exists($int = 'wan') {
+ global $blockaliasname, $config;
+ /* No rules, we we know it doesn't exist */
+ if (!is_array($config['filter']['rule'])) {
+ return false;
+ }
+
+ /* Search through the rules for one referencing our alias */
+ foreach ($config['filter']['rule'] as $rule)
+ if ($rule['source']['address'] == $blockaliasname . strtoupper($int) && ($rule['interface'] == $int))
+ return true;
+ return false;
+}
+
+function easyrule_block_rule_create($int = 'wan') {
+ global $blockaliasname, $config;
+ /* If the alias doesn't exist, exit.
+ * Can't create an empty alias, and we don't know a host */
+ if (easyrule_block_alias_getid($int) === false)
+ return false;
+
+ /* If the rule already exists, no need to do it again */
+ if (easyrule_block_rule_exists($int))
+ return true;
+
+ /* No rules, start a new array */
+ if (!is_array($config['filter']['rule'])) {
+ $config['filter']['rule'] = array();
+ }
+
+ filter_rules_sort();
+ $a_filter = &$config['filter']['rule'];
+
+ /* Make up a new rule */
+ $filterent = array();
+ $filterent['type'] = 'block';
+ $filterent['interface'] = $int;
+ $filterent['source']['address'] = $blockaliasname . strtoupper($int);
+ $filterent['destination']['any'] = '';
+ $filterent['descr'] = "Easy Rule: Blocked from Firewall Log View";
+
+ $a_filter[] = $filterent;
+
+ return true;
+}
+
+function easyrule_block_alias_getid($int = 'wan') {
+ global $blockaliasname, $config;
+ if (!is_array($config['aliases']))
+ return false;
+
+ /* Hunt down an alias with the name we want, return its id */
+ foreach ($config['aliases']['alias'] as $aliasid => $alias)
+ if ($alias['name'] == $blockaliasname . strtoupper($int))
+ return $aliasid;
+
+ return false;
+}
+
+function easyrule_block_alias_add($host, $int = 'wan') {
+ global $blockaliasname, $config;
+ /* If the host isn't a valid IP address, bail */
+ if (!is_ipaddr($host))
+ return false;
+
+ /* If there are no aliases, start an array */
+ if (!is_array($config['aliases']['alias']))
+ $config['aliases']['alias'] = array();
+
+ aliases_sort();
+ $a_aliases = &$config['aliases']['alias'];
+
+ /* Try to get the ID if the alias already exists */
+ $id = easyrule_block_alias_getid($int);
+ if ($id === false)
+ unset($id);
+
+ $alias = array();
+
+ if (isset($id) && $a_aliases[$id]) {
+ /* Make sure this IP isn't already in the list. */
+ if (in_array($host.'/32', explode(" ", $a_aliases[$id]['address'])))
+ return true;
+ /* Since the alias already exists, just add to it. */
+ $alias['name'] = $a_aliases[$id]['name'];
+ $alias['type'] = $a_aliases[$id]['type'];
+ $alias['descr'] = $a_aliases[$id]['descr'];
+
+ $alias['address'] = $a_aliases[$id]['address'] . ' ' . $host . '/32';
+ $alias['detail'] = $a_aliases[$id]['detail'] . 'Entry added ' . date('r') . '||';
+ } else {
+ /* Create a new alias with all the proper information */
+ $alias['name'] = $blockaliasname . strtoupper($int);
+ $alias['type'] = 'network';
+ $alias['descr'] = mb_convert_encoding("Hosts blocked from Firewall Log view","HTML-ENTITIES","auto");
+
+ $alias['address'] = $host . '/32';
+ $alias['detail'] = 'Entry added ' . date('r') . '||';
+ }
+
+ /* Replace the old alias if needed, otherwise tack it on the end */
+ if (isset($id) && $a_aliases[$id])
+ $a_aliases[$id] = $alias;
+ else
+ $a_aliases[] = $alias;
+
+ return true;
+}
+
+function easyrule_block_host_add($host, $int = 'wan') {
+ global $retval;
+ /* Bail if the supplied host is not a valid IP address */
+ if (!is_ipaddr($host))
+ return false;
+
+ /* Flag whether or not we need to reload the filter */
+ $dirty = false;
+
+ /* Attempt to add this host to the alias */
+ if (easyrule_block_alias_add($host, $int)) {
+ $dirty = true;
+ } else {
+ /* Couldn't add the alias, or adding the host failed. */
+ return false;
+ }
+
+ /* Attempt to add the firewall rule if it doesn't exist.
+ * Failing to add the rule isn't necessarily an error, it may
+ * have been modified by the user in some way. Adding to the
+ * Alias is what's important.
+ */
+ if (!easyrule_block_rule_exists($int)) {
+ if (easyrule_block_rule_create($int)) {
+ $dirty = true;
+ } else {
+ return false;
+ }
+ }
+
+ /* If needed, write the config and reload the filter */
+ if ($dirty) {
+ write_config();
+ config_lock();
+ $retval = filter_configure();
+ config_unlock();
+ header("Location: firewall_aliases.php");
+ exit;
+ } else {
+ return false;
+ }
+}
+
+function easyrule_pass_rule_add($int, $proto, $srchost, $dsthost, $dstport) {
+ global $config;
+
+ /* No rules, start a new array */
+ if (!is_array($config['filter']['rule'])) {
+ $config['filter']['rule'] = array();
+ }
+
+ filter_rules_sort();
+ $a_filter = &$config['filter']['rule'];
+
+ /* Make up a new rule */
+ $filterent = array();
+ $filterent['type'] = 'pass';
+ $filterent['interface'] = $int;
+ $filterent['descr'] = "Easy Rule: Passed from Firewall Log View";
+
+ if ($proto != "any")
+ $filterent['protocol'] = $proto;
+ else
+ unset($filterent['protocol']);
+
+ /* Default to only allow echo requests, since that's what most people want and
+ * it should be a safe choice. */
+ if ($proto == "icmp")
+ $filterent['icmptype'] = 'echoreq';
+
+ pconfig_to_address($filterent['source'], $srchost, 32);
+ pconfig_to_address($filterent['destination'], $dsthost, 32, '', $dstport, $dstport);
+
+ $a_filter[] = $filterent;
+
+ write_config();
+ config_lock();
+ $retval = filter_configure();
+ config_unlock();
+ header("Location: firewall_rules.php?if={$int}");
+ exit;
+}
+?> \ No newline at end of file
diff --git a/usr/local/www/easyrule.php b/usr/local/www/easyrule.php
new file mode 100644
index 0000000..f60b594
--- /dev/null
+++ b/usr/local/www/easyrule.php
@@ -0,0 +1,132 @@
+<?php
+/*
+ easyrule.php
+
+ Copyright (C) 2009 Jim Pingle (jpingle@gmail.com)
+ Sponsored By Anathematic @ pfSense Forums
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+*/
+
+$pgtitle = "Status : EasyRule";
+require_once("guiconfig.inc");
+require_once("easyrule.inc");
+$retval = 0;
+$message = "";
+
+if ($_GET && isset($_GET['action'])) {
+ switch ($_GET['action']) {
+ case 'block':
+ /* Check that we have a valid host */
+ if (isset($_GET['src']) && isset($_GET['int'])) {
+ if (!is_ipaddr($_GET['src'])) {
+ $message .= "Tried to block invalid IP: " . htmlspecialchars($_GET['src']) . "<br/>";
+ break;
+ }
+ $_GET['int'] = easyrule_find_rule_interface($_GET['int']);
+ if ($_GET['int'] === false) {
+ $message .= "Invalid interface for block rule: " . htmlspecialchars($_GET['int']) . "<br/>";
+ break;
+ }
+ if (easyrule_block_host_add($_GET['src'], $_GET['int'])) {
+ /* shouldn't get here, the function will redirect */
+ $message .= "Host added successfully" . "<br/>";
+ } else {
+ $message .= "Failed to create block rule, alias, or add host." . "<br/>";
+ }
+ } else {
+ $message .= "Tried to block but had no host IP or interface<br/>";
+ }
+ break;
+ case 'pass':
+ /* Check for valid int, srchost, dsthost, dstport, and proto */
+ if (isset($_GET['int']) && isset($_GET['proto']) && isset($_GET['src']) && isset($_GET['dst'])) {
+ $_GET['int'] = easyrule_find_rule_interface($_GET['int']);
+ if ($_GET['int'] === false) {
+ $message .= "Invalid interface for pass rule: " . htmlspecialchars($_GET['int']) . "<br/>";
+ break;
+ }
+ if (getprotobyname($_GET['proto']) == -1) {
+ $message .= "Invalid protocol for pass rule: " . htmlspecialchars($_GET['proto']) . "<br/>";
+ break;
+ }
+ if (!is_ipaddr($_GET['src'])) {
+ $message .= "Tried to pass invalid source IP: " . htmlspecialchars($_GET['src']) . "<br/>";
+ break;
+ }
+ if (!is_ipaddr($_GET['dst'])) {
+ $message .= "Tried to pass invalid destination IP: " . htmlspecialchars($_GET['dst']) . "<br/>";
+ break;
+ }
+ if (($_GET['proto'] != 'icmp') && !isset($_GET['dstport'])) {
+ $message .= "Missing destination port: " . htmlspecialchars($_GET['dstport']) . "<br/>";
+ break;
+ }
+ if ($_GET['proto'] == 'icmp') {
+ $_GET['dstport'] = 0;
+ }
+ if (!is_numeric($_GET['dstport']) || ($_GET['dstport'] < 0) || ($_GET['dstport'] > 65536)) {
+ $message .= "Tried to pass invalid destination port: " . htmlspecialchars($_GET['dstport']) . "<br/>";
+ break;
+ }
+ /* Should have valid input... */
+ if (easyrule_pass_rule_add($_GET['int'], $_GET['proto'], $_GET['src'], $_GET['dst'], $_GET['dstport'])) {
+ /* Shouldn't get here, the function should redirect. */
+ $message .= "Successfully added pass rule!" . "<br/>";
+ } else {
+ $message .= "Failed to add pass rule." . "<br/>";
+ }
+ } else {
+ $message = "Missing parameters for pass rule";
+ break;
+ }
+ break;
+ }
+}
+
+if(stristr($retval, "error") == true)
+ $message = $retval;
+
+include("head.inc"); ?>
+<body link="#000000" vlink="#000000" alink="#000000">
+<? include("fbegin.inc"); ?>
+<p class="pgtitle"><?=$pgtitle?></p>
+<table width="100%" border="0" cellpadding="0" cellspacing="0">
+ <tr>
+ <td>
+<?php if ($input_errors) print_input_errors($input_errors); ?>
+
+<?php if ($message) { ?>
+<br/>
+Message: <?php echo $message; ?>
+<br/>
+<? } else { ?>
+This is the Easy Rule status page, mainly used to display errors when adding rules.
+If you are seeing this, there apparently was not an error, and you navigated to the
+page directly without telling it what to do.<br/><br/>
+This page is meant to be called from the block/pass buttons on the Firewall Logs page, <a href="http://192.168.56.101/diag_logs_filter.php">Status &gt; System Logs,
+Firewall Tab</a>.
+<br />
+<? } ?>
+</td></tr></table>
+<?php include("fend.inc"); ?>
diff --git a/usr/local/www/filter_log.inc b/usr/local/www/filter_log.inc
index f7b6c32..5c54dbd 100644
--- a/usr/local/www/filter_log.inc
+++ b/usr/local/www/filter_log.inc
@@ -1,7 +1,7 @@
<?php
/* $Id$ */
/*
- log.inc.php
+ filter_log.inc
part of pfSesne by Scott Ullrich
originally based on m0n0wall (http://m0n0.ch/wall)
@@ -31,13 +31,16 @@
*/
/* format filter logs */
-function conv_log_filter($logfile, $nentries, $tail = 50) {
+function conv_log_filter($logfile, $nentries, $tail = 50, $filtertext = "") {
global $config, $g;
/* Make sure this is a number before using it in a system call */
if (!(is_numeric($tail)))
return;
+ if ($filtertext)
+ $tail = 5000;
+
/* FreeBSD 8 splits pf log lines into two lines, so we need to at least
* tail twice as many, plus some extra to account for unparseable lines */
$tail = $tail * 2 + 50;
@@ -45,7 +48,7 @@ function conv_log_filter($logfile, $nentries, $tail = 50) {
/* Always do a reverse tail, to be sure we're grabbing the 'end' of the log. */
$logarr = "";
- if(isset($config['system']['usefifolog']))
+ if(isset($config['system']['usefifolog']))
exec("/usr/sbin/fifolog_reader {$logfile} | /usr/bin/tail -r -n {$tail}", $logarr);
else
exec("/usr/sbin/clog {$logfile} | grep -v \"CLOG\" | grep -v \"\033\" | /usr/bin/tail -r -n {$tail}", $logarr);
@@ -60,7 +63,7 @@ function conv_log_filter($logfile, $nentries, $tail = 50) {
break;
$flent = parse_filter_line($logent);
- if ($flent != "") {
+ if (($flent != "") && (match_filter_line($flent, $filtertext))) {
$counter++;
$filterlog[] = $flent;
}
@@ -69,6 +72,13 @@ function conv_log_filter($logfile, $nentries, $tail = 50) {
return isset($config['syslog']['reverse']) ? $filterlog : array_reverse($filterlog);
}
+function match_filter_line($flent, $filtertext = "") {
+ if (!$filtertext)
+ return true;
+ $filtertext = str_replace(' ', '\s+', $filtertext);
+ return preg_match("/{$filtertext}/i", implode(" ", array_values($flent)));
+}
+
function collapse_filter_lines($logarr) {
$lastline = "";
$collapsed = array();
@@ -95,8 +105,17 @@ function parse_filter_line($line) {
list($all, $flent['time'], $host, $rule, $flent['act'], $flent['realint'], $details, $src, $dst, $leftovers) = $log_split;
- $flent['src'] = convert_port_period_to_colon($src);
- $flent['dst'] = convert_port_period_to_colon($dst);
+ list($flent['srcip'], $flent['srcport']) = parse_ipport($src);
+ list($flent['dstip'], $flent['dstport']) = parse_ipport($dst);
+
+ $flent['src'] = $flent['srcip'];
+ $flent['dst'] = $flent['dstip'];
+
+ if ($flent['srcport'])
+ $flent['src'] .= ':' . $flent['srcport'];
+ if ($flent['dstport'])
+ $flent['dst'] .= ':' . $flent['dstport'];
+
$flent['interface'] = convert_log_interface_to_friendly_interface_name($flent['realint']);
$tmp = split("/", $rule);
@@ -104,7 +123,7 @@ function parse_filter_line($line) {
$proto = array(" ", "(?)");
/* Attempt to determine the protocol, based on several possible patterns.
- * The value returned by strpos() must be strictly checkeded against the
+ * The value returned by strpos() must be strictly checkeded against the
* boolean FALSE because it could return a valid answer of 0 upon success. */
if (!(strpos($details, 'proto ') === FALSE)) {
preg_match("/.*\sproto\s(.*)\s\(/", $details, $proto);
@@ -152,26 +171,49 @@ function convert_log_interface_to_friendly_interface_name($int) {
return $int;
}
-function convert_port_period_to_colon($addr) {
+function parse_ipport($addr) {
+ $addr = rtrim($addr, ":");
+ $port = '';
if (substr_count($addr, '.') > 1) {
- /* IPv4 - Change the port delimiter to : */
+ /* IPv4 */
$addr_split = split("\.", $addr);
- if($addr_split[4] == "") {
- $newvar = "{$addr_split[0]}.{$addr_split[1]}.{$addr_split[2]}.{$addr_split[3]}";
- $newvar = rtrim($newvar, ":");
- } else {
+ $ip = "{$addr_split[0]}.{$addr_split[1]}.{$addr_split[2]}.{$addr_split[3]}";
+
+ if ($ip == "...")
+ return array($addr, '');
+
+ if($addr_split[4] != "") {
$port_split = split("\:", $addr_split[4]);
- $newvar = "{$addr_split[0]}.{$addr_split[1]}.{$addr_split[2]}.{$addr_split[3]}:{$port_split[0]}";
- $newvar = rtrim($newvar, ":");
+ $port = $port_split[0];
}
- if($newvar == "...")
- return $addr;
- return $newvar;
} else {
- /* IPv6 - Leave it alone */
+ /* IPv6 */
$addr = split(" ", $addr);
- return rtrim($addr[0], ":");
+ $addr = rtrim($addr[0], ":");
+ $addr_split = split("\.", $addr);
+ if (count($addr_split) > 1) {
+ $ip = $addr_split[0];
+ $port = $addr_split[1];
+ } else {
+ $ip = $addr;
+ }
+ }
+
+ return array($ip, $port);
+}
+
+function get_port_with_service($port, $proto) {
+ if (!$port)
+ return '';
+
+ $service = getservbyport($port, $proto);
+ $portstr = "";
+ if ($service) {
+ $portstr = "<span title=\"Service {$port}/{$proto}: {$service}\">" . htmlspecialchars($port) . "</span>";
+ } else {
+ $portstr = htmlspecialchars($port);
}
+ return ':' . $portstr;
}
function find_rule_by_number($rulenum, $type="rules") {
diff --git a/usr/local/www/filterparser.php b/usr/local/www/filterparser.php
index 79e927d..2ca79fe 100644
--- a/usr/local/www/filterparser.php
+++ b/usr/local/www/filterparser.php
@@ -63,7 +63,11 @@ while(!feof($log)) {
proto - Protocol (e.g. TCP, UDP, ICMP, etc)
tcpflags - TCP flags/control bits
src - Source address with port
+ srcip - Source IP
+ srcport - Source Port
dst - Destination address with port
+ dstip - Destination IP
+ dstport - Destination Port
*/
if ($flent != "")
echo "{$flent['time']} {$flent['act']} {$flent['realint']} {$flent['proto']} {$flent['src']} {$flent['dst']}\n";
diff --git a/usr/local/www/protochart/ProtoChart.js b/usr/local/www/protochart/ProtoChart.js
new file mode 100644
index 0000000..4e60f18
--- /dev/null
+++ b/usr/local/www/protochart/ProtoChart.js
@@ -0,0 +1,2653 @@
+/**
+ * Class: ProtoChart
+ * Version: v0.5 beta
+ *
+ * ProtoChart is a charting lib on top of Prototype.
+ * This library is heavily motivated by excellent work done by:
+ * * Flot <http://code.google.com/p/flot/>
+ * * Flotr <http://solutoire.com/flotr/>
+ *
+ * Complete examples can be found at: <http://www.deensoft.com/lab/protochart>
+ */
+
+/**
+ * Events:
+ * ProtoChart:mousemove - Fired when mouse is moved over the chart
+ * ProtoChart:plotclick - Fired when graph is clicked
+ * ProtoChart:dataclick - Fired when graph is clicked AND the click is on a data point
+ * ProtoChart:selected - Fired when certain region on the graph is selected
+ * ProtoChart:hit - Fired when mouse is moved near or over certain data point on the graph
+ */
+
+
+if(!Proto) var Proto = {};
+
+Proto.Chart = Class.create({
+ /**
+ * Function:
+ * {Object} elem
+ * {Object} data
+ * {Object} options
+ */
+ initialize: function(elem, data, options)
+ {
+ options = options || {};
+ this.graphData = [];
+ /**
+ * Property: options
+ *
+ * Description: Various options can be set. More details in description.
+ *
+ * colors:
+ * {Array} - pass in a array which contains strings of colors you want to use. Default has 6 color set.
+ *
+ * legend:
+ * {BOOL} - show - if you want to show the legend. Default is false
+ * {integer} - noColumns - Number of columns for the legend. Default is 1
+ * {function} - labelFormatter - A function that returns a string. The function is called with a string and is expected to return a string. Default = null
+ * {string} - labelBoxBorderColor - border color for the little label boxes. Default #CCC
+ * {HTMLElem} - container - an HTML id or HTML element where the legend should be rendered. If left null means to put the legend on top of the Chart
+ * {string} - position - position for the legend on the Chart. Default value 'ne'
+ * {integer} - margin - default valud of 5
+ * {string} - backgroundColor - default to null (which means auto-detect)
+ * {float} - backgroundOpacity - leave it 0 to avoid background
+ *
+ * xaxis (yaxis) options:
+ * {string} - mode - default is null but you can pass a string "time" to indicate time series
+ * {integer} - min
+ * {integer} - max
+ * {float} - autoscaleMargin - in % to add if auto-setting min/max
+ * {mixed} - ticks - either [1, 3] or [[1, "a"], 3] or a function which gets axis info and returns ticks
+ * {function} - tickFormatter - A function that returns a string as a tick label. Default is null
+ * {float} - tickDecimals
+ * {integer} - tickSize
+ * {integer} - minTickSize
+ * {array} - monthNames
+ * {string} - timeformat
+ *
+ * Points / Lines / Bars options:
+ * {bool} - show, default is false
+ * {integer} - radius: default is 3
+ * {integer} - lineWidth : default is 2
+ * {bool} - fill : default is true
+ * {string} - fillColor: default is #ffffff
+ *
+ * Grid options:
+ * {string} - color
+ * {string} - backgroundColor - defualt is *null*
+ * {string} - tickColor - default is *#dddddd*
+ * {integer} - labelMargin - should be in pixels default is 3
+ * {integer} - borderWidth - default *1*
+ * {bool} - clickable - default *null* - pass in TRUE if you wish to monitor click events
+ * {mixed} - coloredAreas - default *null* - pass in mixed object eg. {x1, x2}
+ * {string} - coloredAreasColor - default *#f4f4f4*
+ * {bool} - drawXAxis - default *true*
+ * {bool} - drawYAxis - default *true*
+ *
+ * selection options:
+ * {string} - mode : either "x", "y" or "xy"
+ * {string} - color : string
+ */
+ this.options = this.merge(options,{
+ colors: ["#edc240", "#00A8F0", "#C0D800", "#cb4b4b", "#4da74d", "#9440ed"],
+ legend: {
+ show: false,
+ noColumns: 1,
+ labelFormatter: null,
+ labelBoxBorderColor: "#ccc",
+ container: null,
+ position: "ne",
+ margin: 5,
+ backgroundColor: null,
+ backgroundOpacity: 0.85
+ },
+ xaxis: {
+ mode: null,
+ min: null,
+ max: null,
+ autoscaleMargin: null,
+ ticks: null,
+ tickFormatter: null,
+ tickDecimals: null,
+ tickSize: null,
+ minTickSize: null,
+ monthNames: null,
+ timeformat: null
+ },
+ yaxis: {
+ mode: null,
+ min: null,
+ max: null,
+ ticks: null,
+ tickFormatter: null,
+ tickDecimals: null,
+ tickSize: null,
+ minTickSize: null,
+ monthNames: null,
+ timeformat: null,
+ autoscaleMargin: 0.02
+ },
+
+ points: {
+ show: false,
+ radius: 3,
+ lineWidth: 2,
+ fill: true,
+ fillColor: "#ffffff"
+ },
+ lines: {
+ show: false,
+ lineWidth: 2,
+ fill: false,
+ fillColor: null
+ },
+ bars: {
+ show: false,
+ lineWidth: 2,
+ barWidth: 1,
+ fill: true,
+ fillColor: null,
+ showShadow: false,
+ fillOpacity: 0.4,
+ autoScale: true
+ },
+ pies: {
+ show: false,
+ radius: 50,
+ borderWidth: 1,
+ fill: true,
+ fillColor: null,
+ fillOpacity: 0.90,
+ labelWidth: 30,
+ fontSize: 11,
+ autoScale: true
+ },
+ grid: {
+ color: "#545454",
+ backgroundColor: null,
+ tickColor: "#dddddd",
+ labelMargin: 3,
+ borderWidth: 1,
+ clickable: null,
+ coloredAreas: null,
+ coloredAreasColor: "#f4f4f4",
+ drawXAxis: true,
+ drawYAxis: true
+ },
+ mouse: {
+ track: false,
+ position: 'se',
+ fixedPosition: true,
+ clsName: 'mouseValHolder',
+ trackFormatter: this.defaultTrackFormatter,
+ margin: 3,
+ color: '#ff3f19',
+ trackDecimals: 1,
+ sensibility: 2,
+ radius: 5,
+ lineColor: '#cb4b4b'
+ },
+ selection: {
+ mode: null,
+ color: "#97CBFF"
+ },
+ allowDataClick: true,
+ makeRandomColor: false,
+ shadowSize: 4
+ });
+
+ /*
+ * Local variables.
+ */
+ this.canvas = null;
+ this.overlay = null;
+ this.eventHolder = null;
+ this.context = null;
+ this.overlayContext = null;
+
+ this.domObj = $(elem);
+
+ this.xaxis = {};
+ this.yaxis = {};
+ this.chartOffset = {left: 0, right: 0, top: 0, bottom: 0};
+ this.yLabelMaxWidth = 0;
+ this.yLabelMaxHeight = 0;
+ this.xLabelBoxWidth = 0;
+ this.canvasWidth = 0;
+ this.canvasHeight = 0;
+ this.chartWidth = 0;
+ this.chartHeight = 0;
+ this.hozScale = 0;
+ this.vertScale = 0;
+ this.workarounds = {};
+
+ this.domObj = $(elem);
+
+ this.barDataRange = [];
+
+ this.lastMousePos = { pageX: null, pageY: null };
+ this.selection = { first: { x: -1, y: -1}, second: { x: -1, y: -1} };
+ this.prevSelection = null;
+ this.selectionInterval = null;
+ this.ignoreClick = false;
+ this.prevHit = null;
+
+ if(this.options.makeRandomColor)
+ this.options.color = this.makeRandomColor(this.options.colors);
+
+ this.setData(data);
+ this.constructCanvas();
+ this.setupGrid();
+ this.draw();
+ },
+ /**
+ * Private function internally used.
+ */
+ merge: function(src, dest)
+ {
+ var result = dest || {};
+ for(var i in src){
+ result[i] = (typeof(src[i]) == 'object' && !(src[i].constructor == Array || src[i].constructor == RegExp)) ? this.merge(src[i], dest[i]) : result[i] = src[i];
+ }
+ return result;
+ },
+ /**
+ * Function: setData
+ * {Object} data
+ *
+ * Description:
+ * Sets datasoruces properly then sets the Bar Width accordingly, then copies the default data options and then processes the graph data
+ *
+ * Returns: none
+ *
+ */
+ setData: function(data)
+ {
+ this.graphData = this.parseData(data);
+ this.setBarWidth();
+ this.copyGraphDataOptions();
+ this.processGraphData();
+ },
+ /**
+ * Function: parseData
+ * {Object} data
+ *
+ * Return:
+ * {Object} result
+ *
+ * Description:
+ * Takes the provided data object and converts it into generic data that we can understand. User can pass in data in 3 different ways:
+ * - [d1, d2]
+ * - [{data: d1, label: "data1"}, {data: d2, label: "data2"}]
+ * - [d1, {data: d1, label: "data1"}]
+ *
+ * This function parses these senarios and makes it readable
+ */
+ parseData: function(data)
+ {
+ var res = [];
+ data.each(function(d){
+ var s;
+ if(d.data) {
+ s = {};
+ for(var v in d) {
+ s[v] = d[v];
+ }
+ }
+ else {
+ s = {data: d};
+ }
+ res.push(s);
+ }.bind(this));
+ return res;
+ },
+ /**
+ * function: makeRandomColor
+ * {Object} colorSet
+ *
+ * Return:
+ * {Array} result - array containing random colors
+ */
+ makeRandomColor: function(colorSet)
+ {
+ var randNum = Math.floor(Math.random() * colorSet.length);
+ var randArr = [];
+ var newArr = [];
+ randArr.push(randNum);
+
+ while(randArr.length < colorSet.length)
+ {
+ var tempNum = Math.floor(Math.random() * colorSet.length);
+
+ while(checkExisted(tempNum, randArr))
+ tempNum = Math.floor(Math.random() * colorSet.length);
+
+ randArr.push(tempNum);
+ }
+
+ randArr.each(function(ra){
+ newArr.push(colorSet[ra]);
+
+ }.bind(this));
+ return newArr;
+ },
+ /**
+ * function: checkExisted
+ * {Object} needle
+ * {Object} haystack
+ *
+ * return:
+ * {bool} existed - true if it finds needle in the haystack
+ */
+ checkExisted: function(needle, haystack)
+ {
+ var existed = false;
+ haystack.each(function(aNeedle){
+ if(aNeedle == needle) {
+ existed = true;
+ throw $break;
+ }
+ }.bind(this));
+ return existed;
+ },
+ /**
+ * function: setBarWidth
+ *
+ * Description: sets the bar width for Bar Graph, you should enable *autoScale* property for bar graph
+ */
+ setBarWidth: function()
+ {
+ if(this.options.bars.show && this.options.bars.autoScale)
+ {
+ this.options.bars.barWidth = 1 / this.graphData.length / 1.2;
+ }
+ },
+ /**
+ * Function: copyGraphDataOptions
+ *
+ * Description: Private function that goes through each graph data (series) and assigned the graph
+ * properties to it.
+ */
+ copyGraphDataOptions: function()
+ {
+ var i, neededColors = this.graphData.length, usedColors = [], assignedColors = [];
+
+ this.graphData.each(function(gd){
+ var sc = gd.color;
+ if(sc) {
+ --neededColors;
+ if(Object.isNumber(sc)) {
+ assignedColors.push(sc);
+ }
+ else {
+ usedColors.push(this.parseColor(sc));
+ }
+ }
+ }.bind(this));
+
+
+ assignedColors.each(function(ac){
+ neededColors = Math.max(neededColors, ac + 1);
+ });
+
+ var colors = [];
+ var variation = 0;
+ i = 0;
+ while (colors.length < neededColors) {
+ var c;
+ if (this.options.colors.length == i) {
+ c = new Proto.Color(100, 100, 100);
+ }
+ else {
+ c = this.parseColor(this.options.colors[i]);
+ }
+
+ var sign = variation % 2 == 1 ? -1 : 1;
+ var factor = 1 + sign * Math.ceil(variation / 2) * 0.2;
+ c.scale(factor, factor, factor);
+
+ colors.push(c);
+
+ ++i;
+ if (i >= this.options.colors.length) {
+ i = 0;
+ ++variation;
+ }
+ }
+
+ var colorIndex = 0, s;
+
+ this.graphData.each(function(gd){
+ if(gd.color == null)
+ {
+ gd.color = colors[colorIndex].toString();
+ ++colorIndex;
+ }
+ else if(Object.isNumber(gd.color)) {
+ gd.color = colors[gd.color].toString();
+ }
+
+ gd.lines = Object.extend(Object.clone(this.options.lines), gd.lines);
+ gd.points = Object.extend(Object.clone(this.options.points), gd.points);
+ gd.bars = Object.extend(Object.clone(this.options.bars), gd.bars);
+ gd.mouse = Object.extend(Object.clone(this.options.mouse), gd.mouse);
+ if (gd.shadowSize == null) {
+ gd.shadowSize = this.options.shadowSize;
+ }
+ }.bind(this));
+
+ },
+ /**
+ * Function: processGraphData
+ *
+ * Description: processes graph data, setup xaxis and yaxis min and max points.
+ */
+ processGraphData: function() {
+
+ this.xaxis.datamin = this.yaxis.datamin = Number.MAX_VALUE;
+ this.xaxis.datamax = this.yaxis.datamax = Number.MIN_VALUE;
+
+ this.graphData.each(function(gd) {
+ var data = gd.data;
+ data.each(function(d){
+ if(d == null) {
+ return;
+ }
+
+ var x = d[0], y = d[1];
+ if(!x || !y || isNaN(x = +x) || isNaN(y = +y)) {
+ d = null;
+ return;
+ }
+
+ if (x < this.xaxis.datamin)
+ this.xaxis.datamin = x;
+ if (x > this.xaxis.datamax)
+ this.xaxis.datamax = x;
+ if (y < this.yaxis.datamin)
+ this.yaxis.datamin = y;
+ if (y > this.yaxis.datamax)
+ this.yaxis.datamax = y;
+ }.bind(this));
+ }.bind(this));
+
+
+ if (this.xaxis.datamin == Number.MAX_VALUE)
+ this.xaxis.datamin = 0;
+ if (this.yaxis.datamin == Number.MAX_VALUE)
+ this.yaxis.datamin = 0;
+ if (this.xaxis.datamax == Number.MIN_VALUE)
+ this.xaxis.datamax = 1;
+ if (this.yaxis.datamax == Number.MIN_VALUE)
+ this.yaxis.datamax = 1;
+ },
+ /**
+ * Function: constructCanvas
+ *
+ * Description: constructs the main canvas for drawing. It replicates the HTML elem (usually DIV) passed
+ * in via constructor. If there is no height/width assigned to the HTML elem then we take a default size
+ * of 400px (width) and 300px (height)
+ */
+ constructCanvas: function() {
+
+ this.canvasWidth = this.domObj.getWidth();
+ this.canvasHeight = this.domObj.getHeight();
+ this.domObj.update(""); // clear target
+ this.domObj.setStyle({
+ "position": "relative"
+ });
+
+ if (this.canvasWidth <= 0) {
+ this.canvasWdith = 400;
+ }
+ if(this.canvasHeight <= 0) {
+ this.canvasHeight = 300;
+ }
+
+ this.canvas = (Prototype.Browser.IE) ? document.createElement("canvas") : new Element("CANVAS", {'width': this.canvasWidth, 'height': this.canvasHeight});
+ Element.extend(this.canvas);
+ this.canvas.style.width = this.canvasWidth + "px";
+ this.canvas.style.height = this.canvasHeight + "px";
+
+ this.domObj.appendChild(this.canvas);
+
+ if (Prototype.Browser.IE) // excanvas hack
+ {
+ this.canvas = $(window.G_vmlCanvasManager.initElement(this.canvas));
+ }
+ this.canvas = $(this.canvas);
+
+ this.context = this.canvas.getContext("2d");
+
+ this.overlay = (Prototype.Browser.IE) ? document.createElement("canvas") : new Element("CANVAS", {'width': this.canvasWidth, 'height': this.canvasHeight});
+ Element.extend(this.overlay);
+ this.overlay.style.width = this.canvasWidth + "px";
+ this.overlay.style.height = this.canvasHeight + "px";
+ this.overlay.style.position = "absolute";
+ this.overlay.style.left = "0px";
+ this.overlay.style.right = "0px";
+
+ this.overlay.setStyle({
+ 'position': 'absolute',
+ 'left': '0px',
+ 'right': '0px'
+ });
+ this.domObj.appendChild(this.overlay);
+
+ if (Prototype.Browser.IE) {
+ this.overlay = $(window.G_vmlCanvasManager.initElement(this.overlay));
+ }
+
+ this.overlay = $(this.overlay);
+ this.overlayContext = this.overlay.getContext("2d");
+
+ if(this.options.selection.mode)
+ {
+ this.overlay.observe('mousedown', this.onMouseDown.bind(this));
+ this.overlay.observe('mousemove', this.onMouseMove.bind(this));
+ }
+ if(this.options.grid.clickable) {
+ this.overlay.observe('click', this.onClick.bind(this));
+ }
+ if(this.options.mouse.track)
+ {
+ this.overlay.observe('mousemove', this.onMouseMove.bind(this));
+ }
+ },
+ /**
+ * function: setupGrid
+ *
+ * Description: a container function that does a few interesting things.
+ *
+ * 1. calls <extendXRangeIfNeededByBar> function which makes sure that our axis are expanded if needed
+ *
+ * 2. calls <setRange> function providing xaxis options which fixes the ranges according to data points
+ *
+ * 3. calls <prepareTickGeneration> function for xaxis which generates ticks according to options provided by user
+ *
+ * 4. calls <setTicks> function for xaxis that sets the ticks
+ *
+ * similar sequence is called for y-axis.
+ *
+ * At the end if this is a pie chart than we insert Labels (around the pie chart) via <insertLabels> and we also call <insertLegend>
+ */
+ setupGrid: function()
+ {
+ if(this.options.bars.show)
+ {
+ this.xaxis.max += 0.5;
+ this.xaxis.min -= 0.5;
+ }
+ //x-axis
+ this.extendXRangeIfNeededByBar();
+ this.setRange(this.xaxis, this.options.xaxis);
+ this.prepareTickGeneration(this.xaxis, this.options.xaxis);
+ this.setTicks(this.xaxis, this.options.xaxis);
+
+
+ //y-axis
+ this.setRange(this.yaxis, this.options.yaxis);
+ this.prepareTickGeneration(this.yaxis, this.options.yaxis);
+ this.setTicks(this.yaxis, this.options.yaxis);
+ this.setSpacing();
+
+ if(!this.options.pies.show)
+ {
+ this.insertLabels();
+ }
+ this.insertLegend();
+ },
+ /**
+ * function: setRange
+ *
+ * parameters:
+ * {Object} axis
+ * {Object} axisOptions
+ */
+ setRange: function(axis, axisOptions) {
+ var min = axisOptions.min != null ? axisOptions.min : axis.datamin;
+ var max = axisOptions.max != null ? axisOptions.max : axis.datamax;
+
+ if (max - min == 0.0) {
+ // degenerate case
+ var widen;
+ if (max == 0.0)
+ widen = 1.0;
+ else
+ widen = 0.01;
+
+ min -= widen;
+ max += widen;
+ }
+ else {
+ // consider autoscaling
+ var margin = axisOptions.autoscaleMargin;
+ if (margin != null) {
+ if (axisOptions.min == null) {
+ min -= (max - min) * margin;
+ // make sure we don't go below zero if all values
+ // are positive
+ if (min < 0 && axis.datamin >= 0)
+ min = 0;
+ }
+ if (axisOptions.max == null) {
+ max += (max - min) * margin;
+ if (max > 0 && axis.datamax <= 0)
+ max = 0;
+ }
+ }
+ }
+ axis.min = min;
+ axis.max = max;
+ },
+ /**
+ * function: prepareTickGeneration
+ *
+ * Parameters:
+ * {Object} axis
+ * {Object} axisOptions
+ */
+ prepareTickGeneration: function(axis, axisOptions) {
+ // estimate number of ticks
+ var noTicks;
+ if (Object.isNumber(axisOptions.ticks) && axisOptions.ticks > 0)
+ noTicks = axisOptions.ticks;
+ else if (axis == this.xaxis)
+ noTicks = this.canvasWidth / 100;
+ else
+ noTicks = this.canvasHeight / 60;
+
+ var delta = (axis.max - axis.min) / noTicks;
+ var size, generator, unit, formatter, i, magn, norm;
+
+ if (axisOptions.mode == "time") {
+ function formatDate(d, fmt, monthNames) {
+ var leftPad = function(n) {
+ n = "" + n;
+ return n.length == 1 ? "0" + n : n;
+ };
+
+ var r = [];
+ var escape = false;
+ if (monthNames == null)
+ monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+ for (var i = 0; i < fmt.length; ++i) {
+ var c = fmt.charAt(i);
+
+ if (escape) {
+ switch (c) {
+ case 'h': c = "" + d.getHours(); break;
+ case 'H': c = leftPad(d.getHours()); break;
+ case 'M': c = leftPad(d.getMinutes()); break;
+ case 'S': c = leftPad(d.getSeconds()); break;
+ case 'd': c = "" + d.getDate(); break;
+ case 'm': c = "" + (d.getMonth() + 1); break;
+ case 'y': c = "" + d.getFullYear(); break;
+ case 'b': c = "" + monthNames[d.getMonth()]; break;
+ }
+ r.push(c);
+ escape = false;
+ }
+ else {
+ if (c == "%")
+ escape = true;
+ else
+ r.push(c);
+ }
+ }
+ return r.join("");
+ }
+
+
+ // map of app. size of time units in milliseconds
+ var timeUnitSize = {
+ "second": 1000,
+ "minute": 60 * 1000,
+ "hour": 60 * 60 * 1000,
+ "day": 24 * 60 * 60 * 1000,
+ "month": 30 * 24 * 60 * 60 * 1000,
+ "year": 365.2425 * 24 * 60 * 60 * 1000
+ };
+
+
+ // the allowed tick sizes, after 1 year we use
+ // an integer algorithm
+ var spec = [
+ [1, "second"], [2, "second"], [5, "second"], [10, "second"],
+ [30, "second"],
+ [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
+ [30, "minute"],
+ [1, "hour"], [2, "hour"], [4, "hour"],
+ [8, "hour"], [12, "hour"],
+ [1, "day"], [2, "day"], [3, "day"],
+ [0.25, "month"], [0.5, "month"], [1, "month"],
+ [2, "month"], [3, "month"], [6, "month"],
+ [1, "year"]
+ ];
+
+ var minSize = 0;
+ if (axisOptions.minTickSize != null) {
+ if (typeof axisOptions.tickSize == "number")
+ minSize = axisOptions.tickSize;
+ else
+ minSize = axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]];
+ }
+
+ for (i = 0; i < spec.length - 1; ++i) {
+ if (delta < (spec[i][0] * timeUnitSize[spec[i][1]] + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) {
+ break;
+ }
+ }
+
+ size = spec[i][0];
+ unit = spec[i][1];
+
+ // special-case the possibility of several years
+ if (unit == "year") {
+ magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
+ norm = (delta / timeUnitSize.year) / magn;
+ if (norm < 1.5)
+ size = 1;
+ else if (norm < 3)
+ size = 2;
+ else if (norm < 7.5)
+ size = 5;
+ else
+ size = 10;
+
+ size *= magn;
+ }
+
+ if (axisOptions.tickSize) {
+ size = axisOptions.tickSize[0];
+ unit = axisOptions.tickSize[1];
+ }
+
+ var floorInBase = this.floorInBase; //gives us a reference to a global function..
+
+ generator = function(axis) {
+ var ticks = [],
+ tickSize = axis.tickSize[0], unit = axis.tickSize[1],
+ d = new Date(axis.min);
+
+ var step = tickSize * timeUnitSize[unit];
+
+
+
+ if (unit == "second")
+ d.setSeconds(floorInBase(d.getSeconds(), tickSize));
+ if (unit == "minute")
+ d.setMinutes(floorInBase(d.getMinutes(), tickSize));
+ if (unit == "hour")
+ d.setHours(floorInBase(d.getHours(), tickSize));
+ if (unit == "month")
+ d.setMonth(floorInBase(d.getMonth(), tickSize));
+ if (unit == "year")
+ d.setFullYear(floorInBase(d.getFullYear(), tickSize));
+
+ // reset smaller components
+ d.setMilliseconds(0);
+ if (step >= timeUnitSize.minute)
+ d.setSeconds(0);
+ if (step >= timeUnitSize.hour)
+ d.setMinutes(0);
+ if (step >= timeUnitSize.day)
+ d.setHours(0);
+ if (step >= timeUnitSize.day * 4)
+ d.setDate(1);
+ if (step >= timeUnitSize.year)
+ d.setMonth(0);
+
+
+ var carry = 0, v;
+ do {
+ v = d.getTime();
+ ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
+ if (unit == "month") {
+ if (tickSize < 1) {
+ d.setDate(1);
+ var start = d.getTime();
+ d.setMonth(d.getMonth() + 1);
+ var end = d.getTime();
+ d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
+ carry = d.getHours();
+ d.setHours(0);
+ }
+ else
+ d.setMonth(d.getMonth() + tickSize);
+ }
+ else if (unit == "year") {
+ d.setFullYear(d.getFullYear() + tickSize);
+ }
+ else
+ d.setTime(v + step);
+ } while (v < axis.max);
+
+ return ticks;
+ };
+
+ formatter = function (v, axis) {
+ var d = new Date(v);
+
+ // first check global format
+ if (axisOptions.timeformat != null)
+ return formatDate(d, axisOptions.timeformat, axisOptions.monthNames);
+
+ var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
+ var span = axis.max - axis.min;
+
+ if (t < timeUnitSize.minute)
+ fmt = "%h:%M:%S";
+ else if (t < timeUnitSize.day) {
+ if (span < 2 * timeUnitSize.day)
+ fmt = "%h:%M";
+ else
+ fmt = "%b %d %h:%M";
+ }
+ else if (t < timeUnitSize.month)
+ fmt = "%b %d";
+ else if (t < timeUnitSize.year) {
+ if (span < timeUnitSize.year)
+ fmt = "%b";
+ else
+ fmt = "%b %y";
+ }
+ else
+ fmt = "%y";
+
+ return formatDate(d, fmt, axisOptions.monthNames);
+ };
+ }
+ else {
+ // pretty rounding of base-10 numbers
+ var maxDec = axisOptions.tickDecimals;
+ var dec = -Math.floor(Math.log(delta) / Math.LN10);
+ if (maxDec != null && dec > maxDec)
+ dec = maxDec;
+
+ magn = Math.pow(10, -dec);
+ norm = delta / magn; // norm is between 1.0 and 10.0
+
+ if (norm < 1.5)
+ size = 1;
+ else if (norm < 3) {
+ size = 2;
+ // special case for 2.5, requires an extra decimal
+ if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
+ size = 2.5;
+ ++dec;
+ }
+ }
+ else if (norm < 7.5)
+ size = 5;
+ else
+ size = 10;
+
+ size *= magn;
+
+ if (axisOptions.minTickSize != null && size < axisOptions.minTickSize)
+ size = axisOptions.minTickSize;
+
+ if (axisOptions.tickSize != null)
+ size = axisOptions.tickSize;
+
+ axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec);
+
+ var floorInBase = this.floorInBase;
+
+ generator = function (axis) {
+ var ticks = [];
+ var start = floorInBase(axis.min, axis.tickSize);
+ // then spew out all possible ticks
+ var i = 0, v;
+ do {
+ v = start + i * axis.tickSize;
+ ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
+ ++i;
+ } while (v < axis.max);
+ return ticks;
+ };
+
+ formatter = function (v, axis) {
+ if(v) {
+ return v.toFixed(axis.tickDecimals);
+ }
+ return 0;
+ };
+ }
+
+ axis.tickSize = unit ? [size, unit] : size;
+ axis.tickGenerator = generator;
+ if (Object.isFunction(axisOptions.tickFormatter))
+ axis.tickFormatter = function (v, axis) { return "" + axisOptions.tickFormatter(v, axis); };
+ else
+ axis.tickFormatter = formatter;
+ },
+ /**
+ * function: extendXRangeIfNeededByBar
+ */
+ extendXRangeIfNeededByBar: function() {
+
+ if (this.options.xaxis.max == null) {
+ // great, we're autoscaling, check if we might need a bump
+ var newmax = this.xaxis.max;
+ this.graphData.each(function(gd){
+ if(gd.bars.show && gd.bars.barWidth + this.xaxis.datamax > newmax)
+ {
+ newmax = this.xaxis.datamax + gd.bars.barWidth;
+ }
+ }.bind(this));
+ this.xaxis.nax = newmax;
+
+ }
+ },
+ /**
+ * function: setTicks
+ *
+ * parameters:
+ * {Object} axis
+ * {Object} axisOptions
+ */
+ setTicks: function(axis, axisOptions) {
+ axis.ticks = [];
+
+ if (axisOptions.ticks == null)
+ axis.ticks = axis.tickGenerator(axis);
+ else if (typeof axisOptions.ticks == "number") {
+ if (axisOptions.ticks > 0)
+ axis.ticks = axis.tickGenerator(axis);
+ }
+ else if (axisOptions.ticks) {
+ var ticks = axisOptions.ticks;
+
+ if (Object.isFunction(ticks))
+ // generate the ticks
+ ticks = ticks({ min: axis.min, max: axis.max });
+
+ // clean up the user-supplied ticks, copy them over
+ //var i, v;
+ ticks.each(function(t, i){
+ var v = null;
+ var label = null;
+ if(typeof t == 'object') {
+ v = t[0];
+ if(t.length > 1) { label = t[1]; }
+ }
+ else {
+ v = t;
+ }
+ if(!label) {
+ label = axis.tickFormatter(v, axis);
+ }
+ axis.ticks[i] = {v: v, label: label}
+ }.bind(this));
+
+ }
+
+ if (axisOptions.autoscaleMargin != null && axis.ticks.length > 0) {
+ if (axisOptions.min == null)
+ axis.min = Math.min(axis.min, axis.ticks[0].v);
+ if (axisOptions.max == null && axis.ticks.length > 1)
+ axis.max = Math.min(axis.max, axis.ticks[axis.ticks.length - 1].v);
+ }
+ },
+ /**
+ * Function: setSpacing
+ *
+ * Parameters: none
+ */
+ setSpacing: function() {
+ // calculate y label dimensions
+ var i, labels = [], l;
+ for (i = 0; i < this.yaxis.ticks.length; ++i) {
+ l = this.yaxis.ticks[i].label;
+
+ if (l)
+ labels.push('<div class="tickLabel">' + l + '</div>');
+ }
+
+ if (labels.length > 0) {
+ var dummyDiv = new Element('div', {'style': 'position:absolute;top:-10000px;font-size:smaller'});
+ dummyDiv.update(labels.join(""));
+ this.domObj.insert(dummyDiv);
+ this.yLabelMaxWidth = dummyDiv.getWidth();
+ this.yLabelMaxHeight = dummyDiv.select('div')[0].getHeight();
+ dummyDiv.remove();
+ }
+
+ var maxOutset = this.options.grid.borderWidth;
+ if (this.options.points.show)
+ maxOutset = Math.max(maxOutset, this.options.points.radius + this.options.points.lineWidth/2);
+ for (i = 0; i < this.graphData.length; ++i) {
+ if (this.graphData[i].points.show)
+ maxOutset = Math.max(maxOutset, this.graphData[i].points.radius + this.graphData[i].points.lineWidth/2);
+ }
+
+ this.chartOffset.left = this.chartOffset.right = this.chartOffset.top = this.chartOffset.bottom = maxOutset;
+
+ this.chartOffset.left += this.yLabelMaxWidth + this.options.grid.labelMargin;
+ this.chartWidth = this.canvasWidth - this.chartOffset.left - this.chartOffset.right;
+
+ this.xLabelBoxWidth = this.chartWidth / 6;
+ labels = [];
+
+ for (i = 0; i < this.xaxis.ticks.length; ++i) {
+ l = this.xaxis.ticks[i].label;
+ if (l) {
+ labels.push('<span class="tickLabel" width="' + this.xLabelBoxWidth + '">' + l + '</span>');
+ }
+ }
+
+ var xLabelMaxHeight = 0;
+ if (labels.length > 0) {
+ var dummyDiv = new Element('div', {'style': 'position:absolute;top:-10000px;font-size:smaller'});
+ dummyDiv.update(labels.join(""));
+ this.domObj.appendChild(dummyDiv);
+ xLabelMaxHeight = dummyDiv.getHeight();
+ dummyDiv.remove();
+ }
+
+ this.chartOffset.bottom += xLabelMaxHeight + this.options.grid.labelMargin;
+ this.chartHeight = this.canvasHeight - this.chartOffset.bottom - this.chartOffset.top;
+ this.hozScale = this.chartWidth / (this.xaxis.max - this.xaxis.min);
+ this.vertScale = this.chartHeight / (this.yaxis.max - this.yaxis.min);
+ },
+ /**
+ * function: draw
+ */
+ draw: function() {
+ if(this.options.bars.show)
+ {
+ this.extendXRangeIfNeededByBar();
+ this.setSpacing();
+ this.drawGrid();
+ this.drawBarGraph(this.graphData, this.barDataRange);
+ }
+ else if(this.options.pies.show)
+ {
+ this.preparePieData(this.graphData);
+ this.drawPieGraph(this.graphData);
+ }
+ else
+ {
+ this.drawGrid();
+ for (var i = 0; i < this.graphData.length; i++) {
+ this.drawGraph(this.graphData[i]);
+ }
+ }
+ },
+ /**
+ * function: translateHoz
+ *
+ * Paramters:
+ * {Object} x
+ *
+ * Description: Given a value this function translate it to relative x coord on canvas
+ */
+ translateHoz: function(x) {
+ return (x - this.xaxis.min) * this.hozScale;
+ },
+ /**
+ * function: translateVert
+ *
+ * parameters:
+ * {Object} y
+ *
+ * Description: Given a value this function translate it to relative y coord on canvas
+ */
+ translateVert: function(y) {
+ return this.chartHeight - (y - this.yaxis.min) * this.vertScale;
+ },
+ /**
+ * function: drawGrid
+ *
+ * parameters: none
+ *
+ * description: draws the actual grid on the canvas
+ */
+ drawGrid: function() {
+ var i;
+
+ this.context.save();
+ this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
+ this.context.translate(this.chartOffset.left, this.chartOffset.top);
+
+ // draw background, if any
+ if (this.options.grid.backgroundColor != null) {
+ this.context.fillStyle = this.options.grid.backgroundColor;
+ this.context.fillRect(0, 0, this.chartWidth, this.chartHeight);
+ }
+
+ // draw colored areas
+ if (this.options.grid.coloredAreas) {
+ var areas = this.options.grid.coloredAreas;
+ if (Object.isFunction(areas)) {
+ areas = areas({ xmin: this.xaxis.min, xmax: this.xaxis.max, ymin: this.yaxis.min, ymax: this.yaxis.max });
+ }
+
+ areas.each(function(a){
+ // clip
+ if (a.x1 == null || a.x1 < this.xaxis.min)
+ a.x1 = this.xaxis.min;
+ if (a.x2 == null || a.x2 > this.xaxis.max)
+ a.x2 = this.xaxis.max;
+ if (a.y1 == null || a.y1 < this.yaxis.min)
+ a.y1 = this.yaxis.min;
+ if (a.y2 == null || a.y2 > this.yaxis.max)
+ a.y2 = this.yaxis.max;
+
+ var tmp;
+ if (a.x1 > a.x2) {
+ tmp = a.x1;
+ a.x1 = a.x2;
+ a.x2 = tmp;
+ }
+ if (a.y1 > a.y2) {
+ tmp = a.y1;
+ a.y1 = a.y2;
+ a.y2 = tmp;
+ }
+
+ if (a.x1 >= this.xaxis.max || a.x2 <= this.xaxis.min || a.x1 == a.x2
+ || a.y1 >= this.yaxis.max || a.y2 <= this.yaxis.min || a.y1 == a.y2)
+ return;
+
+ this.context.fillStyle = a.color || this.options.grid.coloredAreasColor;
+ this.context.fillRect(Math.floor(this.translateHoz(a.x1)), Math.floor(this.translateVert(a.y2)),
+ Math.floor(this.translateHoz(a.x2) - this.translateHoz(a.x1)), Math.floor(this.translateVert(a.y1) - this.translateVert(a.y2)));
+ }.bind(this));
+
+
+ }
+
+ // draw the inner grid
+ this.context.lineWidth = 1;
+ this.context.strokeStyle = this.options.grid.tickColor;
+ this.context.beginPath();
+ var v;
+ if (this.options.grid.drawXAxis) {
+ this.xaxis.ticks.each(function(aTick){
+ v = aTick.v;
+ if(v <= this.xaxis.min || v >= this.xaxis.max) {
+ return;
+ }
+ this.context.moveTo(Math.floor(this.translateHoz(v)) + this.context.lineWidth / 2, 0);
+ this.context.lineTo(Math.floor(this.translateHoz(v)) + this.context.lineWidth / 2, this.chartHeight);
+ }.bind(this));
+
+ }
+
+ if (this.options.grid.drawYAxis) {
+ this.yaxis.ticks.each(function(aTick){
+ v = aTick.v;
+ if(v <= this.yaxis.min || v >= this.yaxis.max) {
+ return;
+ }
+ this.context.moveTo(0, Math.floor(this.translateVert(v)) + this.context.lineWidth / 2);
+ this.context.lineTo(this.chartWidth, Math.floor(this.translateVert(v)) + this.context.lineWidth / 2);
+ }.bind(this));
+
+ }
+ this.context.stroke();
+
+ if (this.options.grid.borderWidth) {
+ // draw border
+ this.context.lineWidth = this.options.grid.borderWidth;
+ this.context.strokeStyle = this.options.grid.color;
+ this.context.lineJoin = "round";
+ this.context.strokeRect(0, 0, this.chartWidth, this.chartHeight);
+ this.context.restore();
+ }
+ },
+ /**
+ * function: insertLabels
+ *
+ * parameters: none
+ *
+ * description: inserts the label with proper spacing. Both on X and Y axis
+ */
+ insertLabels: function() {
+ this.domObj.select(".tickLabels").invoke('remove');
+
+ var i, tick;
+ var html = '<div class="tickLabels" style="font-size:smaller;color:' + this.options.grid.color + '">';
+
+ // do the x-axis
+ this.xaxis.ticks.each(function(tick){
+ if (!tick.label || tick.v < this.xaxis.min || tick.v > this.xaxis.max)
+ return;
+ html += '<div style="position:absolute;top:' + (this.chartOffset.top + this.chartHeight + this.options.grid.labelMargin) + 'px;left:' + (this.chartOffset.left + this.translateHoz(tick.v) - this.xLabelBoxWidth/2) + 'px;width:' + this.xLabelBoxWidth + 'px;text-align:center" class="tickLabel">' + tick.label + "</div>";
+
+ }.bind(this));
+
+ // do the y-axis
+ this.yaxis.ticks.each(function(tick){
+ if (!tick.label || tick.v < this.yaxis.min || tick.v > this.yaxis.max)
+ return;
+ html += '<div id="ylabels" style="position:absolute;top:' + (this.chartOffset.top + this.translateVert(tick.v) - this.yLabelMaxHeight/2) + 'px;left:0;width:' + this.yLabelMaxWidth + 'px;text-align:right" class="tickLabel">' + tick.label + "</div>";
+ }.bind(this));
+
+ html += '</div>';
+
+ this.domObj.insert(html);
+ },
+ /**
+ * function: drawGraph
+ *
+ * Paramters:
+ * {Object} graphData
+ *
+ * Description: given a graphData (series) this function calls a proper lower level method to draw it.
+ */
+ drawGraph: function(graphData) {
+ if (graphData.lines.show || (!graphData.bars.show && !graphData.points.show))
+ this.drawGraphLines(graphData);
+ if (graphData.bars.show)
+ this.drawGraphBar(graphData);
+ if (graphData.points.show)
+ this.drawGraphPoints(graphData);
+ },
+ /**
+ * function: plotLine
+ *
+ * parameters:
+ * {Object} data
+ * {Object} offset
+ *
+ * description:
+ * Helper function that plots a line based on the data provided
+ */
+ plotLine: function(data, offset) {
+ var prev, cur = null, drawx = null, drawy = null;
+
+ this.context.beginPath();
+ for (var i = 0; i < data.length; ++i) {
+ prev = cur;
+ cur = data[i];
+
+ if (prev == null || cur == null)
+ continue;
+
+ var x1 = prev[0], y1 = prev[1],
+ x2 = cur[0], y2 = cur[1];
+
+ // clip with ymin
+ if (y1 <= y2 && y1 < this.yaxis.min) {
+ if (y2 < this.yaxis.min)
+ continue; // line segment is outside
+ // compute new intersection point
+ x1 = (this.yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y1 = this.yaxis.min;
+ }
+ else if (y2 <= y1 && y2 < this.yaxis.min) {
+ if (y1 < this.yaxis.min)
+ continue;
+ x2 = (this.yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y2 = this.yaxis.min;
+ }
+
+ // clip with ymax
+ if (y1 >= y2 && y1 > this.yaxis.max) {
+ if (y2 > this.yaxis.max)
+ continue;
+ x1 = (this.yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y1 = this.yaxis.max;
+ }
+ else if (y2 >= y1 && y2 > this.yaxis.max) {
+ if (y1 > this.yaxis.max)
+ continue;
+ x2 = (this.yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y2 = this.yaxis.max;
+ }
+
+ // clip with xmin
+ if (x1 <= x2 && x1 < this.xaxis.min) {
+ if (x2 < this.xaxis.min)
+ continue;
+ y1 = (this.xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x1 = this.xaxis.min;
+ }
+ else if (x2 <= x1 && x2 < this.xaxis.min) {
+ if (x1 < this.xaxis.min)
+ continue;
+ y2 = (this.xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x2 = this.xaxis.min;
+ }
+
+ // clip with xmax
+ if (x1 >= x2 && x1 > this.xaxis.max) {
+ if (x2 > this.xaxis.max)
+ continue;
+ y1 = (this.xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x1 = this.xaxis.max;
+ }
+ else if (x2 >= x1 && x2 > this.xaxis.max) {
+ if (x1 > this.xaxis.max)
+ continue;
+ y2 = (this.xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x2 = this.xaxis.max;
+ }
+
+ if (drawx != this.translateHoz(x1) || drawy != this.translateVert(y1) + offset)
+ this.context.moveTo(this.translateHoz(x1), this.translateVert(y1) + offset);
+
+ drawx = this.translateHoz(x2);
+ drawy = this.translateVert(y2) + offset;
+ this.context.lineTo(drawx, drawy);
+ }
+ this.context.stroke();
+ },
+ /**
+ * function: plotLineArea
+ *
+ * parameters:
+ * {Object} data
+ *
+ * description:
+ * Helper functoin that plots a colored line graph. This function
+ * takes the data nad then fill in the area on the graph properly
+ */
+ plotLineArea: function(data) {
+ var prev, cur = null;
+
+ var bottom = Math.min(Math.max(0, this.yaxis.min), this.yaxis.max);
+ var top, lastX = 0;
+
+ var areaOpen = false;
+
+ for (var i = 0; i < data.length; ++i) {
+ prev = cur;
+ cur = data[i];
+
+ if (areaOpen && prev != null && cur == null) {
+ // close area
+ this.context.lineTo(this.translateHoz(lastX), this.translateVert(bottom));
+ this.context.fill();
+ areaOpen = false;
+ continue;
+ }
+
+ if (prev == null || cur == null)
+ continue;
+
+ var x1 = prev[0], y1 = prev[1],
+ x2 = cur[0], y2 = cur[1];
+
+ // clip x values
+
+ // clip with xmin
+ if (x1 <= x2 && x1 < this.xaxis.min) {
+ if (x2 < this.xaxis.min)
+ continue;
+ y1 = (this.xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x1 = this.xaxis.min;
+ }
+ else if (x2 <= x1 && x2 < this.xaxis.min) {
+ if (x1 < this.xaxis.min)
+ continue;
+ y2 = (this.xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x2 = this.xaxis.min;
+ }
+
+ // clip with xmax
+ if (x1 >= x2 && x1 > this.xaxis.max) {
+ if (x2 > this.xaxis.max)
+ continue;
+ y1 = (this.xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x1 = this.xaxis.max;
+ }
+ else if (x2 >= x1 && x2 > this.xaxis.max) {
+ if (x1 > this.xaxis.max)
+ continue;
+ y2 = (this.xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x2 = this.xaxis.max;
+ }
+
+ if (!areaOpen) {
+ // open area
+ this.context.beginPath();
+ this.context.moveTo(this.translateHoz(x1), this.translateVert(bottom));
+ areaOpen = true;
+ }
+
+ // now first check the case where both is outside
+ if (y1 >= this.yaxis.max && y2 >= this.yaxis.max) {
+ this.context.lineTo(this.translateHoz(x1), this.translateVert(this.yaxis.max));
+ this.context.lineTo(this.translateHoz(x2), this.translateVert(this.yaxis.max));
+ continue;
+ }
+ else if (y1 <= this.yaxis.min && y2 <= this.yaxis.min) {
+ this.context.lineTo(this.translateHoz(x1), this.translateVert(this.yaxis.min));
+ this.context.lineTo(this.translateHoz(x2), this.translateVert(this.yaxis.min));
+ continue;
+ }
+
+ var x1old = x1, x2old = x2;
+
+ // clip with ymin
+ if (y1 <= y2 && y1 < this.yaxis.min && y2 >= this.yaxis.min) {
+ x1 = (this.yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y1 = this.yaxis.min;
+ }
+ else if (y2 <= y1 && y2 < this.yaxis.min && y1 >= this.yaxis.min) {
+ x2 = (this.yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y2 = this.yaxis.min;
+ }
+
+ // clip with ymax
+ if (y1 >= y2 && y1 > this.yaxis.max && y2 <= this.yaxis.max) {
+ x1 = (this.yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y1 = this.yaxis.max;
+ }
+ else if (y2 >= y1 && y2 > this.yaxis.max && y1 <= this.yaxis.max) {
+ x2 = (this.yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y2 = this.yaxis.max;
+ }
+
+
+ // if the x value was changed we got a rectangle
+ // to fill
+ if (x1 != x1old) {
+ if (y1 <= this.yaxis.min)
+ top = this.yaxis.min;
+ else
+ top = this.yaxis.max;
+
+ this.context.lineTo(this.translateHoz(x1old), this.translateVert(top));
+ this.context.lineTo(this.translateHoz(x1), this.translateVert(top));
+ }
+
+ // fill the triangles
+ this.context.lineTo(this.translateHoz(x1), this.translateVert(y1));
+ this.context.lineTo(this.translateHoz(x2), this.translateVert(y2));
+
+ // fill the other rectangle if it's there
+ if (x2 != x2old) {
+ if (y2 <= this.yaxis.min)
+ top = this.yaxis.min;
+ else
+ top = this.yaxis.max;
+
+ this.context.lineTo(this.translateHoz(x2old), this.translateVert(top));
+ this.context.lineTo(this.translateHoz(x2), this.translateVert(top));
+ }
+
+ lastX = Math.max(x2, x2old);
+ }
+
+ if (areaOpen) {
+ this.context.lineTo(this.translateHoz(lastX), this.translateVert(bottom));
+ this.context.fill();
+ }
+ },
+ /**
+ * function: drawGraphLines
+ *
+ * parameters:
+ * {Object} graphData
+ *
+ * description:
+ * Main function that daws the line graph. This function is called
+ * if <options> lines property is set to show or no other type of
+ * graph is specified. This function depends on <plotLineArea> and
+ * <plotLine> functions.
+ */
+ drawGraphLines: function(graphData) {
+ this.context.save();
+ this.context.translate(this.chartOffset.left, this.chartOffset.top);
+ this.context.lineJoin = "round";
+
+ var lw = graphData.lines.lineWidth;
+ var sw = graphData.shadowSize;
+ // FIXME: consider another form of shadow when filling is turned on
+ if (sw > 0) {
+ // draw shadow in two steps
+ this.context.lineWidth = sw / 2;
+ this.context.strokeStyle = "rgba(0,0,0,0.1)";
+ this.plotLine(graphData.data, lw/2 + sw/2 + this.context.lineWidth/2);
+
+ this.context.lineWidth = sw / 2;
+ this.context.strokeStyle = "rgba(0,0,0,0.2)";
+ this.plotLine(graphData.data, lw/2 + this.context.lineWidth/2);
+ }
+
+ this.context.lineWidth = lw;
+ this.context.strokeStyle = graphData.color;
+ if (graphData.lines.fill) {
+ this.context.fillStyle = graphData.lines.fillColor != null ? graphData.lines.fillColor : this.parseColor(graphData.color).scale(null, null, null, 0.4).toString();
+ this.plotLineArea(graphData.data, 0);
+ }
+
+ this.plotLine(graphData.data, 0);
+ this.context.restore();
+ },
+ /**
+ * function: plotPoints
+ *
+ * parameters:
+ * {Object} data
+ * {Object} radius
+ * {Object} fill
+ *
+ * description:
+ * Helper function that draws the point graph according to the data provided. Size of each
+ * point is provided by radius variable and fill specifies if points
+ * are filled
+ */
+ plotPoints: function(data, radius, fill) {
+ for (var i = 0; i < data.length; ++i) {
+ if (data[i] == null)
+ continue;
+
+ var x = data[i][0], y = data[i][1];
+ if (x < this.xaxis.min || x > this.xaxis.max || y < this.yaxis.min || y > this.yaxis.max)
+ continue;
+
+ this.context.beginPath();
+ this.context.arc(this.translateHoz(x), this.translateVert(y), radius, 0, 2 * Math.PI, true);
+ if (fill)
+ this.context.fill();
+ this.context.stroke();
+ }
+ },
+ /**
+ * function: plotPointShadows
+ *
+ * parameters:
+ * {Object} data
+ * {Object} offset
+ * {Object} radius
+ *
+ * description:
+ * Helper function that draws the shadows for the points.
+ */
+ plotPointShadows: function(data, offset, radius) {
+ for (var i = 0; i < data.length; ++i) {
+ if (data[i] == null)
+ continue;
+
+ var x = data[i][0], y = data[i][1];
+ if (x < this.xaxis.min || x > this.xaxis.max || y < this.yaxis.min || y > this.yaxis.max)
+ continue;
+ this.context.beginPath();
+ this.context.arc(this.translateHoz(x), this.translateVert(y) + offset, radius, 0, Math.PI, false);
+ this.context.stroke();
+ }
+ },
+ /**
+ * function: drawGraphPoints
+ *
+ * paramters:
+ * {Object} graphData
+ *
+ * description:
+ * Draws the point graph onto the canvas. This function depends on helper
+ * functions <plotPointShadows> and <plotPoints>
+ */
+ drawGraphPoints: function(graphData) {
+ this.context.save();
+ this.context.translate(this.chartOffset.left, this.chartOffset.top);
+
+ var lw = graphData.lines.lineWidth;
+ var sw = graphData.shadowSize;
+ if (sw > 0) {
+ // draw shadow in two steps
+ this.context.lineWidth = sw / 2;
+ this.context.strokeStyle = "rgba(0,0,0,0.1)";
+ this.plotPointShadows(graphData.data, sw/2 + this.context.lineWidth/2, graphData.points.radius);
+
+ this.context.lineWidth = sw / 2;
+ this.context.strokeStyle = "rgba(0,0,0,0.2)";
+ this.plotPointShadows(graphData.data, this.context.lineWidth/2, graphData.points.radius);
+ }
+
+ this.context.lineWidth = graphData.points.lineWidth;
+ this.context.strokeStyle = graphData.color;
+ this.context.fillStyle = graphData.points.fillColor != null ? graphData.points.fillColor : graphData.color;
+ this.plotPoints(graphData.data, graphData.points.radius, graphData.points.fill);
+ this.context.restore();
+ },
+ /**
+ * function: preparePieData
+ *
+ * parameters:
+ * {Object} graphData
+ *
+ * Description:
+ * Helper function that manipulates the given data stream so that it can
+ * be plotted as a Pie Chart
+ */
+ preparePieData: function(graphData)
+ {
+ for(i = 0; i < graphData.length; i++)
+ {
+ var data = 0;
+ for(j = 0; j < graphData[i].data.length; j++){
+ data += parseInt(graphData[i].data[j][1]);
+ }
+ graphData[i].data = data;
+ }
+ },
+ /**
+ * function: drawPieShadow
+ *
+ * {Object} anchorX
+ * {Object} anchorY
+ * {Object} radius
+ *
+ * description:
+ * Helper function that draws a shadow for the Pie Chart. This just draws
+ * a circle with offset that simulates shadow. We do not give each piece
+ * of the pie an individual shadow.
+ */
+ drawPieShadow: function(anchorX, anchorY, radius)
+ {
+ this.context.beginPath();
+ this.context.moveTo(anchorX, anchorY);
+ this.context.fillStyle = 'rgba(0,0,0,' + 0.1 + ')';
+ startAngle = 0;
+ endAngle = (Math.PI/180)*360;
+ this.context.arc(anchorX + 2, anchorY +2, radius + (this.options.shadowSize/2), startAngle, endAngle, false);
+ this.context.fill();
+ this.context.closePath();
+ },
+ /**
+ * function: drawPieGraph
+ *
+ * parameters:
+ * {Object} graphData
+ *
+ * description:
+ * Draws the actual pie chart. This function depends on helper function
+ * <drawPieShadow> to draw the actual shadow
+ */
+ drawPieGraph: function(graphData)
+ {
+ var sumData = 0;
+ var radius = 0;
+ var centerX = this.chartWidth/2;
+ var centerY = this.chartHeight/2;
+ var startAngle = 0;
+ var endAngle = 0;
+ var fontSize = this.options.pies.fontSize;
+ var labelWidth = this.options.pies.labelWidth;
+
+ //determine Pie Radius
+ if(!this.options.pies.autoScale)
+ radius = this.options.pies.radius;
+ else
+ radius = (this.chartHeight * 0.85)/2;
+
+ var labelRadius = radius * 1.05;
+
+ for(i = 0; i < graphData.length; i++)
+ sumData += graphData[i].data;
+
+ // used to adjust labels so that everything adds up to 100%
+ totalPct = 0;
+
+ //lets draw the shadow first.. we don't need an individual shadow to every pie rather we just
+ //draw a circle underneath to simulate the shadow...
+ this.drawPieShadow(centerX, centerY, radius, 0, 0);
+
+ //lets draw the actual pie chart now.
+ graphData.each(function(gd, j){
+ var pct = gd.data / sumData;
+ startAngle = endAngle;
+ endAngle += pct * (2 * Math.PI);
+ var sliceMiddle = (endAngle - startAngle) / 2 + startAngle;
+ var labelX = centerX + Math.cos(sliceMiddle) * labelRadius;
+ var labelY = centerY + Math.sin(sliceMiddle) * labelRadius;
+ var anchorX = centerX;
+ var anchorY = centerY;
+ var textAlign = null;
+ var verticalAlign = null;
+ var left = 0;
+ var top = 0;
+
+ //draw pie:
+ //drawing pie
+ this.context.beginPath();
+ this.context.moveTo(anchorX, anchorY);
+ this.context.arc(anchorX, anchorY, radius, startAngle, endAngle, false);
+ this.context.closePath();
+ this.context.fillStyle = this.parseColor(gd.color).scale(null, null, null, this.options.pies.fillOpacity).toString();
+
+ if(this.options.pies.fill) { this.context.fill(); }
+
+ // drawing labels
+ if (sliceMiddle <= 0.25 * (2 * Math.PI))
+ {
+ // text on top and align left
+ textAlign = "left";
+ verticalAlign = "top";
+ left = labelX;
+ top = labelY + fontSize;
+ }
+ else if (sliceMiddle > 0.25 * (2 * Math.PI) && sliceMiddle <= 0.5 * (2 * Math.PI))
+ {
+ // text on bottom and align left
+ textAlign = "left";
+ verticalAlign = "bottom";
+ left = labelX - labelWidth;
+ top = labelY;
+ }
+ else if (sliceMiddle > 0.5 * (2 * Math.PI) && sliceMiddle <= 0.75 * (2 * Math.PI))
+ {
+ // text on bottom and align right
+ textAlign = "right";
+ verticalAlign = "bottom";
+ left = labelX - labelWidth;
+ top = labelY - fontSize;
+ }
+ else
+ {
+ // text on top and align right
+ textAlign = "right";
+ verticalAlign = "bottom";
+ left = labelX;
+ top = labelY - fontSize;
+ }
+
+ left = left + "px";
+ top = top + "px";
+ var textVal = Math.round(pct * 100);
+
+ if (j == graphData.length - 1) {
+ if (textVal + totalPct < 100) {
+ textVal = textVal + 1;
+ } else if (textVal + totalPct > 100) {
+ textVal = textVal - 1;
+ };
+ }
+
+ var html = "<div style=\"position: absolute;zindex:11; width:" + labelWidth + "px;fontSize:" + fontSize + "px;overflow:hidden;top:"+ top + ";left:"+ left + ";textAlign:" + textAlign + ";verticalAlign:" + verticalAlign +"\">" + textVal + "%</div>";
+ //$(html).appendTo(target);
+ this.domObj.insert(html);
+
+ totalPct = totalPct + textVal;
+ }.bind(this));
+
+ },
+ /**
+ * function: drawBarGraph
+ *
+ * parameters:
+ * {Object} graphData
+ * {Object} barDataRange
+ *
+ * description:
+ * Goes through each series in graphdata and passes it onto <drawBarGraphs> function
+ */
+ drawBarGraph: function(graphData, barDataRange)
+ {
+ graphData.each(function(gd, i){
+ this.drawGraphBars(gd, i, graphData.size(), barDataRange);
+ }.bind(this));
+ },
+ /**
+ * function: drawGraphBar
+ *
+ * parameters:
+ * {Object} graphData
+ *
+ * description:
+ * This function is called when an individual series in GraphData is bar graph and plots it
+ */
+ drawGraphBar: function(graphData)
+ {
+ this.drawGraphBars(graphData, 0, this.graphData.length, this.barDataRange);
+ },
+ /**
+ * function: plotBars
+ *
+ * parameters:
+ * {Object} graphData
+ * {Object} data
+ * {Object} barWidth
+ * {Object} offset
+ * {Object} fill
+ * {Object} counter
+ * {Object} total
+ * {Object} barDataRange
+ *
+ * description:
+ * Helper function that draws the bar graph based on data.
+ */
+ plotBars: function(graphData, data, barWidth, offset, fill,counter, total, barDataRange) {
+ var shift = 0;
+
+ if(total % 2 == 0)
+ {
+ shift = (1 + ((counter - total /2 ) - 1)) * barWidth;
+ }
+ else
+ {
+ var interval = 0.5;
+ if(counter == (total/2 - interval )) {
+ shift = - barWidth * interval;
+ }
+ else {
+ shift = (interval + (counter - Math.round(total/2))) * barWidth;
+ }
+ }
+
+ var rangeData = [];
+ data.each(function(d){
+ if(!d) return;
+
+ var x = d[0], y = d[1];
+ var drawLeft = true, drawTop = true, drawRight = true;
+ var left = x + shift, right = x + barWidth + shift, bottom = 0, top = y;
+ var rangeDataPoint = {};
+ rangeDataPoint.left = left;
+ rangeDataPoint.right = right;
+ rangeDataPoint.value = top;
+ rangeData.push(rangeDataPoint);
+
+ if (right < this.xaxis.min || left > this.xaxis.max || top < this.yaxis.min || bottom > this.yaxis.max)
+ return;
+
+ // clip
+ if (left < this.xaxis.min) {
+ left = this.xaxis.min;
+ drawLeft = false;
+ }
+
+ if (right > this.xaxis.max) {
+ right = this.xaxis.max;
+ drawRight = false;
+ }
+
+ if (bottom < this.yaxis.min)
+ bottom = this.yaxis.min;
+
+ if (top > this.yaxis.max) {
+ top = this.yaxis.max;
+ drawTop = false;
+ }
+
+ if(graphData.bars.showShadow && graphData.shadowSize > 0)
+ this.plotShadowOutline(graphData, this.context.strokeStyle, left, bottom, top, right, drawLeft, drawRight, drawTop);
+
+ // fill the bar
+ if (fill) {
+ this.context.beginPath();
+ this.context.moveTo(this.translateHoz(left), this.translateVert(bottom) + offset);
+ this.context.lineTo(this.translateHoz(left), this.translateVert(top) + offset);
+ this.context.lineTo(this.translateHoz(right), this.translateVert(top) + offset);
+ this.context.lineTo(this.translateHoz(right), this.translateVert(bottom) + offset);
+ this.context.fill();
+ }
+
+ // draw outline
+ if (drawLeft || drawRight || drawTop) {
+ this.context.beginPath();
+ this.context.moveTo(this.translateHoz(left), this.translateVert(bottom) + offset);
+ if (drawLeft)
+ this.context.lineTo(this.translateHoz(left), this.translateVert(top) + offset);
+ else
+ this.context.moveTo(this.translateHoz(left), this.translateVert(top) + offset);
+
+ if (drawTop)
+ this.context.lineTo(this.translateHoz(right), this.translateVert(top) + offset);
+ else
+ this.context.moveTo(this.translateHoz(right), this.translateVert(top) + offset);
+ if (drawRight)
+ this.context.lineTo(this.translateHoz(right), this.translateVert(bottom) + offset);
+ else
+ this.context.moveTo(this.translateHoz(right), this.translateVert(bottom) + offset);
+ this.context.stroke();
+ }
+ }.bind(this));
+
+ barDataRange.push(rangeData);
+ },
+ /**
+ * function: plotShadowOutline
+ *
+ * parameters:
+ * {Object} graphData
+ * {Object} orgStrokeStyle
+ * {Object} left
+ * {Object} bottom
+ * {Object} top
+ * {Object} right
+ * {Object} drawLeft
+ * {Object} drawRight
+ * {Object} drawTop
+ *
+ * description:
+ * Helper function that draws a outline simulating shadow for bar chart
+ */
+ plotShadowOutline: function(graphData, orgStrokeStyle, left, bottom, top, right, drawLeft, drawRight, drawTop)
+ {
+ var orgOpac = 0.3;
+
+ for(var n = 1; n <= this.options.shadowSize/2; n++)
+ {
+ var opac = orgOpac * n;
+ this.context.beginPath();
+ this.context.strokeStyle = "rgba(0,0,0," + opac + ")";
+
+ this.context.moveTo(this.translateHoz(left) + n, this.translateVert(bottom));
+
+ if(drawLeft)
+ this.context.lineTo(this.translateHoz(left) + n, this.translateVert(top) - n);
+ else
+ this.context.moveTo(this.translateHoz(left) + n, this.translateVert(top) - n);
+
+ if(drawTop)
+ this.context.lineTo(this.translateHoz(right) + n, this.translateVert(top) - n);
+ else
+ this.context.moveTo(this.translateHoz(right) + n, this.translateVert(top) - n);
+
+ if(drawRight)
+ this.context.lineTo(this.translateHoz(right) + n, this.translateVert(bottom));
+ else
+ this.context.lineTo(this.translateHoz(right) + n, this.translateVert(bottom));
+
+ this.context.stroke();
+ this.context.closePath();
+ }
+
+ this.context.strokeStyle = orgStrokeStyle;
+ },
+ /**
+ * function: drawGraphBars
+ *
+ * parameters:
+ * {Object} graphData
+ * {Object} counter
+ * {Object} total
+ * {Object} barDataRange
+ *
+ * description:
+ * Draws the actual bar graphs. Calls <plotBars> to draw the individual bar
+ */
+ drawGraphBars: function(graphData, counter, total, barDataRange){
+ this.context.save();
+ this.context.translate(this.chartOffset.left, this.chartOffset.top);
+ this.context.lineJoin = "round";
+
+ var bw = graphData.bars.barWidth;
+ var lw = Math.min(graphData.bars.lineWidth, bw);
+
+
+ this.context.lineWidth = lw;
+ this.context.strokeStyle = graphData.color;
+ if (graphData.bars.fill) {
+ this.context.fillStyle = graphData.bars.fillColor != null ? graphData.bars.fillColor : this.parseColor(graphData.color).scale(null, null, null, this.options.bars.fillOpacity).toString();
+ }
+ this.plotBars(graphData, graphData.data, bw, 0, graphData.bars.fill, counter, total, barDataRange);
+ this.context.restore();
+ },
+ /**
+ * function: insertLegend
+ *
+ * description:
+ * inserts legend onto the graph. *legend: {show: true}* must be set in <options>
+ * for for this to work.
+ */
+ insertLegend: function() {
+ this.domObj.select(".legend").invoke('remove');
+
+ if (!this.options.legend.show)
+ return;
+
+ var fragments = [];
+ var rowStarted = false;
+ this.graphData.each(function(gd, index){
+ if(!gd.label) {
+ return;
+ }
+ if(index % this.options.legend.noColumns == 0) {
+ if(rowStarted) {
+ fragments.push('</tr>');
+ }
+ fragments.push('<tr>');
+ rowStarted = true;
+ }
+ var label = gd.label;
+ if(this.options.legend.labelFormatter != null) {
+ label = this.options.legend.labelFormatter(label);
+ }
+
+ fragments.push(
+ '<td class="legendColorBox"><div style="border:1px solid ' + this.options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:14px;height:10px;background-color:' + gd.color + ';overflow:hidden"></div></div></td>' +
+ '<td class="legendLabel">' + label + '</td>');
+
+ }.bind(this));
+
+ if (rowStarted)
+ fragments.push('</tr>');
+
+ if(fragments.length > 0){
+ var table = '<table style="font-size:smaller;color:' + this.options.grid.color + '">' + fragments.join("") + '</table>';
+ if($(this.options.legend.container) != null){
+ $(this.options.legend.container).insert(table);
+ }else{
+ var pos = '';
+ var p = this.options.legend.position, m = this.options.legend.margin;
+
+ if(p.charAt(0) == 'n') pos += 'top:' + (m + this.chartOffset.top) + 'px;';
+ else if(p.charAt(0) == 's') pos += 'bottom:' + (m + this.chartOffset.bottom) + 'px;';
+ if(p.charAt(1) == 'e') pos += 'right:' + (m + this.chartOffset.right) + 'px;';
+ else if(p.charAt(1) == 'w') pos += 'left:' + (m + this.chartOffset.bottom) + 'px;';
+ var div = this.domObj.insert('<div class="ProtoChart-legend" style="border: 1px solid '+this.options.legend.borderColor+'; position:absolute;z-index:2;' + pos +'">' + table + '</div>').getElementsBySelector('div.ProtoChart-legend').first();
+
+ if(this.options.legend.backgroundOpacity != 0.0){
+ var c = this.options.legend.backgroundColor;
+ if(c == null){
+ var tmp = (this.options.grid.backgroundColor != null) ? this.options.grid.backgroundColor : this.extractColor(div);
+ c = this.parseColor(tmp).adjust(null, null, null, 1).toString();
+ }
+ this.domObj.insert('<div class="ProtoChart-legend-bg" style="position:absolute;width:' + div.getWidth() + 'px;height:' + div.getHeight() + 'px;' + pos +'background-color:' + c + ';"> </div>').select('div.ProtoChart-legend-bg').first().setStyle({
+ 'opacity': this.options.legend.backgroundOpacity
+ });
+ }
+ }
+ }
+ },
+ /**
+ * Function: onMouseMove
+ *
+ * parameters:
+ * event: {Object} ev
+ *
+ * Description:
+ * Called whenever the mouse is moved on the graph. This takes care of the mousetracking.
+ * This event also fires <ProtoChart:mousemove> event, which gets current position of the
+ * mouse as a parameters.
+ */
+ onMouseMove: function(ev) {
+ var e = ev || window.event;
+ if (e.pageX == null && e.clientX != null) {
+ var de = document.documentElement, b = $(document.body);
+ this.lastMousePos.pageX = e.clientX + (de && de.scrollLeft || b.scrollLeft || 0);
+ this.lastMousePos.pageY = e.clientY + (de && de.scrollTop || b.scrollTop || 0);
+ }
+ else {
+ this.lastMousePos.pageX = e.pageX;
+ this.lastMousePos.pageY = e.pageY;
+ }
+
+ var offset = this.overlay.cumulativeOffset();
+ var pos = {
+ x: this.xaxis.min + (e.pageX - offset.left - this.chartOffset.left) / this.hozScale,
+ y: this.yaxis.max - (e.pageY - offset.top - this.chartOffset.top) / this.vertScale
+ };
+
+ if(this.options.mouse.track && this.selectionInterval == null) {
+ this.hit(ev, pos);
+ }
+ this.domObj.fire("ProtoChart:mousemove", [ pos ]);
+ },
+ /**
+ * Function: onMouseDown
+ *
+ * Parameters:
+ * Event - {Object} e
+ *
+ * Description:
+ * Called whenever the mouse is clicked.
+ */
+ onMouseDown: function(e) {
+ if (e.which != 1) // only accept left-click
+ return;
+
+ document.body.focus();
+
+ if (document.onselectstart !== undefined && this.workarounds.onselectstart == null) {
+ this.workarounds.onselectstart = document.onselectstart;
+ document.onselectstart = function () { return false; };
+ }
+ if (document.ondrag !== undefined && this.workarounds.ondrag == null) {
+ this.workarounds.ondrag = document.ondrag;
+ document.ondrag = function () { return false; };
+ }
+
+ this.setSelectionPos(this.selection.first, e);
+
+ if (this.selectionInterval != null)
+ clearInterval(this.selectionInterval);
+ this.lastMousePos.pageX = null;
+ this.selectionInterval = setInterval(this.updateSelectionOnMouseMove.bind(this), 200);
+
+ this.overlay.observe("mouseup", this.onSelectionMouseUp.bind(this));
+ },
+ /**
+ * Function: onClick
+ * parameters:
+ * Event - {Object} e
+ * Description:
+ * Handles the "click" event on the chart. This function fires <ProtoChart:plotclick> event. If
+ * <options.allowDataClick> is enabled then it also fires <ProtoChart:dataclick> event which gives
+ * you access to exact data point where user clicked.
+ */
+ onClick: function(e) {
+ if (this.ignoreClick) {
+ this.ignoreClick = false;
+ return;
+ }
+ var offset = this.overlay.cumulativeOffset();
+ var pos ={
+ x: this.xaxis.min + (e.pageX - offset.left - this.chartOffset.left) / this.hozScale,
+ y: this.yaxis.max - (e.pageY - offset.top - this.chartOffset.top) / this.vertScale
+ };
+ this.domObj.fire("ProtoChart:plotclick", [ pos ]);
+
+ if(this.options.allowDataClick)
+ {
+ var dataPoint = {};
+ if(this.options.points.show)
+ {
+ dataPoint = this.getDataClickPoint(pos, this.options);
+ this.domObj.fire("ProtoChart:dataclick", [dataPoint]);
+ }
+ else if(this.options.lines.show && this.options.points.show)
+ {
+ dataPoint = this.getDataClickPoint(pos, this.options);
+ this.domObj.fire("ProtoChart:dataclick", [dataPoint]);
+ }
+ else if(this.options.bars.show)
+ {
+ if(this.barDataRange.length > 0)
+ {
+ dataPoint = this.getDataClickPoint(pos, this.options, this.barDataRange);
+ this.domObj.fire("ProtoChart:dataclick", [dataPoint]);
+ }
+ }
+ }
+ },
+ /**
+ * Internal function used by onClick method.
+ */
+ getDataClickPoint: function(pos, options, barDataRange)
+ {
+ pos.x = parseInt(pos.x);
+ pos.y = parseInt(pos.y);
+ var yClick = pos.y.toFixed(0);
+ var dataVal = {};
+
+ dataVal.position = pos;
+ dataVal.value = '';
+
+ if(options.points.show)
+ {
+ this.graphData.each(function(gd){
+ var temp = gd.data;
+ var xClick = parseInt(pos.x.toFixed(0));
+ if(xClick < 0) { xClick = 0; }
+ if(temp[xClick] && yClick >= temp[xClick][1] - (this.options.points.radius * 10) && yClick <= temp[xClick][1] + (this.options.points.radius * 10)) {
+ dataVal.value = temp[xClick][1];
+ throw $break;
+ }
+
+ }.bind(this));
+ }
+ else if(options.bars.show)
+ {
+ xClick = pos.x;
+ this.barDataRange.each(function(barData){
+ barData.each(function(data){
+ var temp = data;
+ if(xClick > temp.left && xClick < temp.right) {
+ dataVal.value = temp.value;
+ throw $break;
+ }
+ }.bind(this));
+ }.bind(this));
+
+ }
+
+ return dataVal;
+ },
+ /**
+ * Function: triggerSelectedEvent
+ *
+ * Description:
+ * Internal function called when a selection on the graph is made. This function
+ * fires <ProtoChart:selected> event which has a parameter representing the selection
+ * {
+ * x1: {int}, y1: {int},
+ * x2: {int}, y2: {int}
+ * }
+ */
+ triggerSelectedEvent: function() {
+ var x1, x2, y1, y2;
+ if (this.selection.first.x <= this.selection.second.x) {
+ x1 = this.selection.first.x;
+ x2 = this.selection.second.x;
+ }
+ else {
+ x1 = this.selection.second.x;
+ x2 = this.selection.first.x;
+ }
+
+ if (this.selection.first.y >= this.selection.second.y) {
+ y1 = this.selection.first.y;
+ y2 = this.selection.second.y;
+ }
+ else {
+ y1 = this.selection.second.y;
+ y2 = this.selection.first.y;
+ }
+
+ x1 = this.xaxis.min + x1 / this.hozScale;
+ x2 = this.xaxis.min + x2 / this.hozScale;
+
+ y1 = this.yaxis.max - y1 / this.vertScale;
+ y2 = this.yaxis.max - y2 / this.vertScale;
+
+ this.domObj.fire("ProtoChart:selected", [ { x1: x1, y1: y1, x2: x2, y2: y2 } ]);
+ },
+ /**
+ * Internal function
+ */
+ onSelectionMouseUp: function(e) {
+ if (document.onselectstart !== undefined)
+ document.onselectstart = this.workarounds.onselectstart;
+ if (document.ondrag !== undefined)
+ document.ondrag = this.workarounds.ondrag;
+
+ if (this.selectionInterval != null) {
+ clearInterval(this.selectionInterval);
+ this.selectionInterval = null;
+ }
+
+ this.setSelectionPos(this.selection.second, e);
+ this.clearSelection();
+ if (!this.selectionIsSane() || e.which != 1)
+ return false;
+
+ this.drawSelection();
+ this.triggerSelectedEvent();
+ this.ignoreClick = true;
+
+ return false;
+ },
+ setSelectionPos: function(pos, e) {
+ var offset = $(this.overlay).cumulativeOffset();
+ if (this.options.selection.mode == "y") {
+ if (pos == this.selection.first)
+ pos.x = 0;
+ else
+ pos.x = this.chartWidth;
+ }
+ else {
+ pos.x = e.pageX - offset.left - this.chartOffset.left;
+ pos.x = Math.min(Math.max(0, pos.x), this.chartWidth);
+ }
+
+ if (this.options.selection.mode == "x") {
+ if (pos == this.selection.first)
+ pos.y = 0;
+ else
+ pos.y = this.chartHeight;
+ }
+ else {
+ pos.y = e.pageY - offset.top - this.chartOffset.top;
+ pos.y = Math.min(Math.max(0, pos.y), this.chartHeight);
+ }
+ },
+ updateSelectionOnMouseMove: function() {
+ if (this.lastMousePos.pageX == null)
+ return;
+
+ this.setSelectionPos(this.selection.second, this.lastMousePos);
+ this.clearSelection();
+ if (this.selectionIsSane())
+ this.drawSelection();
+ },
+ clearSelection: function() {
+ if (this.prevSelection == null)
+ return;
+
+ var x = Math.min(this.prevSelection.first.x, this.prevSelection.second.x),
+ y = Math.min(this.prevSelection.first.y, this.prevSelection.second.y),
+ w = Math.abs(this.prevSelection.second.x - this.prevSelection.first.x),
+ h = Math.abs(this.prevSelection.second.y - this.prevSelection.first.y);
+
+ this.overlayContext.clearRect(x + this.chartOffset.left - this.overlayContext.lineWidth,
+ y + this.chartOffset.top - this.overlayContext.lineWidth,
+ w + this.overlayContext.lineWidth*2,
+ h + this.overlayContext.lineWidth*2);
+
+ this.prevSelection = null;
+ },
+ /**
+ * Function: setSelection
+ *
+ * Parameters:
+ * Area - {Object} area represented as a range like: {x1: 3, y1: 3, x2: 4, y2: 8}
+ *
+ * Description:
+ * Sets the current graph selection to the provided range. Calls <drawSelection> and
+ * <triggerSelectedEvent> functions internally.
+ */
+ setSelection: function(area) {
+ this.clearSelection();
+
+ if (this.options.selection.mode == "x") {
+ this.selection.first.y = 0;
+ this.selection.second.y = this.chartHeight;
+ }
+ else {
+ this.selection.first.y = (this.yaxis.max - area.y1) * this.vertScale;
+ this.selection.second.y = (this.yaxis.max - area.y2) * this.vertScale;
+ }
+ if (this.options.selection.mode == "y") {
+ this.selection.first.x = 0;
+ this.selection.second.x = this.chartWidth;
+ }
+ else {
+ this.selection.first.x = (area.x1 - this.xaxis.min) * this.hozScale;
+ this.selection.second.x = (area.x2 - this.xaxis.min) * this.hozScale;
+ }
+
+ this.drawSelection();
+ this.triggerSelectedEvent();
+ },
+ /**
+ * Function: drawSelection
+ * Description: Internal function called to draw the selection made on the graph.
+ */
+ drawSelection: function() {
+ if (this.prevSelection != null &&
+ this.selection.first.x == this.prevSelection.first.x &&
+ this.selection.first.y == this.prevSelection.first.y &&
+ this.selection.second.x == this.prevSelection.second.x &&
+ this.selection.second.y == this.prevSelection.second.y)
+ {
+ return;
+ }
+
+ this.overlayContext.strokeStyle = this.parseColor(this.options.selection.color).scale(null, null, null, 0.8).toString();
+ this.overlayContext.lineWidth = 1;
+ this.context.lineJoin = "round";
+ this.overlayContext.fillStyle = this.parseColor(this.options.selection.color).scale(null, null, null, 0.4).toString();
+
+ this.prevSelection = { first: { x: this.selection.first.x,
+ y: this.selection.first.y },
+ second: { x: this.selection.second.x,
+ y: this.selection.second.y } };
+
+ var x = Math.min(this.selection.first.x, this.selection.second.x),
+ y = Math.min(this.selection.first.y, this.selection.second.y),
+ w = Math.abs(this.selection.second.x - this.selection.first.x),
+ h = Math.abs(this.selection.second.y - this.selection.first.y);
+
+ this.overlayContext.fillRect(x + this.chartOffset.left, y + this.chartOffset.top, w, h);
+ this.overlayContext.strokeRect(x + this.chartOffset.left, y + this.chartOffset.top, w, h);
+ },
+ /**
+ * Internal function
+ */
+ selectionIsSane: function() {
+ var minSize = 5;
+ return Math.abs(this.selection.second.x - this.selection.first.x) >= minSize &&
+ Math.abs(this.selection.second.y - this.selection.first.y) >= minSize;
+ },
+ /**
+ * Internal function that formats the track. This is the format the text is shown when mouse
+ * tracking is enabled.
+ */
+ defaultTrackFormatter: function(val)
+ {
+ return '['+val.x+', '+val.y+']';
+ },
+ /**
+ * Function: clearHit
+ */
+ clearHit: function(){
+ if(this.prevHit){
+ this.overlayContext.clearRect(
+ this.translateHoz(this.prevHit.x) + this.chartOffset.left - this.options.mouse.radius*2,
+ this.translateVert(this.prevHit.y) + this.chartOffset.top - this.options.mouse.radius*2,
+ this.options.mouse.radius*3 + this.options.points.lineWidth*3,
+ this.options.mouse.radius*3 + this.options.points.lineWidth*3
+ );
+ this.prevHit = null;
+ }
+ },
+ /**
+ * Function: hit
+ *
+ * Parameters:
+ * event - {Object} event object
+ * mouse - {Object} mouse object that is used to keep track of mouse movement
+ *
+ * Description:
+ * If hit occurs this function will fire a ProtoChart:hit event.
+ */
+ hit: function(event, mouse){
+ /**
+ * Nearest data element.
+ */
+ var n = {
+ dist:Number.MAX_VALUE,
+ x:null,
+ y:null,
+ mouse:null
+ };
+
+
+ for(var i = 0, data, xsens, ysens; i < this.graphData.length; i++){
+ if(!this.graphData[i].mouse.track) continue;
+ data = this.graphData[i].data;
+ xsens = (this.hozScale*this.graphData[i].mouse.sensibility);
+ ysens = (this.vertScale*this.graphData[i].mouse.sensibility);
+ for(var j = 0, xabs, yabs; j < data.length; j++){
+ xabs = this.hozScale*Math.abs(data[j][0] - mouse.x);
+ yabs = this.vertScale*Math.abs(data[j][1] - mouse.y);
+
+ if(xabs < xsens && yabs < ysens && (xabs+yabs) < n.dist){
+ n.dist = (xabs+yabs);
+ n.x = data[j][0];
+ n.y = data[j][1];
+ n.mouse = this.graphData[i].mouse;
+ }
+ }
+ }
+
+ if(n.mouse && n.mouse.track && !this.prevHit || (this.prevHit && n.x != this.prevHit.x && n.y != this.prevHit.y)){
+ var el = this.domObj.select('.'+this.options.mouse.clsName).first();
+ if(!el){
+ var pos = '', p = this.options.mouse.position, m = this.options.mouse.margin;
+ if(p.charAt(0) == 'n') pos += 'top:' + (m + this.chartOffset.top) + 'px;';
+ else if(p.charAt(0) == 's') pos += 'bottom:' + (m + this.chartOffset.bottom) + 'px;';
+ if(p.charAt(1) == 'e') pos += 'right:' + (m + this.chartOffset.right) + 'px;';
+ else if(p.charAt(1) == 'w') pos += 'left:' + (m + this.chartOffset.bottom) + 'px;';
+
+ this.domObj.insert('<div class="'+this.options.mouse.clsName+'" style="display:none;position:absolute;'+pos+'"></div>');
+ return;
+ }
+ if(n.x !== null && n.y !== null){
+ el.setStyle({display:'block'});
+
+ this.clearHit();
+ if(n.mouse.lineColor != null){
+ this.overlayContext.save();
+ this.overlayContext.translate(this.chartOffset.left, this.chartOffset.top);
+ this.overlayContext.lineWidth = this.options.points.lineWidth;
+ this.overlayContext.strokeStyle = n.mouse.lineColor;
+ this.overlayContext.fillStyle = '#ffffff';
+ this.overlayContext.beginPath();
+
+
+ this.overlayContext.arc(this.translateHoz(n.x), this.translateVert(n.y), this.options.mouse.radius, 0, 2 * Math.PI, true);
+ this.overlayContext.fill();
+ this.overlayContext.stroke();
+ this.overlayContext.restore();
+ }
+ this.prevHit = n;
+
+ var decimals = n.mouse.trackDecimals;
+ if(decimals == null || decimals < 0) decimals = 0;
+ if(!this.options.mouse.fixedPosition)
+ {
+ el.setStyle({
+ left: (this.translateHoz(n.x) + this.options.mouse.radius + 10) + "px",
+ top: (this.translateVert(n.y) + this.options.mouse.radius + 10) + "px"
+ });
+ }
+ el.innerHTML = n.mouse.trackFormatter({x: n.x.toFixed(decimals), y: n.y.toFixed(decimals)});
+ this.domObj.fire( 'ProtoChart:hit', [n] )
+ }else if(this.options.prevHit){
+ el.setStyle({display:'none'});
+ this.clearHit();
+ }
+ }
+ },
+ /**
+ * Internal function
+ */
+ floorInBase: function(n, base) {
+ return base * Math.floor(n / base);
+ },
+ /**
+ * Function: extractColor
+ *
+ * Parameters:
+ * element - HTML element or ID of an HTML element
+ *
+ * Returns:
+ * color in string format
+ */
+ extractColor: function(element)
+ {
+ var color;
+ do
+ {
+ color = $(element).getStyle('background-color').toLowerCase();
+ if(color != '' && color != 'transparent')
+ {
+ break;
+ }
+ element = element.up(0); //or else just get the parent ....
+ } while(element.nodeName.toLowerCase() != 'body');
+
+ //safari fix
+ if(color == 'rgba(0, 0, 0, 0)')
+ return 'transparent';
+ return color;
+ },
+ /**
+ * Function: parseColor
+ *
+ * Parameters:
+ * str - color string in different formats
+ *
+ * Returns:
+ * a Proto.Color Object - use toString() function to retreive the color in rgba/rgb format
+ */
+ parseColor: function(str)
+ {
+ var result;
+
+ /**
+ * rgb(num,num,num)
+ */
+ if((result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)))
+ return new Proto.Color(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]));
+
+ /**
+ * rgba(num,num,num,num)
+ */
+ if((result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)))
+ return new Proto.Color(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]), parseFloat(result[4]));
+
+ /**
+ * rgb(num%,num%,num%)
+ */
+ if((result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)))
+ return new Proto.Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55);
+
+ /**
+ * rgba(num%,num%,num%,num)
+ */
+ if((result = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)))
+ return new Proto.Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4]));
+
+ /**
+ * #a0b1c2
+ */
+ if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)))
+ return new Proto.Color(parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16));
+
+ /**
+ * #fff
+ */
+ if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)))
+ return new Proto.Color(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16));
+
+ /**
+ * Otherwise, check if user wants transparent .. or we just return a standard color;
+ */
+ var name = str.strip().toLowerCase();
+ if(name == 'transparent'){
+ return new Proto.Color(255, 255, 255, 0);
+ }
+
+ return new Proto.Color(100,100,100, 1);
+
+ }
+});
+
+if(!Proto) var Proto = {};
+
+/**
+ * Class: Proto.Color
+ *
+ * Helper class that manipulates colors using RGBA values.
+ *
+ */
+
+Proto.Color = Class.create({
+ initialize: function(r, g, b, a) {
+ this.rgba = ['r', 'g', 'b', 'a'];
+ var x = 4;
+ while(-1<--x) {
+ this[this.rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0);
+ }
+ },
+ toString: function() {
+ if(this.a >= 1.0) {
+ return "rgb(" + [this.r, this.g, this.b].join(",") +")";
+ }
+ else {
+ return "rgba("+[this.r, this.g, this.b, this.a].join(",")+")";
+ }
+ },
+ scale: function(rf, gf, bf, af) {
+ x = 4;
+ while(-1<--x) {
+ if(arguments[x] != null) {
+ this[this.rgba[x]] *= arguments[x];
+ }
+ }
+ return this.normalize();
+ },
+ adjust: function(rd, gd, bd, ad) {
+ x = 4; //rgba.length
+ while (-1<--x) {
+ if (arguments[x] != null)
+ this[this.rgba[x]] += arguments[x];
+ }
+ return this.normalize();
+ },
+ clone: function() {
+ return new Proto.Color(this.r, this.b, this.g, this.a);
+ },
+ limit: function(val,minVal,maxVal) {
+ return Math.max(Math.min(val, maxVal), minVal);
+ },
+ normalize: function() {
+ this.r = this.limit(parseInt(this.r), 0, 255);
+ this.g = this.limit(parseInt(this.g), 0, 255);
+ this.b = this.limit(parseInt(this.b), 0, 255);
+ this.a = this.limit(this.a, 0, 1);
+ return this;
+ }
+}); \ No newline at end of file
diff --git a/usr/local/www/protochart/excanvas-compressed.js b/usr/local/www/protochart/excanvas-compressed.js
new file mode 100644
index 0000000..9d71658
--- /dev/null
+++ b/usr/local/www/protochart/excanvas-compressed.js
@@ -0,0 +1,19 @@
+if(!window.CanvasRenderingContext2D){(function(){var I=Math,i=I.round,L=I.sin,M=I.cos,m=10,A=m/2,Q={init:function(a){var b=a||document;if(/MSIE/.test(navigator.userAgent)&&!window.opera){var c=this;b.attachEvent("onreadystatechange",function(){c.r(b)})}},r:function(a){if(a.readyState=="complete"){if(!a.namespaces["s"]){a.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml")}var b=a.createStyleSheet();b.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}g_vml_\\:*{behavior:url(#default#VML)}";
+var c=a.getElementsByTagName("canvas");for(var d=0;d<c.length;d++){if(!c[d].getContext){this.initElement(c[d])}}}},q:function(a){var b=a.outerHTML,c=a.ownerDocument.createElement(b);if(b.slice(-2)!="/>"){var d="/"+a.tagName,e;while((e=a.nextSibling)&&e.tagName!=d){e.removeNode()}if(e){e.removeNode()}}a.parentNode.replaceChild(c,a);return c},initElement:function(a){a=this.q(a);a.getContext=function(){if(this.l){return this.l}return this.l=new K(this)};a.attachEvent("onpropertychange",V);a.attachEvent("onresize",
+W);var b=a.attributes;if(b.width&&b.width.specified){a.style.width=b.width.nodeValue+"px"}else{a.width=a.clientWidth}if(b.height&&b.height.specified){a.style.height=b.height.nodeValue+"px"}else{a.height=a.clientHeight}return a}};function V(a){var b=a.srcElement;switch(a.propertyName){case "width":b.style.width=b.attributes.width.nodeValue+"px";b.getContext().clearRect();break;case "height":b.style.height=b.attributes.height.nodeValue+"px";b.getContext().clearRect();break}}function W(a){var b=a.srcElement;
+if(b.firstChild){b.firstChild.style.width=b.clientWidth+"px";b.firstChild.style.height=b.clientHeight+"px"}}Q.init();var R=[];for(var E=0;E<16;E++){for(var F=0;F<16;F++){R[E*16+F]=E.toString(16)+F.toString(16)}}function J(){return[[1,0,0],[0,1,0],[0,0,1]]}function G(a,b){var c=J();for(var d=0;d<3;d++){for(var e=0;e<3;e++){var g=0;for(var h=0;h<3;h++){g+=a[d][h]*b[h][e]}c[d][e]=g}}return c}function N(a,b){b.fillStyle=a.fillStyle;b.lineCap=a.lineCap;b.lineJoin=a.lineJoin;b.lineWidth=a.lineWidth;b.miterLimit=
+a.miterLimit;b.shadowBlur=a.shadowBlur;b.shadowColor=a.shadowColor;b.shadowOffsetX=a.shadowOffsetX;b.shadowOffsetY=a.shadowOffsetY;b.strokeStyle=a.strokeStyle;b.d=a.d;b.e=a.e}function O(a){var b,c=1;a=String(a);if(a.substring(0,3)=="rgb"){var d=a.indexOf("(",3),e=a.indexOf(")",d+1),g=a.substring(d+1,e).split(",");b="#";for(var h=0;h<3;h++){b+=R[Number(g[h])]}if(g.length==4&&a.substr(3,1)=="a"){c=g[3]}}else{b=a}return[b,c]}function S(a){switch(a){case "butt":return"flat";case "round":return"round";
+case "square":default:return"square"}}function K(a){this.a=J();this.m=[];this.k=[];this.c=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=m*1;this.globalAlpha=1;this.canvas=a;var b=a.ownerDocument.createElement("div");b.style.width=a.clientWidth+"px";b.style.height=a.clientHeight+"px";b.style.overflow="hidden";b.style.position="absolute";a.appendChild(b);this.j=b;this.d=1;this.e=1}var j=K.prototype;j.clearRect=function(){this.j.innerHTML=
+"";this.c=[]};j.beginPath=function(){this.c=[]};j.moveTo=function(a,b){this.c.push({type:"moveTo",x:a,y:b});this.f=a;this.g=b};j.lineTo=function(a,b){this.c.push({type:"lineTo",x:a,y:b});this.f=a;this.g=b};j.bezierCurveTo=function(a,b,c,d,e,g){this.c.push({type:"bezierCurveTo",cp1x:a,cp1y:b,cp2x:c,cp2y:d,x:e,y:g});this.f=e;this.g=g};j.quadraticCurveTo=function(a,b,c,d){var e=this.f+0.6666666666666666*(a-this.f),g=this.g+0.6666666666666666*(b-this.g),h=e+(c-this.f)/3,l=g+(d-this.g)/3;this.bezierCurveTo(e,
+g,h,l,c,d)};j.arc=function(a,b,c,d,e,g){c*=m;var h=g?"at":"wa",l=a+M(d)*c-A,n=b+L(d)*c-A,o=a+M(e)*c-A,f=b+L(e)*c-A;if(l==o&&!g){l+=0.125}this.c.push({type:h,x:a,y:b,radius:c,xStart:l,yStart:n,xEnd:o,yEnd:f})};j.rect=function(a,b,c,d){this.moveTo(a,b);this.lineTo(a+c,b);this.lineTo(a+c,b+d);this.lineTo(a,b+d);this.closePath()};j.strokeRect=function(a,b,c,d){this.beginPath();this.moveTo(a,b);this.lineTo(a+c,b);this.lineTo(a+c,b+d);this.lineTo(a,b+d);this.closePath();this.stroke()};j.fillRect=function(a,
+b,c,d){this.beginPath();this.moveTo(a,b);this.lineTo(a+c,b);this.lineTo(a+c,b+d);this.lineTo(a,b+d);this.closePath();this.fill()};j.createLinearGradient=function(a,b,c,d){var e=new H("gradient");return e};j.createRadialGradient=function(a,b,c,d,e,g){var h=new H("gradientradial");h.n=c;h.o=g;h.i.x=a;h.i.y=b;return h};j.drawImage=function(a,b){var c,d,e,g,h,l,n,o,f=a.runtimeStyle.width,k=a.runtimeStyle.height;a.runtimeStyle.width="auto";a.runtimeStyle.height="auto";var q=a.width,r=a.height;a.runtimeStyle.width=
+f;a.runtimeStyle.height=k;if(arguments.length==3){c=arguments[1];d=arguments[2];h=(l=0);n=(e=q);o=(g=r)}else if(arguments.length==5){c=arguments[1];d=arguments[2];e=arguments[3];g=arguments[4];h=(l=0);n=q;o=r}else if(arguments.length==9){h=arguments[1];l=arguments[2];n=arguments[3];o=arguments[4];c=arguments[5];d=arguments[6];e=arguments[7];g=arguments[8]}else{throw"Invalid number of arguments";}var s=this.b(c,d),t=[],v=10,w=10;t.push(" <g_vml_:group",' coordsize="',m*v,",",m*w,'"',' coordorigin="0,0"',
+' style="width:',v,";height:",w,";position:absolute;");if(this.a[0][0]!=1||this.a[0][1]){var x=[];x.push("M11='",this.a[0][0],"',","M12='",this.a[1][0],"',","M21='",this.a[0][1],"',","M22='",this.a[1][1],"',","Dx='",i(s.x/m),"',","Dy='",i(s.y/m),"'");var p=s,y=this.b(c+e,d),z=this.b(c,d+g),B=this.b(c+e,d+g);p.x=Math.max(p.x,y.x,z.x,B.x);p.y=Math.max(p.y,y.y,z.y,B.y);t.push("padding:0 ",i(p.x/m),"px ",i(p.y/m),"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",x.join(""),", sizingmethod='clip');")}else{t.push("top:",
+i(s.y/m),"px;left:",i(s.x/m),"px;")}t.push(' ">','<g_vml_:image src="',a.src,'"',' style="width:',m*e,";"," height:",m*g,';"',' cropleft="',h/q,'"',' croptop="',l/r,'"',' cropright="',(q-h-n)/q,'"',' cropbottom="',(r-l-o)/r,'"'," />","</g_vml_:group>");this.j.insertAdjacentHTML("BeforeEnd",t.join(""))};j.stroke=function(a){var b=[],c=O(a?this.fillStyle:this.strokeStyle),d=c[0],e=c[1]*this.globalAlpha,g=10,h=10;b.push("<g_vml_:shape",' fillcolor="',d,'"',' filled="',Boolean(a),'"',' style="position:absolute;width:',
+g,";height:",h,';"',' coordorigin="0 0" coordsize="',m*g," ",m*h,'"',' stroked="',!a,'"',' strokeweight="',this.lineWidth,'"',' strokecolor="',d,'"',' path="');var l={x:null,y:null},n={x:null,y:null};for(var o=0;o<this.c.length;o++){var f=this.c[o];if(f.type=="moveTo"){b.push(" m ");var k=this.b(f.x,f.y);b.push(i(k.x),",",i(k.y))}else if(f.type=="lineTo"){b.push(" l ");var k=this.b(f.x,f.y);b.push(i(k.x),",",i(k.y))}else if(f.type=="close"){b.push(" x ")}else if(f.type=="bezierCurveTo"){b.push(" c ");
+var k=this.b(f.x,f.y),q=this.b(f.cp1x,f.cp1y),r=this.b(f.cp2x,f.cp2y);b.push(i(q.x),",",i(q.y),",",i(r.x),",",i(r.y),",",i(k.x),",",i(k.y))}else if(f.type=="at"||f.type=="wa"){b.push(" ",f.type," ");var k=this.b(f.x,f.y),s=this.b(f.xStart,f.yStart),t=this.b(f.xEnd,f.yEnd);b.push(i(k.x-this.d*f.radius),",",i(k.y-this.e*f.radius)," ",i(k.x+this.d*f.radius),",",i(k.y+this.e*f.radius)," ",i(s.x),",",i(s.y)," ",i(t.x),",",i(t.y))}if(k){if(l.x==null||k.x<l.x){l.x=k.x}if(n.x==null||k.x>n.x){n.x=k.x}if(l.y==
+null||k.y<l.y){l.y=k.y}if(n.y==null||k.y>n.y){n.y=k.y}}}b.push(' ">');if(typeof this.fillStyle=="object"){var v={x:"50%",y:"50%"},w=n.x-l.x,x=n.y-l.y,p=w>x?w:x;v.x=i(this.fillStyle.i.x/w*100+50)+"%";v.y=i(this.fillStyle.i.y/x*100+50)+"%";var y=[];if(this.fillStyle.p=="gradientradial"){var z=this.fillStyle.n/p*100,B=this.fillStyle.o/p*100-z}else{var z=0,B=100}var C={offset:null,color:null},D={offset:null,color:null};this.fillStyle.h.sort(function(T,U){return T.offset-U.offset});for(var o=0;o<this.fillStyle.h.length;o++){var u=
+this.fillStyle.h[o];y.push(u.offset*B+z,"% ",u.color,",");if(u.offset>C.offset||C.offset==null){C.offset=u.offset;C.color=u.color}if(u.offset<D.offset||D.offset==null){D.offset=u.offset;D.color=u.color}}y.pop();b.push("<g_vml_:fill",' color="',D.color,'"',' color2="',C.color,'"',' type="',this.fillStyle.p,'"',' focusposition="',v.x,", ",v.y,'"',' colors="',y.join(""),'"',' opacity="',e,'" />')}else if(a){b.push('<g_vml_:fill color="',d,'" opacity="',e,'" />')}else{b.push("<g_vml_:stroke",' opacity="',
+e,'"',' joinstyle="',this.lineJoin,'"',' miterlimit="',this.miterLimit,'"',' endcap="',S(this.lineCap),'"',' weight="',this.lineWidth,'px"',' color="',d,'" />')}b.push("</g_vml_:shape>");this.j.insertAdjacentHTML("beforeEnd",b.join(""));this.c=[]};j.fill=function(){this.stroke(true)};j.closePath=function(){this.c.push({type:"close"})};j.b=function(a,b){return{x:m*(a*this.a[0][0]+b*this.a[1][0]+this.a[2][0])-A,y:m*(a*this.a[0][1]+b*this.a[1][1]+this.a[2][1])-A}};j.save=function(){var a={};N(this,a);
+this.k.push(a);this.m.push(this.a);this.a=G(J(),this.a)};j.restore=function(){N(this.k.pop(),this);this.a=this.m.pop()};j.translate=function(a,b){var c=[[1,0,0],[0,1,0],[a,b,1]];this.a=G(c,this.a)};j.rotate=function(a){var b=M(a),c=L(a),d=[[b,c,0],[-c,b,0],[0,0,1]];this.a=G(d,this.a)};j.scale=function(a,b){this.d*=a;this.e*=b;var c=[[a,0,0],[0,b,0],[0,0,1]];this.a=G(c,this.a)};j.clip=function(){};j.arcTo=function(){};j.createPattern=function(){return new P};function H(a){this.p=a;this.n=0;this.o=
+0;this.h=[];this.i={x:0,y:0}}H.prototype.addColorStop=function(a,b){b=O(b);this.h.push({offset:1-a,color:b})};function P(){}G_vmlCanvasManager=Q;CanvasRenderingContext2D=K;CanvasGradient=H;CanvasPattern=P})()};
diff --git a/usr/local/www/protochart/excanvas.js b/usr/local/www/protochart/excanvas.js
new file mode 100644
index 0000000..7914cb9
--- /dev/null
+++ b/usr/local/www/protochart/excanvas.js
@@ -0,0 +1,785 @@
+// Copyright 2006 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+// Known Issues:
+//
+// * Patterns are not implemented.
+// * Radial gradient are not implemented. The VML version of these look very
+// different from the canvas one.
+// * Clipping paths are not implemented.
+// * Coordsize. The width and height attribute have higher priority than the
+// width and height style values which isn't correct.
+// * Painting mode isn't implemented.
+// * Canvas width/height should is using content-box by default. IE in
+// Quirks mode will draw the canvas using border-box. Either change your
+// doctype to HTML5
+// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
+// or use Box Sizing Behavior from WebFX
+// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
+// * Optimize. There is always room for speed improvements.
+
+// only add this code if we do not already have a canvas implementation
+if (!window.CanvasRenderingContext2D) {
+
+(function () {
+
+ // alias some functions to make (compiled) code shorter
+ var m = Math;
+ var mr = m.round;
+ var ms = m.sin;
+ var mc = m.cos;
+
+ // this is used for sub pixel precision
+ var Z = 10;
+ var Z2 = Z / 2;
+
+ var G_vmlCanvasManager_ = {
+ init: function (opt_doc) {
+ var doc = opt_doc || document;
+ if (/MSIE/.test(navigator.userAgent) && !window.opera) {
+ var self = this;
+ doc.attachEvent("onreadystatechange", function () {
+ self.init_(doc);
+ });
+ }
+ },
+
+ init_: function (doc) {
+ if (doc.readyState == "complete") {
+ // create xmlns
+ if (!doc.namespaces["g_vml_"]) {
+ doc.namespaces.add("g_vml_", "urn:schemas-microsoft-com:vml");
+ }
+
+ // setup default css
+ var ss = doc.createStyleSheet();
+ ss.cssText = "canvas{display:inline-block;overflow:hidden;" +
+ // default size is 300x150 in Gecko and Opera
+ "text-align:left;width:300px;height:150px}" +
+ "g_vml_\\:*{behavior:url(#default#VML)}";
+
+ // find all canvas elements
+ var els = doc.getElementsByTagName("canvas");
+ for (var i = 0; i < els.length; i++) {
+ if (!els[i].getContext) {
+ this.initElement(els[i]);
+ }
+ }
+ }
+ },
+
+ fixElement_: function (el) {
+ // in IE before version 5.5 we would need to add HTML: to the tag name
+ // but we do not care about IE before version 6
+ var outerHTML = el.outerHTML;
+
+ var newEl = el.ownerDocument.createElement(outerHTML);
+ // if the tag is still open IE has created the children as siblings and
+ // it has also created a tag with the name "/FOO"
+ if (outerHTML.slice(-2) != "/>") {
+ var tagName = "/" + el.tagName;
+ var ns;
+ // remove content
+ while ((ns = el.nextSibling) && ns.tagName != tagName) {
+ ns.removeNode();
+ }
+ // remove the incorrect closing tag
+ if (ns) {
+ ns.removeNode();
+ }
+ }
+ el.parentNode.replaceChild(newEl, el);
+ return newEl;
+ },
+
+ /**
+ * Public initializes a canvas element so that it can be used as canvas
+ * element from now on. This is called automatically before the page is
+ * loaded but if you are creating elements using createElement you need to
+ * make sure this is called on the element.
+ * @param {HTMLElement} el The canvas element to initialize.
+ * @return {HTMLElement} the element that was created.
+ */
+ initElement: function (el) {
+ el = this.fixElement_(el);
+ el.getContext = function () {
+ if (this.context_) {
+ return this.context_;
+ }
+ return this.context_ = new CanvasRenderingContext2D_(this);
+ };
+
+ // do not use inline function because that will leak memory
+ el.attachEvent('onpropertychange', onPropertyChange);
+ el.attachEvent('onresize', onResize);
+
+ var attrs = el.attributes;
+ if (attrs.width && attrs.width.specified) {
+ // TODO: use runtimeStyle and coordsize
+ // el.getContext().setWidth_(attrs.width.nodeValue);
+ el.style.width = attrs.width.nodeValue + "px";
+ } else {
+ el.width = el.clientWidth;
+ }
+ if (attrs.height && attrs.height.specified) {
+ // TODO: use runtimeStyle and coordsize
+ // el.getContext().setHeight_(attrs.height.nodeValue);
+ el.style.height = attrs.height.nodeValue + "px";
+ } else {
+ el.height = el.clientHeight;
+ }
+ //el.getContext().setCoordsize_()
+ return el;
+ }
+ };
+
+ function onPropertyChange(e) {
+ var el = e.srcElement;
+
+ switch (e.propertyName) {
+ case 'width':
+ el.style.width = el.attributes.width.nodeValue + "px";
+ el.getContext().clearRect();
+ break;
+ case 'height':
+ el.style.height = el.attributes.height.nodeValue + "px";
+ el.getContext().clearRect();
+ break;
+ }
+ }
+
+ function onResize(e) {
+ var el = e.srcElement;
+ if (el.firstChild) {
+ el.firstChild.style.width = el.clientWidth + 'px';
+ el.firstChild.style.height = el.clientHeight + 'px';
+ }
+ }
+
+ G_vmlCanvasManager_.init();
+
+ // precompute "00" to "FF"
+ var dec2hex = [];
+ for (var i = 0; i < 16; i++) {
+ for (var j = 0; j < 16; j++) {
+ dec2hex[i * 16 + j] = i.toString(16) + j.toString(16);
+ }
+ }
+
+ function createMatrixIdentity() {
+ return [
+ [1, 0, 0],
+ [0, 1, 0],
+ [0, 0, 1]
+ ];
+ }
+
+ function matrixMultiply(m1, m2) {
+ var result = createMatrixIdentity();
+
+ for (var x = 0; x < 3; x++) {
+ for (var y = 0; y < 3; y++) {
+ var sum = 0;
+
+ for (var z = 0; z < 3; z++) {
+ sum += m1[x][z] * m2[z][y];
+ }
+
+ result[x][y] = sum;
+ }
+ }
+ return result;
+ }
+
+ function copyState(o1, o2) {
+ o2.fillStyle = o1.fillStyle;
+ o2.lineCap = o1.lineCap;
+ o2.lineJoin = o1.lineJoin;
+ o2.lineWidth = o1.lineWidth;
+ o2.miterLimit = o1.miterLimit;
+ o2.shadowBlur = o1.shadowBlur;
+ o2.shadowColor = o1.shadowColor;
+ o2.shadowOffsetX = o1.shadowOffsetX;
+ o2.shadowOffsetY = o1.shadowOffsetY;
+ o2.strokeStyle = o1.strokeStyle;
+ o2.arcScaleX_ = o1.arcScaleX_;
+ o2.arcScaleY_ = o1.arcScaleY_;
+ }
+
+ function processStyle(styleString) {
+ var str, alpha = 1;
+
+ styleString = String(styleString);
+ if (styleString.substring(0, 3) == "rgb") {
+ var start = styleString.indexOf("(", 3);
+ var end = styleString.indexOf(")", start + 1);
+ var guts = styleString.substring(start + 1, end).split(",");
+
+ str = "#";
+ for (var i = 0; i < 3; i++) {
+ str += dec2hex[Number(guts[i])];
+ }
+
+ if ((guts.length == 4) && (styleString.substr(3, 1) == "a")) {
+ alpha = guts[3];
+ }
+ } else {
+ str = styleString;
+ }
+
+ return [str, alpha];
+ }
+
+ function processLineCap(lineCap) {
+ switch (lineCap) {
+ case "butt":
+ return "flat";
+ case "round":
+ return "round";
+ case "square":
+ default:
+ return "square";
+ }
+ }
+
+ /**
+ * This class implements CanvasRenderingContext2D interface as described by
+ * the WHATWG.
+ * @param {HTMLElement} surfaceElement The element that the 2D context should
+ * be associated with
+ */
+ function CanvasRenderingContext2D_(surfaceElement) {
+ this.m_ = createMatrixIdentity();
+
+ this.mStack_ = [];
+ this.aStack_ = [];
+ this.currentPath_ = [];
+
+ // Canvas context properties
+ this.strokeStyle = "#000";
+ this.fillStyle = "#000";
+
+ this.lineWidth = 1;
+ this.lineJoin = "miter";
+ this.lineCap = "butt";
+ this.miterLimit = Z * 1;
+ this.globalAlpha = 1;
+ this.canvas = surfaceElement;
+
+ var el = surfaceElement.ownerDocument.createElement('div');
+ el.style.width = surfaceElement.clientWidth + 'px';
+ el.style.height = surfaceElement.clientHeight + 'px';
+ el.style.overflow = 'hidden';
+ el.style.position = 'absolute';
+ surfaceElement.appendChild(el);
+
+ this.element_ = el;
+ this.arcScaleX_ = 1;
+ this.arcScaleY_ = 1;
+ }
+
+ var contextPrototype = CanvasRenderingContext2D_.prototype;
+ contextPrototype.clearRect = function() {
+ this.element_.innerHTML = "";
+ this.currentPath_ = [];
+ };
+
+ contextPrototype.beginPath = function() {
+ // TODO: Branch current matrix so that save/restore has no effect
+ // as per safari docs.
+
+ this.currentPath_ = [];
+ };
+
+ contextPrototype.moveTo = function(aX, aY) {
+ this.currentPath_.push({type: "moveTo", x: aX, y: aY});
+ this.currentX_ = aX;
+ this.currentY_ = aY;
+ };
+
+ contextPrototype.lineTo = function(aX, aY) {
+ this.currentPath_.push({type: "lineTo", x: aX, y: aY});
+ this.currentX_ = aX;
+ this.currentY_ = aY;
+ };
+
+ contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,
+ aCP2x, aCP2y,
+ aX, aY) {
+ this.currentPath_.push({type: "bezierCurveTo",
+ cp1x: aCP1x,
+ cp1y: aCP1y,
+ cp2x: aCP2x,
+ cp2y: aCP2y,
+ x: aX,
+ y: aY});
+ this.currentX_ = aX;
+ this.currentY_ = aY;
+ };
+
+ contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
+ // the following is lifted almost directly from
+ // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes
+ var cp1x = this.currentX_ + 2.0 / 3.0 * (aCPx - this.currentX_);
+ var cp1y = this.currentY_ + 2.0 / 3.0 * (aCPy - this.currentY_);
+ var cp2x = cp1x + (aX - this.currentX_) / 3.0;
+ var cp2y = cp1y + (aY - this.currentY_) / 3.0;
+ this.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, aX, aY);
+ };
+
+ contextPrototype.arc = function(aX, aY, aRadius,
+ aStartAngle, aEndAngle, aClockwise) {
+ aRadius *= Z;
+ var arcType = aClockwise ? "at" : "wa";
+
+ var xStart = aX + (mc(aStartAngle) * aRadius) - Z2;
+ var yStart = aY + (ms(aStartAngle) * aRadius) - Z2;
+
+ var xEnd = aX + (mc(aEndAngle) * aRadius) - Z2;
+ var yEnd = aY + (ms(aEndAngle) * aRadius) - Z2;
+
+ // IE won't render arches drawn counter clockwise if xStart == xEnd.
+ if (xStart == xEnd && !aClockwise) {
+ xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something
+ // that can be represented in binary
+ }
+
+ this.currentPath_.push({type: arcType,
+ x: aX,
+ y: aY,
+ radius: aRadius,
+ xStart: xStart,
+ yStart: yStart,
+ xEnd: xEnd,
+ yEnd: yEnd});
+
+ };
+
+ contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
+ this.moveTo(aX, aY);
+ this.lineTo(aX + aWidth, aY);
+ this.lineTo(aX + aWidth, aY + aHeight);
+ this.lineTo(aX, aY + aHeight);
+ this.closePath();
+ };
+
+ contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
+ // Will destroy any existing path (same as FF behaviour)
+ this.beginPath();
+ this.moveTo(aX, aY);
+ this.lineTo(aX + aWidth, aY);
+ this.lineTo(aX + aWidth, aY + aHeight);
+ this.lineTo(aX, aY + aHeight);
+ this.closePath();
+ this.stroke();
+ };
+
+ contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
+ // Will destroy any existing path (same as FF behaviour)
+ this.beginPath();
+ this.moveTo(aX, aY);
+ this.lineTo(aX + aWidth, aY);
+ this.lineTo(aX + aWidth, aY + aHeight);
+ this.lineTo(aX, aY + aHeight);
+ this.closePath();
+ this.fill();
+ };
+
+ contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
+ var gradient = new CanvasGradient_("gradient");
+ return gradient;
+ };
+
+ contextPrototype.createRadialGradient = function(aX0, aY0,
+ aR0, aX1,
+ aY1, aR1) {
+ var gradient = new CanvasGradient_("gradientradial");
+ gradient.radius1_ = aR0;
+ gradient.radius2_ = aR1;
+ gradient.focus_.x = aX0;
+ gradient.focus_.y = aY0;
+ return gradient;
+ };
+
+ contextPrototype.drawImage = function (image, var_args) {
+ var dx, dy, dw, dh, sx, sy, sw, sh;
+
+ // to find the original width we overide the width and height
+ var oldRuntimeWidth = image.runtimeStyle.width;
+ var oldRuntimeHeight = image.runtimeStyle.height;
+ image.runtimeStyle.width = 'auto';
+ image.runtimeStyle.height = 'auto';
+
+ // get the original size
+ var w = image.width;
+ var h = image.height;
+
+ // and remove overides
+ image.runtimeStyle.width = oldRuntimeWidth;
+ image.runtimeStyle.height = oldRuntimeHeight;
+
+ if (arguments.length == 3) {
+ dx = arguments[1];
+ dy = arguments[2];
+ sx = sy = 0;
+ sw = dw = w;
+ sh = dh = h;
+ } else if (arguments.length == 5) {
+ dx = arguments[1];
+ dy = arguments[2];
+ dw = arguments[3];
+ dh = arguments[4];
+ sx = sy = 0;
+ sw = w;
+ sh = h;
+ } else if (arguments.length == 9) {
+ sx = arguments[1];
+ sy = arguments[2];
+ sw = arguments[3];
+ sh = arguments[4];
+ dx = arguments[5];
+ dy = arguments[6];
+ dw = arguments[7];
+ dh = arguments[8];
+ } else {
+ throw "Invalid number of arguments";
+ }
+
+ var d = this.getCoords_(dx, dy);
+
+ var w2 = sw / 2;
+ var h2 = sh / 2;
+
+ var vmlStr = [];
+
+ var W = 10;
+ var H = 10;
+
+ // For some reason that I've now forgotten, using divs didn't work
+ vmlStr.push(' <g_vml_:group',
+ ' coordsize="', Z * W, ',', Z * H, '"',
+ ' coordorigin="0,0"' ,
+ ' style="width:', W, ';height:', H, ';position:absolute;');
+
+ // If filters are necessary (rotation exists), create them
+ // filters are bog-slow, so only create them if abbsolutely necessary
+ // The following check doesn't account for skews (which don't exist
+ // in the canvas spec (yet) anyway.
+
+ if (this.m_[0][0] != 1 || this.m_[0][1]) {
+ var filter = [];
+
+ // Note the 12/21 reversal
+ filter.push("M11='", this.m_[0][0], "',",
+ "M12='", this.m_[1][0], "',",
+ "M21='", this.m_[0][1], "',",
+ "M22='", this.m_[1][1], "',",
+ "Dx='", mr(d.x / Z), "',",
+ "Dy='", mr(d.y / Z), "'");
+
+ // Bounding box calculation (need to minimize displayed area so that
+ // filters don't waste time on unused pixels.
+ var max = d;
+ var c2 = this.getCoords_(dx + dw, dy);
+ var c3 = this.getCoords_(dx, dy + dh);
+ var c4 = this.getCoords_(dx + dw, dy + dh);
+
+ max.x = Math.max(max.x, c2.x, c3.x, c4.x);
+ max.y = Math.max(max.y, c2.y, c3.y, c4.y);
+
+ vmlStr.push("padding:0 ", mr(max.x / Z), "px ", mr(max.y / Z),
+ "px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",
+ filter.join(""), ", sizingmethod='clip');");
+ } else {
+ vmlStr.push("top:", mr(d.y / Z), "px;left:", mr(d.x / Z), "px;");
+ }
+
+ vmlStr.push(' ">' ,
+ '<g_vml_:image src="', image.src, '"',
+ ' style="width:', Z * dw, ';',
+ ' height:', Z * dh, ';"',
+ ' cropleft="', sx / w, '"',
+ ' croptop="', sy / h, '"',
+ ' cropright="', (w - sx - sw) / w, '"',
+ ' cropbottom="', (h - sy - sh) / h, '"',
+ ' />',
+ '</g_vml_:group>');
+
+ this.element_.insertAdjacentHTML("BeforeEnd",
+ vmlStr.join(""));
+ };
+
+ contextPrototype.stroke = function(aFill) {
+ var lineStr = [];
+ var lineOpen = false;
+ var a = processStyle(aFill ? this.fillStyle : this.strokeStyle);
+ var color = a[0];
+ var opacity = a[1] * this.globalAlpha;
+
+ var W = 10;
+ var H = 10;
+
+ lineStr.push('<g_vml_:shape',
+ ' fillcolor="', color, '"',
+ ' filled="', Boolean(aFill), '"',
+ ' style="position:absolute;width:', W, ';height:', H, ';"',
+ ' coordorigin="0 0" coordsize="', Z * W, ' ', Z * H, '"',
+ ' stroked="', !aFill, '"',
+ ' strokeweight="', this.lineWidth, '"',
+ ' strokecolor="', color, '"',
+ ' path="');
+
+ var newSeq = false;
+ var min = {x: null, y: null};
+ var max = {x: null, y: null};
+
+ for (var i = 0; i < this.currentPath_.length; i++) {
+ var p = this.currentPath_[i];
+
+ if (p.type == "moveTo") {
+ lineStr.push(" m ");
+ var c = this.getCoords_(p.x, p.y);
+ lineStr.push(mr(c.x), ",", mr(c.y));
+ } else if (p.type == "lineTo") {
+ lineStr.push(" l ");
+ var c = this.getCoords_(p.x, p.y);
+ lineStr.push(mr(c.x), ",", mr(c.y));
+ } else if (p.type == "close") {
+ lineStr.push(" x ");
+ } else if (p.type == "bezierCurveTo") {
+ lineStr.push(" c ");
+ var c = this.getCoords_(p.x, p.y);
+ var c1 = this.getCoords_(p.cp1x, p.cp1y);
+ var c2 = this.getCoords_(p.cp2x, p.cp2y);
+ lineStr.push(mr(c1.x), ",", mr(c1.y), ",",
+ mr(c2.x), ",", mr(c2.y), ",",
+ mr(c.x), ",", mr(c.y));
+ } else if (p.type == "at" || p.type == "wa") {
+ lineStr.push(" ", p.type, " ");
+ var c = this.getCoords_(p.x, p.y);
+ var cStart = this.getCoords_(p.xStart, p.yStart);
+ var cEnd = this.getCoords_(p.xEnd, p.yEnd);
+
+ lineStr.push(mr(c.x - this.arcScaleX_ * p.radius), ",",
+ mr(c.y - this.arcScaleY_ * p.radius), " ",
+ mr(c.x + this.arcScaleX_ * p.radius), ",",
+ mr(c.y + this.arcScaleY_ * p.radius), " ",
+ mr(cStart.x), ",", mr(cStart.y), " ",
+ mr(cEnd.x), ",", mr(cEnd.y));
+ }
+
+
+ // TODO: Following is broken for curves due to
+ // move to proper paths.
+
+ // Figure out dimensions so we can do gradient fills
+ // properly
+ if(c) {
+ if (min.x == null || c.x < min.x) {
+ min.x = c.x;
+ }
+ if (max.x == null || c.x > max.x) {
+ max.x = c.x;
+ }
+ if (min.y == null || c.y < min.y) {
+ min.y = c.y;
+ }
+ if (max.y == null || c.y > max.y) {
+ max.y = c.y;
+ }
+ }
+ }
+ lineStr.push(' ">');
+
+ if (typeof this.fillStyle == "object") {
+ var focus = {x: "50%", y: "50%"};
+ var width = (max.x - min.x);
+ var height = (max.y - min.y);
+ var dimension = (width > height) ? width : height;
+
+ focus.x = mr((this.fillStyle.focus_.x / width) * 100 + 50) + "%";
+ focus.y = mr((this.fillStyle.focus_.y / height) * 100 + 50) + "%";
+
+ var colors = [];
+
+ // inside radius (%)
+ if (this.fillStyle.type_ == "gradientradial") {
+ var inside = (this.fillStyle.radius1_ / dimension * 100);
+
+ // percentage that outside radius exceeds inside radius
+ var expansion = (this.fillStyle.radius2_ / dimension * 100) - inside;
+ } else {
+ var inside = 0;
+ var expansion = 100;
+ }
+
+ var insidecolor = {offset: null, color: null};
+ var outsidecolor = {offset: null, color: null};
+
+ // We need to sort 'colors' by percentage, from 0 > 100 otherwise ie
+ // won't interpret it correctly
+ this.fillStyle.colors_.sort(function (cs1, cs2) {
+ return cs1.offset - cs2.offset;
+ });
+
+ for (var i = 0; i < this.fillStyle.colors_.length; i++) {
+ var fs = this.fillStyle.colors_[i];
+
+ colors.push( (fs.offset * expansion) + inside, "% ", fs.color, ",");
+
+ if (fs.offset > insidecolor.offset || insidecolor.offset == null) {
+ insidecolor.offset = fs.offset;
+ insidecolor.color = fs.color;
+ }
+
+ if (fs.offset < outsidecolor.offset || outsidecolor.offset == null) {
+ outsidecolor.offset = fs.offset;
+ outsidecolor.color = fs.color;
+ }
+ }
+ colors.pop();
+
+ lineStr.push('<g_vml_:fill',
+ ' color="', outsidecolor.color, '"',
+ ' color2="', insidecolor.color, '"',
+ ' type="', this.fillStyle.type_, '"',
+ ' focusposition="', focus.x, ', ', focus.y, '"',
+ ' colors="', colors.join(""), '"',
+ ' opacity="', opacity, '" />');
+ } else if (aFill) {
+ lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity, '" />');
+ } else {
+ lineStr.push(
+ '<g_vml_:stroke',
+ ' opacity="', opacity,'"',
+ ' joinstyle="', this.lineJoin, '"',
+ ' miterlimit="', this.miterLimit, '"',
+ ' endcap="', processLineCap(this.lineCap) ,'"',
+ ' weight="', this.lineWidth, 'px"',
+ ' color="', color,'" />'
+ );
+ }
+
+ lineStr.push("</g_vml_:shape>");
+
+ this.element_.insertAdjacentHTML("beforeEnd", lineStr.join(""));
+
+ //this.currentPath_ = [];
+ };
+
+ contextPrototype.fill = function() {
+ this.stroke(true);
+ };
+
+ contextPrototype.closePath = function() {
+ this.currentPath_.push({type: "close"});
+ };
+
+ /**
+ * @private
+ */
+ contextPrototype.getCoords_ = function(aX, aY) {
+ return {
+ x: Z * (aX * this.m_[0][0] + aY * this.m_[1][0] + this.m_[2][0]) - Z2,
+ y: Z * (aX * this.m_[0][1] + aY * this.m_[1][1] + this.m_[2][1]) - Z2
+ }
+ };
+
+ contextPrototype.save = function() {
+ var o = {};
+ copyState(this, o);
+ this.aStack_.push(o);
+ this.mStack_.push(this.m_);
+ this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);
+ };
+
+ contextPrototype.restore = function() {
+ copyState(this.aStack_.pop(), this);
+ this.m_ = this.mStack_.pop();
+ };
+
+ contextPrototype.translate = function(aX, aY) {
+ var m1 = [
+ [1, 0, 0],
+ [0, 1, 0],
+ [aX, aY, 1]
+ ];
+
+ this.m_ = matrixMultiply(m1, this.m_);
+ };
+
+ contextPrototype.rotate = function(aRot) {
+ var c = mc(aRot);
+ var s = ms(aRot);
+
+ var m1 = [
+ [c, s, 0],
+ [-s, c, 0],
+ [0, 0, 1]
+ ];
+
+ this.m_ = matrixMultiply(m1, this.m_);
+ };
+
+ contextPrototype.scale = function(aX, aY) {
+ this.arcScaleX_ *= aX;
+ this.arcScaleY_ *= aY;
+ var m1 = [
+ [aX, 0, 0],
+ [0, aY, 0],
+ [0, 0, 1]
+ ];
+
+ this.m_ = matrixMultiply(m1, this.m_);
+ };
+
+ /******** STUBS ********/
+ contextPrototype.clip = function() {
+ // TODO: Implement
+ };
+
+ contextPrototype.arcTo = function() {
+ // TODO: Implement
+ };
+
+ contextPrototype.createPattern = function() {
+ return new CanvasPattern_;
+ };
+
+ // Gradient / Pattern Stubs
+ function CanvasGradient_(aType) {
+ this.type_ = aType;
+ this.radius1_ = 0;
+ this.radius2_ = 0;
+ this.colors_ = [];
+ this.focus_ = {x: 0, y: 0};
+ }
+
+ CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {
+ aColor = processStyle(aColor);
+ this.colors_.push({offset: 1-aOffset, color: aColor});
+ };
+
+ function CanvasPattern_() {}
+
+ // set up externs
+ G_vmlCanvasManager = G_vmlCanvasManager_;
+ CanvasRenderingContext2D = CanvasRenderingContext2D_;
+ CanvasGradient = CanvasGradient_;
+ CanvasPattern = CanvasPattern_;
+
+})();
+
+} // if
diff --git a/usr/local/www/themes/nervecenter/images/icons/icon_block_add.gif b/usr/local/www/themes/nervecenter/images/icons/icon_block_add.gif
new file mode 100644
index 0000000..eb726d6
--- /dev/null
+++ b/usr/local/www/themes/nervecenter/images/icons/icon_block_add.gif
Binary files differ
diff --git a/usr/local/www/themes/nervecenter/images/icons/icon_pass_add.gif b/usr/local/www/themes/nervecenter/images/icons/icon_pass_add.gif
new file mode 100644
index 0000000..f7f4c20
--- /dev/null
+++ b/usr/local/www/themes/nervecenter/images/icons/icon_pass_add.gif
Binary files differ
OpenPOWER on IntegriCloud