From c0b6fddeecb352cd95134d2c82e769f73edb18e7 Mon Sep 17 00:00:00 2001 From: jim-p Date: Sat, 6 Jun 2009 17:59:08 -0400 Subject: Sync my recent stuff to HEAD. Easy Rule, Log parsing tweaks, Firewall Log filter, Firewall Log Summary graphs. Also add ProtoChart. --- usr/local/www/diag_logs_filter.php | 81 +- usr/local/www/diag_logs_filter_dynamic.php | 5 +- usr/local/www/diag_logs_filter_summary.php | 227 ++ usr/local/www/easyrule.inc | 260 ++ usr/local/www/easyrule.php | 132 + usr/local/www/filter_log.inc | 82 +- usr/local/www/filterparser.php | 4 + usr/local/www/protochart/ProtoChart.js | 2653 ++++++++++++++++++++ usr/local/www/protochart/excanvas-compressed.js | 19 + usr/local/www/protochart/excanvas.js | 785 ++++++ .../nervecenter/images/icons/icon_block_add.gif | Bin 0 -> 192 bytes .../nervecenter/images/icons/icon_pass_add.gif | Bin 0 -> 183 bytes 12 files changed, 4217 insertions(+), 31 deletions(-) create mode 100644 usr/local/www/diag_logs_filter_summary.php create mode 100644 usr/local/www/easyrule.inc create mode 100644 usr/local/www/easyrule.php create mode 100644 usr/local/www/protochart/ProtoChart.js create mode 100644 usr/local/www/protochart/excanvas-compressed.js create mode 100644 usr/local/www/protochart/excanvas.js create mode 100644 usr/local/www/themes/nervecenter/images/icons/icon_block_add.gif create mode 100644 usr/local/www/themes/nervecenter/images/icons/icon_pass_add.gif (limited to 'usr') 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 . + Copyright (C) 2003-2009 Manuel Kasper , + Jim Pingle @.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"); 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");
+
+ Normal View | Dynamic View | Summary View +
- Last records; (Switch to regular view) Pause: + Last records; Pause:
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 @@ +'; + print "{$descr[$stat]} data"; + $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 = " {$outstr}"; + } elseif (substr_count($outstr, '/') == 1) { + list($proto, $port) = explode('/', $outstr); + $service = getservbyport($port, strtolower($proto)); + if ($service) + $outstr .= ": {$service}"; + } + print "{$outstr}{$summary[$stat][$k[$i]]}\n"; + } + } + $leftover = $gotlines - $total; + if ($leftover > 0) { + print "Other{$leftover}\n"; + } + print ''; +} + +function pie_block($summary, $stat, $num) { + global $gotlines, $descr; + uasort($summary[$stat] , 'cmp'); + $k = array_keys($summary[$stat]); + $total = 0; + $numentries = 0; + print "\n"; + print ""; + print ""; + print "
{$descr[$stat]}
\n"; + print "
"; +} + +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"); ?> + + + + + + + +

+ + + + + +
+ +
+
+ + +
+ Normal View | Dynamic View | Summary View

+
+ +This is a firewall log summary, of the last lines of the firewall log (Max ).
+NOTE: IE8 users must enable compatibility view. + +

+

+

+

+

+

+

+

+

+

+

+

+

+

+ +
+
+
+ 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 @@ + "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 @@ +"; + break; + } + $_GET['int'] = easyrule_find_rule_interface($_GET['int']); + if ($_GET['int'] === false) { + $message .= "Invalid interface for block rule: " . htmlspecialchars($_GET['int']) . "
"; + break; + } + if (easyrule_block_host_add($_GET['src'], $_GET['int'])) { + /* shouldn't get here, the function will redirect */ + $message .= "Host added successfully" . "
"; + } else { + $message .= "Failed to create block rule, alias, or add host." . "
"; + } + } else { + $message .= "Tried to block but had no host IP or interface
"; + } + 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']) . "
"; + break; + } + if (getprotobyname($_GET['proto']) == -1) { + $message .= "Invalid protocol for pass rule: " . htmlspecialchars($_GET['proto']) . "
"; + break; + } + if (!is_ipaddr($_GET['src'])) { + $message .= "Tried to pass invalid source IP: " . htmlspecialchars($_GET['src']) . "
"; + break; + } + if (!is_ipaddr($_GET['dst'])) { + $message .= "Tried to pass invalid destination IP: " . htmlspecialchars($_GET['dst']) . "
"; + break; + } + if (($_GET['proto'] != 'icmp') && !isset($_GET['dstport'])) { + $message .= "Missing destination port: " . htmlspecialchars($_GET['dstport']) . "
"; + 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']) . "
"; + 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!" . "
"; + } else { + $message .= "Failed to add pass rule." . "
"; + } + } else { + $message = "Missing parameters for pass rule"; + break; + } + break; + } +} + +if(stristr($retval, "error") == true) + $message = $retval; + +include("head.inc"); ?> + + +

+ + +
+ + + +
+Message: +
+ +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.

+This page is meant to be called from the block/pass buttons on the Firewall Logs page, Status > System Logs, +Firewall Tab. +
+ +
+ 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 @@ 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 = "" . htmlspecialchars($port) . ""; + } 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 + * * Flotr + * + * Complete examples can be found at: + */ + +/** + * 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 function which makes sure that our axis are expanded if needed + * + * 2. calls function providing xaxis options which fixes the ranges according to data points + * + * 3. calls function for xaxis which generates ticks according to options provided by user + * + * 4. calls 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 and we also call + */ + 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('
' + l + '
'); + } + + 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('' + l + ''); + } + } + + 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 = '
'; + + // 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 += '
' + tick.label + "
"; + + }.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 += '
' + tick.label + "
"; + }.bind(this)); + + html += '
'; + + 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 lines property is set to show or no other type of + * graph is specified. This function depends on and + * 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 and + */ + 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 + * 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 = "
" + textVal + "%
"; + //$(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 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 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 + * 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(''); + } + fragments.push(''); + rowStarted = true; + } + var label = gd.label; + if(this.options.legend.labelFormatter != null) { + label = this.options.legend.labelFormatter(label); + } + + fragments.push( + '
' + + '' + label + ''); + + }.bind(this)); + + if (rowStarted) + fragments.push(''); + + if(fragments.length > 0){ + var table = '' + fragments.join("") + '
'; + 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('
' + table + '
').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('
').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 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 event. If + * is enabled then it also fires 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 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 and + * 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(''); + 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"){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(" ','","");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("n.x){n.x=k.x}if(l.y== +null||k.yn.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;oC.offset||C.offset==null){C.offset=u.offset;C.color=u.color}if(u.offset')}else if(a){b.push('')}else{b.push("')}b.push("");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(' ' , + '', + ''); + + 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(' 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(''); + } else if (aFill) { + lineStr.push(''); + } else { + lineStr.push( + '' + ); + } + + lineStr.push(""); + + 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 Binary files /dev/null and b/usr/local/www/themes/nervecenter/images/icons/icon_block_add.gif 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 Binary files /dev/null and b/usr/local/www/themes/nervecenter/images/icons/icon_pass_add.gif differ -- cgit v1.1