├── .gitignore ├── LICENSE ├── README.md ├── autoload.php ├── composer.json ├── examples ├── base.php ├── baseRandom.php ├── graph.php └── jquery │ ├── examples.css │ └── jquery.flot.js ├── src ├── Autoloader.php ├── Contracts │ └── HongbaoContract.php ├── Handlers │ ├── HongbaoHandler.php │ └── RandomHongbaoHandler.php ├── Hongbao.php └── Library │ └── Helpers │ └── MathHelper.php └── tests ├── ExceptionTest.php ├── TestCaseOnBase.php ├── TestCaseOnBaseRandom.php └── data └── BaseConfig.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | test.php 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Zicai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 红包生成器 2 | 3 | 生成固定红包与随机红包,随机红包金额依据截尾正态分布算法来生成 4 | >Generate fixed red package and random red package, random red envelopes based on truncated normal distribution algorithm to generate 5 | 6 | ### Composer 安装 7 | composer require zicai/hongbao 8 | 9 | ![图像画案例](http://zicai.fun/images/161e0e8515db4a43.gif) 10 | 11 | ### 生成随机红包(Random Red Package) ### 12 | 13 | ```php 14 | require_once '../vendor/autoload.php'; 15 | 16 | use Hongbao\Hongbao; 17 | 18 | /** 19 | * 生成随机红包 20 | */ 21 | $options = [ 22 | 'total_money' => 1000, // 总金额 23 | 'total_number' => 1000, // 总红包数量 24 | 'minimum_val' => 0.01, // 最小随机红包金额 25 | 'maximum_val' => 20, // 最大随机红包金额 26 | ]; 27 | 28 | //通过try catch获取可能出现的参数设置错误信息 29 | try { 30 | $hongbao = Hongbao::getInstance()->randomAmount($options); 31 | foreach ($hongbao as $result) { 32 | echo "
";
33 |         print_r($result);
34 |     }
35 | } catch (\Exception $e) {
36 |     $error = $e->getMessage();
37 |     var_dump($error);
38 | }
39 | ```
40 | 
41 | 
42 | ### 生成固定红包(fixed red package) ###
43 | ```php
44 | require_once '../vendor/autoload.php';
45 | 
46 | use Hongbao\Hongbao;
47 | 
48 | /**
49 |  * 生成固定红包
50 |  */
51 | 
52 | $options = [
53 |     'total_money' => 1000, // 总金额
54 |     'total_number' => 1000, // 总红包数量
55 |     'val' => 0.01, // 单个红包金额
56 | ];
57 | 
58 | //通过try catch获取可能出现的参数设置错误信息
59 | try {
60 |     $hongbao = Hongbao::getInstance()->fixedAmount($options);
61 |     foreach ($hongbao as $result) {
62 |         echo "
";
63 |         print_r($result);
64 |     }
65 | } catch (\Exception $e) {
66 |     $error = $e->getMessage();
67 |     var_dump($error);
68 | }
69 | ```
70 | 


--------------------------------------------------------------------------------
/autoload.php:
--------------------------------------------------------------------------------
 1 | 
 7 |  *
 8 |  * For the full copyright and license information, please view the LICENSE
 9 |  * file that was distributed with this source code.
10 |  */
11 | 
12 | require __DIR__.'/src/Autoloader.php';
13 | 
14 | Hongbao\Autoloader::register();
15 | 


--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
 1 | {
 2 |     "name": "zicai/hongbao",
 3 |     "description": "Generate fixed red package and random red package, random red envelopes based on truncated normal distribution algorithm to generate",
 4 |     "keywords": ["hongbao"],
 5 |     "type": "library",
 6 |     "license": "MIT",
 7 |     "authors": [
 8 |         {
 9 |             "name": "Zicai",
10 |             "email": "pmw1014@163.com"
11 |         }
12 |     ],
13 |     "minimum-stability":"dev",
14 |     "require": {
15 |         "php": ">=7.0.0"
16 |     },
17 |     "require-dev": {
18 |         "phpunit/phpunit": "^7.5"
19 |     },
20 |     "autoload": {
21 |         "psr-4": {
22 |             "Hongbao\\": "src/"
23 |         }
24 |     },
25 |     "extra": {
26 |         "hongbao": {
27 |             "dev-master": "1.0.x-dev"
28 |         }
29 |     }
30 | }
31 | 


--------------------------------------------------------------------------------
/examples/base.php:
--------------------------------------------------------------------------------
 1 |  1000, // 总金额
15 |     'total_number' => 1000, // 总红包数量
16 |     'val' => 0.01, // 单个红包金额
17 |     'limit'=>100,
18 | ];
19 | 
20 | try {
21 |     $hongbao = Hongbao::getInstance()->fixedAmount($options);
22 |     echo "
";
23 |     foreach ($hongbao as $result) {
24 |         print_r($result);
25 |     }
26 | } catch (\Exception $e) {
27 |     $error = $e->getMessage();
28 |     var_dump($error);
29 | }
30 | echo "Down\n";
31 | echo "耗时:" . (microtime(true)-$t1);
32 | echo "\n";
33 | echo "消耗内存:" . round((memory_get_usage()-$m1)/1024/1024,2)."MB\n";
34 | 


--------------------------------------------------------------------------------
/examples/baseRandom.php:
--------------------------------------------------------------------------------
 1 |  10000, // 总金额
15 |     'total_number' => 10000, // 总红包数量
16 |     'minimum_val' => 0.01, // 最小随机红包金额
17 |     'maximum_val' => 20, // 最大随机红包金额
18 | ];
19 | 
20 | try {
21 |     $hongbao = Hongbao::getInstance()->randomAmount($options);
22 |     echo "
";
23 |     foreach ($hongbao as $result) {
24 |         print_r($result);
25 |     }
26 | } catch (\Exception $e) {
27 |     $error = $e->getMessage();
28 |     var_dump($error);
29 | }
30 | echo "Down\n";
31 | 
32 | echo "耗时:" . (microtime(true)-$t1);
33 | echo "\n";
34 | echo "消耗内存:" . round((memory_get_usage()-$m1)/1024/1024,2)."MB\n";
35 | 


--------------------------------------------------------------------------------
/examples/graph.php:
--------------------------------------------------------------------------------
  1 |  isset($_POST['total_money']) ? (float)$_POST['total_money'] : 1000, // 总金额
 12 |     'total_number' => isset($_POST['total_number']) ? (int)$_POST['total_number'] : 1000, // 总红包数量
 13 |     'minimum_val' => isset($_POST['minimum_val']) ? (float)$_POST['minimum_val'] : 0.01, // 最小随机红包金额
 14 |     'maximum_val' => isset($_POST['maximum_val']) ? (float)$_POST['maximum_val'] : 20, // 最大随机红包金额
 15 | ];
 16 | $error = '';
 17 | $data = [];
 18 | $money_left = 0.00;
 19 | try {
 20 |     $Hongbao = Hongbao::getInstance()->randomAmount($options);
 21 |     $i = 0;
 22 |     foreach ($Hongbao as $result) {
 23 |         foreach ($result['data'] as &$row) {
 24 |             $row = [$i, $row];
 25 |             $i++;
 26 |         }
 27 |         $data = array_merge($result['data'],$data);
 28 |         $money_left = $result['money_left'];
 29 |     }
 30 | } catch (\Exception $e) {
 31 |     $error = $e->getMessage();
 32 | }
 33 | ?>
 34 | 
 35 | 
 36 | 
 37 |     
 38 |     Flot Examples: Canvas text
 39 |     
 40 |     
 41 |     
 45 |     
 46 |     
 91 | 
 92 | 
 93 | 
 94 |     
 98 | 
 99 |     
100 | 101 |
102 |
103 |
104 | 105 |

106 |
107 |

108 | 111 |

112 |

113 | 116 |

117 |

118 | 121 |

122 |

123 | 126 |

127 |

128 | 129 |

130 |

131 | "; 134 | echo "消耗内存:" . round((memory_get_usage()-$m1)/1024/1024,2)."MB
"; 135 | ?> 136 |

137 |
138 |
139 | 140 | 141 | -------------------------------------------------------------------------------- /examples/jquery/examples.css: -------------------------------------------------------------------------------- 1 | * { padding: 0; margin: 0; vertical-align: top; } 2 | 3 | body { 4 | background: url(background.png) repeat-x; 5 | font: 18px/1.5em "proxima-nova", Helvetica, Arial, sans-serif; 6 | } 7 | 8 | a { color: #069; } 9 | a:hover { color: #28b; } 10 | 11 | h2 { 12 | margin-top: 15px; 13 | font: normal 32px "omnes-pro", Helvetica, Arial, sans-serif; 14 | } 15 | 16 | h3 { 17 | margin-left: 30px; 18 | font: normal 26px "omnes-pro", Helvetica, Arial, sans-serif; 19 | color: #666; 20 | } 21 | 22 | p { 23 | margin-top: 10px; 24 | } 25 | 26 | button { 27 | font-size: 18px; 28 | padding: 1px 7px; 29 | } 30 | 31 | input { 32 | font-size: 18px; 33 | } 34 | 35 | input[type=checkbox] { 36 | margin: 7px; 37 | } 38 | 39 | #header { 40 | position: relative; 41 | width: 900px; 42 | margin: auto; 43 | } 44 | 45 | #header h2 { 46 | margin-left: 10px; 47 | vertical-align: middle; 48 | font-size: 42px; 49 | font-weight: bold; 50 | text-decoration: none; 51 | color: #000; 52 | } 53 | 54 | #content { 55 | width: 880px; 56 | margin: 0 auto; 57 | padding: 10px; 58 | } 59 | 60 | #footer { 61 | margin-top: 25px; 62 | margin-bottom: 10px; 63 | text-align: center; 64 | font-size: 12px; 65 | color: #999; 66 | } 67 | 68 | .demo-container { 69 | box-sizing: border-box; 70 | width: 850px; 71 | height: 450px; 72 | padding: 20px 15px 15px 15px; 73 | margin: 15px auto 30px auto; 74 | border: 1px solid #ddd; 75 | background: #fff; 76 | background: linear-gradient(#f6f6f6 0, #fff 50px); 77 | background: -o-linear-gradient(#f6f6f6 0, #fff 50px); 78 | background: -ms-linear-gradient(#f6f6f6 0, #fff 50px); 79 | background: -moz-linear-gradient(#f6f6f6 0, #fff 50px); 80 | background: -webkit-linear-gradient(#f6f6f6 0, #fff 50px); 81 | box-shadow: 0 3px 10px rgba(0,0,0,0.15); 82 | -o-box-shadow: 0 3px 10px rgba(0,0,0,0.1); 83 | -ms-box-shadow: 0 3px 10px rgba(0,0,0,0.1); 84 | -moz-box-shadow: 0 3px 10px rgba(0,0,0,0.1); 85 | -webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1); 86 | } 87 | 88 | .demo-placeholder { 89 | width: 100%; 90 | height: 100%; 91 | font-size: 14px; 92 | line-height: 1.2em; 93 | } 94 | 95 | .legend table { 96 | border-spacing: 5px; 97 | } -------------------------------------------------------------------------------- /examples/jquery/jquery.flot.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | 8 | // first an inline dependency, jquery.colorhelpers.js, we inline it here 9 | // for convenience 10 | 11 | /* Plugin for jQuery for working with colors. 12 | * 13 | * Version 1.1. 14 | * 15 | * Inspiration from jQuery color animation plugin by John Resig. 16 | * 17 | * Released under the MIT license by Ole Laursen, October 2009. 18 | * 19 | * Examples: 20 | * 21 | * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() 22 | * var c = $.color.extract($("#mydiv"), 'background-color'); 23 | * console.log(c.r, c.g, c.b, c.a); 24 | * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" 25 | * 26 | * Note that .scale() and .add() return the same modified object 27 | * instead of making a new one. 28 | * 29 | * V. 1.1: Fix error handling so e.g. parsing an empty string does 30 | * produce a color rather than just crashing. 31 | */ 32 | (function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/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 m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/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 m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); 33 | 34 | // the actual Flot code 35 | (function($) { 36 | 37 | // Cache the prototype hasOwnProperty for faster access 38 | 39 | var hasOwnProperty = Object.prototype.hasOwnProperty; 40 | 41 | // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM 42 | // operation produces the same effect as detach, i.e. removing the element 43 | // without touching its jQuery data. 44 | 45 | // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. 46 | 47 | if (!$.fn.detach) { 48 | $.fn.detach = function() { 49 | return this.each(function() { 50 | if (this.parentNode) { 51 | this.parentNode.removeChild( this ); 52 | } 53 | }); 54 | }; 55 | } 56 | 57 | /////////////////////////////////////////////////////////////////////////// 58 | // The Canvas object is a wrapper around an HTML5 tag. 59 | // 60 | // @constructor 61 | // @param {string} cls List of classes to apply to the canvas. 62 | // @param {element} container Element onto which to append the canvas. 63 | // 64 | // Requiring a container is a little iffy, but unfortunately canvas 65 | // operations don't work unless the canvas is attached to the DOM. 66 | 67 | function Canvas(cls, container) { 68 | 69 | var element = container.children("." + cls)[0]; 70 | 71 | if (element == null) { 72 | 73 | element = document.createElement("canvas"); 74 | element.className = cls; 75 | 76 | $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) 77 | .appendTo(container); 78 | 79 | // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas 80 | 81 | if (!element.getContext) { 82 | if (window.G_vmlCanvasManager) { 83 | element = window.G_vmlCanvasManager.initElement(element); 84 | } else { 85 | throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); 86 | } 87 | } 88 | } 89 | 90 | this.element = element; 91 | 92 | var context = this.context = element.getContext("2d"); 93 | 94 | // Determine the screen's ratio of physical to device-independent 95 | // pixels. This is the ratio between the canvas width that the browser 96 | // advertises and the number of pixels actually present in that space. 97 | 98 | // The iPhone 4, for example, has a device-independent width of 320px, 99 | // but its screen is actually 640px wide. It therefore has a pixel 100 | // ratio of 2, while most normal devices have a ratio of 1. 101 | 102 | var devicePixelRatio = window.devicePixelRatio || 1, 103 | backingStoreRatio = 104 | context.webkitBackingStorePixelRatio || 105 | context.mozBackingStorePixelRatio || 106 | context.msBackingStorePixelRatio || 107 | context.oBackingStorePixelRatio || 108 | context.backingStorePixelRatio || 1; 109 | 110 | this.pixelRatio = devicePixelRatio / backingStoreRatio; 111 | 112 | // Size the canvas to match the internal dimensions of its container 113 | 114 | this.resize(container.width(), container.height()); 115 | 116 | // Collection of HTML div layers for text overlaid onto the canvas 117 | 118 | this.textContainer = null; 119 | this.text = {}; 120 | 121 | // Cache of text fragments and metrics, so we can avoid expensively 122 | // re-calculating them when the plot is re-rendered in a loop. 123 | 124 | this._textCache = {}; 125 | } 126 | 127 | // Resizes the canvas to the given dimensions. 128 | // 129 | // @param {number} width New width of the canvas, in pixels. 130 | // @param {number} width New height of the canvas, in pixels. 131 | 132 | Canvas.prototype.resize = function(width, height) { 133 | 134 | if (width <= 0 || height <= 0) { 135 | throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); 136 | } 137 | 138 | var element = this.element, 139 | context = this.context, 140 | pixelRatio = this.pixelRatio; 141 | 142 | // Resize the canvas, increasing its density based on the display's 143 | // pixel ratio; basically giving it more pixels without increasing the 144 | // size of its element, to take advantage of the fact that retina 145 | // displays have that many more pixels in the same advertised space. 146 | 147 | // Resizing should reset the state (excanvas seems to be buggy though) 148 | 149 | if (this.width != width) { 150 | element.width = width * pixelRatio; 151 | element.style.width = width + "px"; 152 | this.width = width; 153 | } 154 | 155 | if (this.height != height) { 156 | element.height = height * pixelRatio; 157 | element.style.height = height + "px"; 158 | this.height = height; 159 | } 160 | 161 | // Save the context, so we can reset in case we get replotted. The 162 | // restore ensure that we're really back at the initial state, and 163 | // should be safe even if we haven't saved the initial state yet. 164 | 165 | context.restore(); 166 | context.save(); 167 | 168 | // Scale the coordinate space to match the display density; so even though we 169 | // may have twice as many pixels, we still want lines and other drawing to 170 | // appear at the same size; the extra pixels will just make them crisper. 171 | 172 | context.scale(pixelRatio, pixelRatio); 173 | }; 174 | 175 | // Clears the entire canvas area, not including any overlaid HTML text 176 | 177 | Canvas.prototype.clear = function() { 178 | this.context.clearRect(0, 0, this.width, this.height); 179 | }; 180 | 181 | // Finishes rendering the canvas, including managing the text overlay. 182 | 183 | Canvas.prototype.render = function() { 184 | 185 | var cache = this._textCache; 186 | 187 | // For each text layer, add elements marked as active that haven't 188 | // already been rendered, and remove those that are no longer active. 189 | 190 | for (var layerKey in cache) { 191 | if (hasOwnProperty.call(cache, layerKey)) { 192 | 193 | var layer = this.getTextLayer(layerKey), 194 | layerCache = cache[layerKey]; 195 | 196 | layer.hide(); 197 | 198 | for (var styleKey in layerCache) { 199 | if (hasOwnProperty.call(layerCache, styleKey)) { 200 | var styleCache = layerCache[styleKey]; 201 | for (var key in styleCache) { 202 | if (hasOwnProperty.call(styleCache, key)) { 203 | 204 | var positions = styleCache[key].positions; 205 | 206 | for (var i = 0, position; position = positions[i]; i++) { 207 | if (position.active) { 208 | if (!position.rendered) { 209 | layer.append(position.element); 210 | position.rendered = true; 211 | } 212 | } else { 213 | positions.splice(i--, 1); 214 | if (position.rendered) { 215 | position.element.detach(); 216 | } 217 | } 218 | } 219 | 220 | if (positions.length == 0) { 221 | delete styleCache[key]; 222 | } 223 | } 224 | } 225 | } 226 | } 227 | 228 | layer.show(); 229 | } 230 | } 231 | }; 232 | 233 | // Creates (if necessary) and returns the text overlay container. 234 | // 235 | // @param {string} classes String of space-separated CSS classes used to 236 | // uniquely identify the text layer. 237 | // @return {object} The jQuery-wrapped text-layer div. 238 | 239 | Canvas.prototype.getTextLayer = function(classes) { 240 | 241 | var layer = this.text[classes]; 242 | 243 | // Create the text layer if it doesn't exist 244 | 245 | if (layer == null) { 246 | 247 | // Create the text layer container, if it doesn't exist 248 | 249 | if (this.textContainer == null) { 250 | this.textContainer = $("
") 251 | .css({ 252 | position: "absolute", 253 | top: 0, 254 | left: 0, 255 | bottom: 0, 256 | right: 0, 257 | 'font-size': "smaller", 258 | color: "#545454" 259 | }) 260 | .insertAfter(this.element); 261 | } 262 | 263 | layer = this.text[classes] = $("
") 264 | .addClass(classes) 265 | .css({ 266 | position: "absolute", 267 | top: 0, 268 | left: 0, 269 | bottom: 0, 270 | right: 0 271 | }) 272 | .appendTo(this.textContainer); 273 | } 274 | 275 | return layer; 276 | }; 277 | 278 | // Creates (if necessary) and returns a text info object. 279 | // 280 | // The object looks like this: 281 | // 282 | // { 283 | // width: Width of the text's wrapper div. 284 | // height: Height of the text's wrapper div. 285 | // element: The jQuery-wrapped HTML div containing the text. 286 | // positions: Array of positions at which this text is drawn. 287 | // } 288 | // 289 | // The positions array contains objects that look like this: 290 | // 291 | // { 292 | // active: Flag indicating whether the text should be visible. 293 | // rendered: Flag indicating whether the text is currently visible. 294 | // element: The jQuery-wrapped HTML div containing the text. 295 | // x: X coordinate at which to draw the text. 296 | // y: Y coordinate at which to draw the text. 297 | // } 298 | // 299 | // Each position after the first receives a clone of the original element. 300 | // 301 | // The idea is that that the width, height, and general 'identity' of the 302 | // text is constant no matter where it is placed; the placements are a 303 | // secondary property. 304 | // 305 | // Canvas maintains a cache of recently-used text info objects; getTextInfo 306 | // either returns the cached element or creates a new entry. 307 | // 308 | // @param {string} layer A string of space-separated CSS classes uniquely 309 | // identifying the layer containing this text. 310 | // @param {string} text Text string to retrieve info for. 311 | // @param {(string|object)=} font Either a string of space-separated CSS 312 | // classes or a font-spec object, defining the text's font and style. 313 | // @param {number=} angle Angle at which to rotate the text, in degrees. 314 | // Angle is currently unused, it will be implemented in the future. 315 | // @param {number=} width Maximum width of the text before it wraps. 316 | // @return {object} a text info object. 317 | 318 | Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { 319 | 320 | var textStyle, layerCache, styleCache, info; 321 | 322 | // Cast the value to a string, in case we were given a number or such 323 | 324 | text = "" + text; 325 | 326 | // If the font is a font-spec object, generate a CSS font definition 327 | 328 | if (typeof font === "object") { 329 | textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; 330 | } else { 331 | textStyle = font; 332 | } 333 | 334 | // Retrieve (or create) the cache for the text's layer and styles 335 | 336 | layerCache = this._textCache[layer]; 337 | 338 | if (layerCache == null) { 339 | layerCache = this._textCache[layer] = {}; 340 | } 341 | 342 | styleCache = layerCache[textStyle]; 343 | 344 | if (styleCache == null) { 345 | styleCache = layerCache[textStyle] = {}; 346 | } 347 | 348 | info = styleCache[text]; 349 | 350 | // If we can't find a matching element in our cache, create a new one 351 | 352 | if (info == null) { 353 | 354 | var element = $("
").html(text) 355 | .css({ 356 | position: "absolute", 357 | 'max-width': width, 358 | top: -9999 359 | }) 360 | .appendTo(this.getTextLayer(layer)); 361 | 362 | if (typeof font === "object") { 363 | element.css({ 364 | font: textStyle, 365 | color: font.color 366 | }); 367 | } else if (typeof font === "string") { 368 | element.addClass(font); 369 | } 370 | 371 | info = styleCache[text] = { 372 | width: element.outerWidth(true), 373 | height: element.outerHeight(true), 374 | element: element, 375 | positions: [] 376 | }; 377 | 378 | element.detach(); 379 | } 380 | 381 | return info; 382 | }; 383 | 384 | // Adds a text string to the canvas text overlay. 385 | // 386 | // The text isn't drawn immediately; it is marked as rendering, which will 387 | // result in its addition to the canvas on the next render pass. 388 | // 389 | // @param {string} layer A string of space-separated CSS classes uniquely 390 | // identifying the layer containing this text. 391 | // @param {number} x X coordinate at which to draw the text. 392 | // @param {number} y Y coordinate at which to draw the text. 393 | // @param {string} text Text string to draw. 394 | // @param {(string|object)=} font Either a string of space-separated CSS 395 | // classes or a font-spec object, defining the text's font and style. 396 | // @param {number=} angle Angle at which to rotate the text, in degrees. 397 | // Angle is currently unused, it will be implemented in the future. 398 | // @param {number=} width Maximum width of the text before it wraps. 399 | // @param {string=} halign Horizontal alignment of the text; either "left", 400 | // "center" or "right". 401 | // @param {string=} valign Vertical alignment of the text; either "top", 402 | // "middle" or "bottom". 403 | 404 | Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { 405 | 406 | var info = this.getTextInfo(layer, text, font, angle, width), 407 | positions = info.positions; 408 | 409 | // Tweak the div's position to match the text's alignment 410 | 411 | if (halign == "center") { 412 | x -= info.width / 2; 413 | } else if (halign == "right") { 414 | x -= info.width; 415 | } 416 | 417 | if (valign == "middle") { 418 | y -= info.height / 2; 419 | } else if (valign == "bottom") { 420 | y -= info.height; 421 | } 422 | 423 | // Determine whether this text already exists at this position. 424 | // If so, mark it for inclusion in the next render pass. 425 | 426 | for (var i = 0, position; position = positions[i]; i++) { 427 | if (position.x == x && position.y == y) { 428 | position.active = true; 429 | return; 430 | } 431 | } 432 | 433 | // If the text doesn't exist at this position, create a new entry 434 | 435 | // For the very first position we'll re-use the original element, 436 | // while for subsequent ones we'll clone it. 437 | 438 | position = { 439 | active: true, 440 | rendered: false, 441 | element: positions.length ? info.element.clone() : info.element, 442 | x: x, 443 | y: y 444 | }; 445 | 446 | positions.push(position); 447 | 448 | // Move the element to its final position within the container 449 | 450 | position.element.css({ 451 | top: Math.round(y), 452 | left: Math.round(x), 453 | 'text-align': halign // In case the text wraps 454 | }); 455 | }; 456 | 457 | // Removes one or more text strings from the canvas text overlay. 458 | // 459 | // If no parameters are given, all text within the layer is removed. 460 | // 461 | // Note that the text is not immediately removed; it is simply marked as 462 | // inactive, which will result in its removal on the next render pass. 463 | // This avoids the performance penalty for 'clear and redraw' behavior, 464 | // where we potentially get rid of all text on a layer, but will likely 465 | // add back most or all of it later, as when redrawing axes, for example. 466 | // 467 | // @param {string} layer A string of space-separated CSS classes uniquely 468 | // identifying the layer containing this text. 469 | // @param {number=} x X coordinate of the text. 470 | // @param {number=} y Y coordinate of the text. 471 | // @param {string=} text Text string to remove. 472 | // @param {(string|object)=} font Either a string of space-separated CSS 473 | // classes or a font-spec object, defining the text's font and style. 474 | // @param {number=} angle Angle at which the text is rotated, in degrees. 475 | // Angle is currently unused, it will be implemented in the future. 476 | 477 | Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { 478 | if (text == null) { 479 | var layerCache = this._textCache[layer]; 480 | if (layerCache != null) { 481 | for (var styleKey in layerCache) { 482 | if (hasOwnProperty.call(layerCache, styleKey)) { 483 | var styleCache = layerCache[styleKey]; 484 | for (var key in styleCache) { 485 | if (hasOwnProperty.call(styleCache, key)) { 486 | var positions = styleCache[key].positions; 487 | for (var i = 0, position; position = positions[i]; i++) { 488 | position.active = false; 489 | } 490 | } 491 | } 492 | } 493 | } 494 | } 495 | } else { 496 | var positions = this.getTextInfo(layer, text, font, angle).positions; 497 | for (var i = 0, position; position = positions[i]; i++) { 498 | if (position.x == x && position.y == y) { 499 | position.active = false; 500 | } 501 | } 502 | } 503 | }; 504 | 505 | /////////////////////////////////////////////////////////////////////////// 506 | // The top-level container for the entire plot. 507 | 508 | function Plot(placeholder, data_, options_, plugins) { 509 | // data is on the form: 510 | // [ series1, series2 ... ] 511 | // where series is either just the data as [ [x1, y1], [x2, y2], ... ] 512 | // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } 513 | 514 | var series = [], 515 | options = { 516 | // the color theme used for graphs 517 | colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], 518 | legend: { 519 | show: true, 520 | noColumns: 1, // number of colums in legend table 521 | labelFormatter: null, // fn: string -> string 522 | labelBoxBorderColor: "#ccc", // border color for the little label boxes 523 | container: null, // container (as jQuery object) to put legend in, null means default on top of graph 524 | position: "ne", // position of default legend container within plot 525 | margin: 5, // distance from grid edge to default legend container within plot 526 | backgroundColor: null, // null means auto-detect 527 | backgroundOpacity: 0.85, // set to 0 to avoid background 528 | sorted: null // default to no legend sorting 529 | }, 530 | xaxis: { 531 | show: null, // null = auto-detect, true = always, false = never 532 | position: "bottom", // or "top" 533 | mode: null, // null or "time" 534 | font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } 535 | color: null, // base color, labels, ticks 536 | tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" 537 | transform: null, // null or f: number -> number to transform axis 538 | inverseTransform: null, // if transform is set, this should be the inverse function 539 | min: null, // min. value to show, null means set automatically 540 | max: null, // max. value to show, null means set automatically 541 | autoscaleMargin: null, // margin in % to add if auto-setting min/max 542 | ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks 543 | tickFormatter: null, // fn: number -> string 544 | labelWidth: null, // size of tick labels in pixels 545 | labelHeight: null, 546 | reserveSpace: null, // whether to reserve space even if axis isn't shown 547 | tickLength: null, // size in pixels of ticks, or "full" for whole line 548 | alignTicksWithAxis: null, // axis number or null for no sync 549 | tickDecimals: null, // no. of decimals, null means auto 550 | tickSize: null, // number or [number, "unit"] 551 | minTickSize: null // number or [number, "unit"] 552 | }, 553 | yaxis: { 554 | autoscaleMargin: 0.02, 555 | position: "left" // or "right" 556 | }, 557 | xaxes: [], 558 | yaxes: [], 559 | series: { 560 | points: { 561 | show: false, 562 | radius: 3, 563 | lineWidth: 2, // in pixels 564 | fill: true, 565 | fillColor: "#ffffff", 566 | symbol: "circle" // or callback 567 | }, 568 | lines: { 569 | // we don't put in show: false so we can see 570 | // whether lines were actively disabled 571 | lineWidth: 2, // in pixels 572 | fill: false, 573 | fillColor: null, 574 | steps: false 575 | // Omit 'zero', so we can later default its value to 576 | // match that of the 'fill' option. 577 | }, 578 | bars: { 579 | show: false, 580 | lineWidth: 2, // in pixels 581 | barWidth: 1, // in units of the x axis 582 | fill: true, 583 | fillColor: null, 584 | align: "left", // "left", "right", or "center" 585 | horizontal: false, 586 | zero: true 587 | }, 588 | shadowSize: 3, 589 | highlightColor: null 590 | }, 591 | grid: { 592 | show: true, 593 | aboveData: false, 594 | color: "#545454", // primary color used for outline and labels 595 | backgroundColor: null, // null for transparent, else color 596 | borderColor: null, // set if different from the grid color 597 | tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" 598 | margin: 0, // distance from the canvas edge to the grid 599 | labelMargin: 5, // in pixels 600 | axisMargin: 8, // in pixels 601 | borderWidth: 2, // in pixels 602 | minBorderMargin: null, // in pixels, null means taken from points radius 603 | markings: null, // array of ranges or fn: axes -> array of ranges 604 | markingsColor: "#f4f4f4", 605 | markingsLineWidth: 2, 606 | // interactive stuff 607 | clickable: false, 608 | hoverable: false, 609 | autoHighlight: true, // highlight in case mouse is near 610 | mouseActiveRadius: 10 // how far the mouse can be away to activate an item 611 | }, 612 | interaction: { 613 | redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow 614 | }, 615 | hooks: {} 616 | }, 617 | surface = null, // the canvas for the plot itself 618 | overlay = null, // canvas for interactive stuff on top of plot 619 | eventHolder = null, // jQuery object that events should be bound to 620 | ctx = null, octx = null, 621 | xaxes = [], yaxes = [], 622 | plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, 623 | plotWidth = 0, plotHeight = 0, 624 | hooks = { 625 | processOptions: [], 626 | processRawData: [], 627 | processDatapoints: [], 628 | processOffset: [], 629 | drawBackground: [], 630 | drawSeries: [], 631 | draw: [], 632 | bindEvents: [], 633 | drawOverlay: [], 634 | shutdown: [] 635 | }, 636 | plot = this; 637 | 638 | // public functions 639 | plot.setData = setData; 640 | plot.setupGrid = setupGrid; 641 | plot.draw = draw; 642 | plot.getPlaceholder = function() { return placeholder; }; 643 | plot.getCanvas = function() { return surface.element; }; 644 | plot.getPlotOffset = function() { return plotOffset; }; 645 | plot.width = function () { return plotWidth; }; 646 | plot.height = function () { return plotHeight; }; 647 | plot.offset = function () { 648 | var o = eventHolder.offset(); 649 | o.left += plotOffset.left; 650 | o.top += plotOffset.top; 651 | return o; 652 | }; 653 | plot.getData = function () { return series; }; 654 | plot.getAxes = function () { 655 | var res = {}, i; 656 | $.each(xaxes.concat(yaxes), function (_, axis) { 657 | if (axis) 658 | res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; 659 | }); 660 | return res; 661 | }; 662 | plot.getXAxes = function () { return xaxes; }; 663 | plot.getYAxes = function () { return yaxes; }; 664 | plot.c2p = canvasToAxisCoords; 665 | plot.p2c = axisToCanvasCoords; 666 | plot.getOptions = function () { return options; }; 667 | plot.highlight = highlight; 668 | plot.unhighlight = unhighlight; 669 | plot.triggerRedrawOverlay = triggerRedrawOverlay; 670 | plot.pointOffset = function(point) { 671 | return { 672 | left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), 673 | top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) 674 | }; 675 | }; 676 | plot.shutdown = shutdown; 677 | plot.destroy = function () { 678 | shutdown(); 679 | placeholder.removeData("plot").empty(); 680 | 681 | series = []; 682 | options = null; 683 | surface = null; 684 | overlay = null; 685 | eventHolder = null; 686 | ctx = null; 687 | octx = null; 688 | xaxes = []; 689 | yaxes = []; 690 | hooks = null; 691 | highlights = []; 692 | plot = null; 693 | }; 694 | plot.resize = function () { 695 | var width = placeholder.width(), 696 | height = placeholder.height(); 697 | surface.resize(width, height); 698 | overlay.resize(width, height); 699 | }; 700 | 701 | // public attributes 702 | plot.hooks = hooks; 703 | 704 | // initialize 705 | initPlugins(plot); 706 | parseOptions(options_); 707 | setupCanvases(); 708 | setData(data_); 709 | setupGrid(); 710 | draw(); 711 | bindEvents(); 712 | 713 | 714 | function executeHooks(hook, args) { 715 | args = [plot].concat(args); 716 | for (var i = 0; i < hook.length; ++i) 717 | hook[i].apply(this, args); 718 | } 719 | 720 | function initPlugins() { 721 | 722 | // References to key classes, allowing plugins to modify them 723 | 724 | var classes = { 725 | Canvas: Canvas 726 | }; 727 | 728 | for (var i = 0; i < plugins.length; ++i) { 729 | var p = plugins[i]; 730 | p.init(plot, classes); 731 | if (p.options) 732 | $.extend(true, options, p.options); 733 | } 734 | } 735 | 736 | function parseOptions(opts) { 737 | 738 | $.extend(true, options, opts); 739 | 740 | // $.extend merges arrays, rather than replacing them. When less 741 | // colors are provided than the size of the default palette, we 742 | // end up with those colors plus the remaining defaults, which is 743 | // not expected behavior; avoid it by replacing them here. 744 | 745 | if (opts && opts.colors) { 746 | options.colors = opts.colors; 747 | } 748 | 749 | if (options.xaxis.color == null) 750 | options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); 751 | if (options.yaxis.color == null) 752 | options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); 753 | 754 | if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility 755 | options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; 756 | if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility 757 | options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; 758 | 759 | if (options.grid.borderColor == null) 760 | options.grid.borderColor = options.grid.color; 761 | if (options.grid.tickColor == null) 762 | options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); 763 | 764 | // Fill in defaults for axis options, including any unspecified 765 | // font-spec fields, if a font-spec was provided. 766 | 767 | // If no x/y axis options were provided, create one of each anyway, 768 | // since the rest of the code assumes that they exist. 769 | 770 | var i, axisOptions, axisCount, 771 | fontSize = placeholder.css("font-size"), 772 | fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, 773 | fontDefaults = { 774 | style: placeholder.css("font-style"), 775 | size: Math.round(0.8 * fontSizeDefault), 776 | variant: placeholder.css("font-variant"), 777 | weight: placeholder.css("font-weight"), 778 | family: placeholder.css("font-family") 779 | }; 780 | 781 | axisCount = options.xaxes.length || 1; 782 | for (i = 0; i < axisCount; ++i) { 783 | 784 | axisOptions = options.xaxes[i]; 785 | if (axisOptions && !axisOptions.tickColor) { 786 | axisOptions.tickColor = axisOptions.color; 787 | } 788 | 789 | axisOptions = $.extend(true, {}, options.xaxis, axisOptions); 790 | options.xaxes[i] = axisOptions; 791 | 792 | if (axisOptions.font) { 793 | axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); 794 | if (!axisOptions.font.color) { 795 | axisOptions.font.color = axisOptions.color; 796 | } 797 | if (!axisOptions.font.lineHeight) { 798 | axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); 799 | } 800 | } 801 | } 802 | 803 | axisCount = options.yaxes.length || 1; 804 | for (i = 0; i < axisCount; ++i) { 805 | 806 | axisOptions = options.yaxes[i]; 807 | if (axisOptions && !axisOptions.tickColor) { 808 | axisOptions.tickColor = axisOptions.color; 809 | } 810 | 811 | axisOptions = $.extend(true, {}, options.yaxis, axisOptions); 812 | options.yaxes[i] = axisOptions; 813 | 814 | if (axisOptions.font) { 815 | axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); 816 | if (!axisOptions.font.color) { 817 | axisOptions.font.color = axisOptions.color; 818 | } 819 | if (!axisOptions.font.lineHeight) { 820 | axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); 821 | } 822 | } 823 | } 824 | 825 | // backwards compatibility, to be removed in future 826 | if (options.xaxis.noTicks && options.xaxis.ticks == null) 827 | options.xaxis.ticks = options.xaxis.noTicks; 828 | if (options.yaxis.noTicks && options.yaxis.ticks == null) 829 | options.yaxis.ticks = options.yaxis.noTicks; 830 | if (options.x2axis) { 831 | options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); 832 | options.xaxes[1].position = "top"; 833 | // Override the inherit to allow the axis to auto-scale 834 | if (options.x2axis.min == null) { 835 | options.xaxes[1].min = null; 836 | } 837 | if (options.x2axis.max == null) { 838 | options.xaxes[1].max = null; 839 | } 840 | } 841 | if (options.y2axis) { 842 | options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); 843 | options.yaxes[1].position = "right"; 844 | // Override the inherit to allow the axis to auto-scale 845 | if (options.y2axis.min == null) { 846 | options.yaxes[1].min = null; 847 | } 848 | if (options.y2axis.max == null) { 849 | options.yaxes[1].max = null; 850 | } 851 | } 852 | if (options.grid.coloredAreas) 853 | options.grid.markings = options.grid.coloredAreas; 854 | if (options.grid.coloredAreasColor) 855 | options.grid.markingsColor = options.grid.coloredAreasColor; 856 | if (options.lines) 857 | $.extend(true, options.series.lines, options.lines); 858 | if (options.points) 859 | $.extend(true, options.series.points, options.points); 860 | if (options.bars) 861 | $.extend(true, options.series.bars, options.bars); 862 | if (options.shadowSize != null) 863 | options.series.shadowSize = options.shadowSize; 864 | if (options.highlightColor != null) 865 | options.series.highlightColor = options.highlightColor; 866 | 867 | // save options on axes for future reference 868 | for (i = 0; i < options.xaxes.length; ++i) 869 | getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; 870 | for (i = 0; i < options.yaxes.length; ++i) 871 | getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; 872 | 873 | // add hooks from options 874 | for (var n in hooks) 875 | if (options.hooks[n] && options.hooks[n].length) 876 | hooks[n] = hooks[n].concat(options.hooks[n]); 877 | 878 | executeHooks(hooks.processOptions, [options]); 879 | } 880 | 881 | function setData(d) { 882 | series = parseData(d); 883 | fillInSeriesOptions(); 884 | processData(); 885 | } 886 | 887 | function parseData(d) { 888 | var res = []; 889 | for (var i = 0; i < d.length; ++i) { 890 | var s = $.extend(true, {}, options.series); 891 | 892 | if (d[i].data != null) { 893 | s.data = d[i].data; // move the data instead of deep-copy 894 | delete d[i].data; 895 | 896 | $.extend(true, s, d[i]); 897 | 898 | d[i].data = s.data; 899 | } 900 | else 901 | s.data = d[i]; 902 | res.push(s); 903 | } 904 | 905 | return res; 906 | } 907 | 908 | function axisNumber(obj, coord) { 909 | var a = obj[coord + "axis"]; 910 | if (typeof a == "object") // if we got a real axis, extract number 911 | a = a.n; 912 | if (typeof a != "number") 913 | a = 1; // default to first axis 914 | return a; 915 | } 916 | 917 | function allAxes() { 918 | // return flat array without annoying null entries 919 | return $.grep(xaxes.concat(yaxes), function (a) { return a; }); 920 | } 921 | 922 | function canvasToAxisCoords(pos) { 923 | // return an object with x/y corresponding to all used axes 924 | var res = {}, i, axis; 925 | for (i = 0; i < xaxes.length; ++i) { 926 | axis = xaxes[i]; 927 | if (axis && axis.used) 928 | res["x" + axis.n] = axis.c2p(pos.left); 929 | } 930 | 931 | for (i = 0; i < yaxes.length; ++i) { 932 | axis = yaxes[i]; 933 | if (axis && axis.used) 934 | res["y" + axis.n] = axis.c2p(pos.top); 935 | } 936 | 937 | if (res.x1 !== undefined) 938 | res.x = res.x1; 939 | if (res.y1 !== undefined) 940 | res.y = res.y1; 941 | 942 | return res; 943 | } 944 | 945 | function axisToCanvasCoords(pos) { 946 | // get canvas coords from the first pair of x/y found in pos 947 | var res = {}, i, axis, key; 948 | 949 | for (i = 0; i < xaxes.length; ++i) { 950 | axis = xaxes[i]; 951 | if (axis && axis.used) { 952 | key = "x" + axis.n; 953 | if (pos[key] == null && axis.n == 1) 954 | key = "x"; 955 | 956 | if (pos[key] != null) { 957 | res.left = axis.p2c(pos[key]); 958 | break; 959 | } 960 | } 961 | } 962 | 963 | for (i = 0; i < yaxes.length; ++i) { 964 | axis = yaxes[i]; 965 | if (axis && axis.used) { 966 | key = "y" + axis.n; 967 | if (pos[key] == null && axis.n == 1) 968 | key = "y"; 969 | 970 | if (pos[key] != null) { 971 | res.top = axis.p2c(pos[key]); 972 | break; 973 | } 974 | } 975 | } 976 | 977 | return res; 978 | } 979 | 980 | function getOrCreateAxis(axes, number) { 981 | if (!axes[number - 1]) 982 | axes[number - 1] = { 983 | n: number, // save the number for future reference 984 | direction: axes == xaxes ? "x" : "y", 985 | options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) 986 | }; 987 | 988 | return axes[number - 1]; 989 | } 990 | 991 | function fillInSeriesOptions() { 992 | 993 | var neededColors = series.length, maxIndex = -1, i; 994 | 995 | // Subtract the number of series that already have fixed colors or 996 | // color indexes from the number that we still need to generate. 997 | 998 | for (i = 0; i < series.length; ++i) { 999 | var sc = series[i].color; 1000 | if (sc != null) { 1001 | neededColors--; 1002 | if (typeof sc == "number" && sc > maxIndex) { 1003 | maxIndex = sc; 1004 | } 1005 | } 1006 | } 1007 | 1008 | // If any of the series have fixed color indexes, then we need to 1009 | // generate at least as many colors as the highest index. 1010 | 1011 | if (neededColors <= maxIndex) { 1012 | neededColors = maxIndex + 1; 1013 | } 1014 | 1015 | // Generate all the colors, using first the option colors and then 1016 | // variations on those colors once they're exhausted. 1017 | 1018 | var c, colors = [], colorPool = options.colors, 1019 | colorPoolSize = colorPool.length, variation = 0; 1020 | 1021 | for (i = 0; i < neededColors; i++) { 1022 | 1023 | c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); 1024 | 1025 | // Each time we exhaust the colors in the pool we adjust 1026 | // a scaling factor used to produce more variations on 1027 | // those colors. The factor alternates negative/positive 1028 | // to produce lighter/darker colors. 1029 | 1030 | // Reset the variation after every few cycles, or else 1031 | // it will end up producing only white or black colors. 1032 | 1033 | if (i % colorPoolSize == 0 && i) { 1034 | if (variation >= 0) { 1035 | if (variation < 0.5) { 1036 | variation = -variation - 0.2; 1037 | } else variation = 0; 1038 | } else variation = -variation; 1039 | } 1040 | 1041 | colors[i] = c.scale('rgb', 1 + variation); 1042 | } 1043 | 1044 | // Finalize the series options, filling in their colors 1045 | 1046 | var colori = 0, s; 1047 | for (i = 0; i < series.length; ++i) { 1048 | s = series[i]; 1049 | 1050 | // assign colors 1051 | if (s.color == null) { 1052 | s.color = colors[colori].toString(); 1053 | ++colori; 1054 | } 1055 | else if (typeof s.color == "number") 1056 | s.color = colors[s.color].toString(); 1057 | 1058 | // turn on lines automatically in case nothing is set 1059 | if (s.lines.show == null) { 1060 | var v, show = true; 1061 | for (v in s) 1062 | if (s[v] && s[v].show) { 1063 | show = false; 1064 | break; 1065 | } 1066 | if (show) 1067 | s.lines.show = true; 1068 | } 1069 | 1070 | // If nothing was provided for lines.zero, default it to match 1071 | // lines.fill, since areas by default should extend to zero. 1072 | 1073 | if (s.lines.zero == null) { 1074 | s.lines.zero = !!s.lines.fill; 1075 | } 1076 | 1077 | // setup axes 1078 | s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); 1079 | s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); 1080 | } 1081 | } 1082 | 1083 | function processData() { 1084 | var topSentry = Number.POSITIVE_INFINITY, 1085 | bottomSentry = Number.NEGATIVE_INFINITY, 1086 | fakeInfinity = Number.MAX_VALUE, 1087 | i, j, k, m, length, 1088 | s, points, ps, x, y, axis, val, f, p, 1089 | data, format; 1090 | 1091 | function updateAxis(axis, min, max) { 1092 | if (min < axis.datamin && min != -fakeInfinity) 1093 | axis.datamin = min; 1094 | if (max > axis.datamax && max != fakeInfinity) 1095 | axis.datamax = max; 1096 | } 1097 | 1098 | $.each(allAxes(), function (_, axis) { 1099 | // init axis 1100 | axis.datamin = topSentry; 1101 | axis.datamax = bottomSentry; 1102 | axis.used = false; 1103 | }); 1104 | 1105 | for (i = 0; i < series.length; ++i) { 1106 | s = series[i]; 1107 | s.datapoints = { points: [] }; 1108 | 1109 | executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); 1110 | } 1111 | 1112 | // first pass: clean and copy data 1113 | for (i = 0; i < series.length; ++i) { 1114 | s = series[i]; 1115 | 1116 | data = s.data; 1117 | format = s.datapoints.format; 1118 | 1119 | if (!format) { 1120 | format = []; 1121 | // find out how to copy 1122 | format.push({ x: true, number: true, required: true }); 1123 | format.push({ y: true, number: true, required: true }); 1124 | 1125 | if (s.bars.show || (s.lines.show && s.lines.fill)) { 1126 | var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); 1127 | format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); 1128 | if (s.bars.horizontal) { 1129 | delete format[format.length - 1].y; 1130 | format[format.length - 1].x = true; 1131 | } 1132 | } 1133 | 1134 | s.datapoints.format = format; 1135 | } 1136 | 1137 | if (s.datapoints.pointsize != null) 1138 | continue; // already filled in 1139 | 1140 | s.datapoints.pointsize = format.length; 1141 | 1142 | ps = s.datapoints.pointsize; 1143 | points = s.datapoints.points; 1144 | 1145 | var insertSteps = s.lines.show && s.lines.steps; 1146 | s.xaxis.used = s.yaxis.used = true; 1147 | 1148 | for (j = k = 0; j < data.length; ++j, k += ps) { 1149 | p = data[j]; 1150 | 1151 | var nullify = p == null; 1152 | if (!nullify) { 1153 | for (m = 0; m < ps; ++m) { 1154 | val = p[m]; 1155 | f = format[m]; 1156 | 1157 | if (f) { 1158 | if (f.number && val != null) { 1159 | val = +val; // convert to number 1160 | if (isNaN(val)) 1161 | val = null; 1162 | else if (val == Infinity) 1163 | val = fakeInfinity; 1164 | else if (val == -Infinity) 1165 | val = -fakeInfinity; 1166 | } 1167 | 1168 | if (val == null) { 1169 | if (f.required) 1170 | nullify = true; 1171 | 1172 | if (f.defaultValue != null) 1173 | val = f.defaultValue; 1174 | } 1175 | } 1176 | 1177 | points[k + m] = val; 1178 | } 1179 | } 1180 | 1181 | if (nullify) { 1182 | for (m = 0; m < ps; ++m) { 1183 | val = points[k + m]; 1184 | if (val != null) { 1185 | f = format[m]; 1186 | // extract min/max info 1187 | if (f.autoscale !== false) { 1188 | if (f.x) { 1189 | updateAxis(s.xaxis, val, val); 1190 | } 1191 | if (f.y) { 1192 | updateAxis(s.yaxis, val, val); 1193 | } 1194 | } 1195 | } 1196 | points[k + m] = null; 1197 | } 1198 | } 1199 | else { 1200 | // a little bit of line specific stuff that 1201 | // perhaps shouldn't be here, but lacking 1202 | // better means... 1203 | if (insertSteps && k > 0 1204 | && points[k - ps] != null 1205 | && points[k - ps] != points[k] 1206 | && points[k - ps + 1] != points[k + 1]) { 1207 | // copy the point to make room for a middle point 1208 | for (m = 0; m < ps; ++m) 1209 | points[k + ps + m] = points[k + m]; 1210 | 1211 | // middle point has same y 1212 | points[k + 1] = points[k - ps + 1]; 1213 | 1214 | // we've added a point, better reflect that 1215 | k += ps; 1216 | } 1217 | } 1218 | } 1219 | } 1220 | 1221 | // give the hooks a chance to run 1222 | for (i = 0; i < series.length; ++i) { 1223 | s = series[i]; 1224 | 1225 | executeHooks(hooks.processDatapoints, [ s, s.datapoints]); 1226 | } 1227 | 1228 | // second pass: find datamax/datamin for auto-scaling 1229 | for (i = 0; i < series.length; ++i) { 1230 | s = series[i]; 1231 | points = s.datapoints.points; 1232 | ps = s.datapoints.pointsize; 1233 | format = s.datapoints.format; 1234 | 1235 | var xmin = topSentry, ymin = topSentry, 1236 | xmax = bottomSentry, ymax = bottomSentry; 1237 | 1238 | for (j = 0; j < points.length; j += ps) { 1239 | if (points[j] == null) 1240 | continue; 1241 | 1242 | for (m = 0; m < ps; ++m) { 1243 | val = points[j + m]; 1244 | f = format[m]; 1245 | if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) 1246 | continue; 1247 | 1248 | if (f.x) { 1249 | if (val < xmin) 1250 | xmin = val; 1251 | if (val > xmax) 1252 | xmax = val; 1253 | } 1254 | if (f.y) { 1255 | if (val < ymin) 1256 | ymin = val; 1257 | if (val > ymax) 1258 | ymax = val; 1259 | } 1260 | } 1261 | } 1262 | 1263 | if (s.bars.show) { 1264 | // make sure we got room for the bar on the dancing floor 1265 | var delta; 1266 | 1267 | switch (s.bars.align) { 1268 | case "left": 1269 | delta = 0; 1270 | break; 1271 | case "right": 1272 | delta = -s.bars.barWidth; 1273 | break; 1274 | default: 1275 | delta = -s.bars.barWidth / 2; 1276 | } 1277 | 1278 | if (s.bars.horizontal) { 1279 | ymin += delta; 1280 | ymax += delta + s.bars.barWidth; 1281 | } 1282 | else { 1283 | xmin += delta; 1284 | xmax += delta + s.bars.barWidth; 1285 | } 1286 | } 1287 | 1288 | updateAxis(s.xaxis, xmin, xmax); 1289 | updateAxis(s.yaxis, ymin, ymax); 1290 | } 1291 | 1292 | $.each(allAxes(), function (_, axis) { 1293 | if (axis.datamin == topSentry) 1294 | axis.datamin = null; 1295 | if (axis.datamax == bottomSentry) 1296 | axis.datamax = null; 1297 | }); 1298 | } 1299 | 1300 | function setupCanvases() { 1301 | 1302 | // Make sure the placeholder is clear of everything except canvases 1303 | // from a previous plot in this container that we'll try to re-use. 1304 | 1305 | placeholder.css("padding", 0) // padding messes up the positioning 1306 | .children().filter(function(){ 1307 | return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); 1308 | }).remove(); 1309 | 1310 | if (placeholder.css("position") == 'static') 1311 | placeholder.css("position", "relative"); // for positioning labels and overlay 1312 | 1313 | surface = new Canvas("flot-base", placeholder); 1314 | overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features 1315 | 1316 | ctx = surface.context; 1317 | octx = overlay.context; 1318 | 1319 | // define which element we're listening for events on 1320 | eventHolder = $(overlay.element).unbind(); 1321 | 1322 | // If we're re-using a plot object, shut down the old one 1323 | 1324 | var existing = placeholder.data("plot"); 1325 | 1326 | if (existing) { 1327 | existing.shutdown(); 1328 | overlay.clear(); 1329 | } 1330 | 1331 | // save in case we get replotted 1332 | placeholder.data("plot", plot); 1333 | } 1334 | 1335 | function bindEvents() { 1336 | // bind events 1337 | if (options.grid.hoverable) { 1338 | eventHolder.mousemove(onMouseMove); 1339 | 1340 | // Use bind, rather than .mouseleave, because we officially 1341 | // still support jQuery 1.2.6, which doesn't define a shortcut 1342 | // for mouseenter or mouseleave. This was a bug/oversight that 1343 | // was fixed somewhere around 1.3.x. We can return to using 1344 | // .mouseleave when we drop support for 1.2.6. 1345 | 1346 | eventHolder.bind("mouseleave", onMouseLeave); 1347 | } 1348 | 1349 | if (options.grid.clickable) 1350 | eventHolder.click(onClick); 1351 | 1352 | executeHooks(hooks.bindEvents, [eventHolder]); 1353 | } 1354 | 1355 | function shutdown() { 1356 | if (redrawTimeout) 1357 | clearTimeout(redrawTimeout); 1358 | 1359 | eventHolder.unbind("mousemove", onMouseMove); 1360 | eventHolder.unbind("mouseleave", onMouseLeave); 1361 | eventHolder.unbind("click", onClick); 1362 | 1363 | executeHooks(hooks.shutdown, [eventHolder]); 1364 | } 1365 | 1366 | function setTransformationHelpers(axis) { 1367 | // set helper functions on the axis, assumes plot area 1368 | // has been computed already 1369 | 1370 | function identity(x) { return x; } 1371 | 1372 | var s, m, t = axis.options.transform || identity, 1373 | it = axis.options.inverseTransform; 1374 | 1375 | // precompute how much the axis is scaling a point 1376 | // in canvas space 1377 | if (axis.direction == "x") { 1378 | s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); 1379 | m = Math.min(t(axis.max), t(axis.min)); 1380 | } 1381 | else { 1382 | s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); 1383 | s = -s; 1384 | m = Math.max(t(axis.max), t(axis.min)); 1385 | } 1386 | 1387 | // data point to canvas coordinate 1388 | if (t == identity) // slight optimization 1389 | axis.p2c = function (p) { return (p - m) * s; }; 1390 | else 1391 | axis.p2c = function (p) { return (t(p) - m) * s; }; 1392 | // canvas coordinate to data point 1393 | if (!it) 1394 | axis.c2p = function (c) { return m + c / s; }; 1395 | else 1396 | axis.c2p = function (c) { return it(m + c / s); }; 1397 | } 1398 | 1399 | function measureTickLabels(axis) { 1400 | 1401 | var opts = axis.options, 1402 | ticks = axis.ticks || [], 1403 | labelWidth = opts.labelWidth || 0, 1404 | labelHeight = opts.labelHeight || 0, 1405 | maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), 1406 | legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", 1407 | layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, 1408 | font = opts.font || "flot-tick-label tickLabel"; 1409 | 1410 | for (var i = 0; i < ticks.length; ++i) { 1411 | 1412 | var t = ticks[i]; 1413 | 1414 | if (!t.label) 1415 | continue; 1416 | 1417 | var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); 1418 | 1419 | labelWidth = Math.max(labelWidth, info.width); 1420 | labelHeight = Math.max(labelHeight, info.height); 1421 | } 1422 | 1423 | axis.labelWidth = opts.labelWidth || labelWidth; 1424 | axis.labelHeight = opts.labelHeight || labelHeight; 1425 | } 1426 | 1427 | function allocateAxisBoxFirstPhase(axis) { 1428 | // find the bounding box of the axis by looking at label 1429 | // widths/heights and ticks, make room by diminishing the 1430 | // plotOffset; this first phase only looks at one 1431 | // dimension per axis, the other dimension depends on the 1432 | // other axes so will have to wait 1433 | 1434 | var lw = axis.labelWidth, 1435 | lh = axis.labelHeight, 1436 | pos = axis.options.position, 1437 | isXAxis = axis.direction === "x", 1438 | tickLength = axis.options.tickLength, 1439 | axisMargin = options.grid.axisMargin, 1440 | padding = options.grid.labelMargin, 1441 | innermost = true, 1442 | outermost = true, 1443 | first = true, 1444 | found = false; 1445 | 1446 | // Determine the axis's position in its direction and on its side 1447 | 1448 | $.each(isXAxis ? xaxes : yaxes, function(i, a) { 1449 | if (a && (a.show || a.reserveSpace)) { 1450 | if (a === axis) { 1451 | found = true; 1452 | } else if (a.options.position === pos) { 1453 | if (found) { 1454 | outermost = false; 1455 | } else { 1456 | innermost = false; 1457 | } 1458 | } 1459 | if (!found) { 1460 | first = false; 1461 | } 1462 | } 1463 | }); 1464 | 1465 | // The outermost axis on each side has no margin 1466 | 1467 | if (outermost) { 1468 | axisMargin = 0; 1469 | } 1470 | 1471 | // The ticks for the first axis in each direction stretch across 1472 | 1473 | if (tickLength == null) { 1474 | tickLength = first ? "full" : 5; 1475 | } 1476 | 1477 | if (!isNaN(+tickLength)) 1478 | padding += +tickLength; 1479 | 1480 | if (isXAxis) { 1481 | lh += padding; 1482 | 1483 | if (pos == "bottom") { 1484 | plotOffset.bottom += lh + axisMargin; 1485 | axis.box = { top: surface.height - plotOffset.bottom, height: lh }; 1486 | } 1487 | else { 1488 | axis.box = { top: plotOffset.top + axisMargin, height: lh }; 1489 | plotOffset.top += lh + axisMargin; 1490 | } 1491 | } 1492 | else { 1493 | lw += padding; 1494 | 1495 | if (pos == "left") { 1496 | axis.box = { left: plotOffset.left + axisMargin, width: lw }; 1497 | plotOffset.left += lw + axisMargin; 1498 | } 1499 | else { 1500 | plotOffset.right += lw + axisMargin; 1501 | axis.box = { left: surface.width - plotOffset.right, width: lw }; 1502 | } 1503 | } 1504 | 1505 | // save for future reference 1506 | axis.position = pos; 1507 | axis.tickLength = tickLength; 1508 | axis.box.padding = padding; 1509 | axis.innermost = innermost; 1510 | } 1511 | 1512 | function allocateAxisBoxSecondPhase(axis) { 1513 | // now that all axis boxes have been placed in one 1514 | // dimension, we can set the remaining dimension coordinates 1515 | if (axis.direction == "x") { 1516 | axis.box.left = plotOffset.left - axis.labelWidth / 2; 1517 | axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; 1518 | } 1519 | else { 1520 | axis.box.top = plotOffset.top - axis.labelHeight / 2; 1521 | axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; 1522 | } 1523 | } 1524 | 1525 | function adjustLayoutForThingsStickingOut() { 1526 | // possibly adjust plot offset to ensure everything stays 1527 | // inside the canvas and isn't clipped off 1528 | 1529 | var minMargin = options.grid.minBorderMargin, 1530 | axis, i; 1531 | 1532 | // check stuff from the plot (FIXME: this should just read 1533 | // a value from the series, otherwise it's impossible to 1534 | // customize) 1535 | if (minMargin == null) { 1536 | minMargin = 0; 1537 | for (i = 0; i < series.length; ++i) 1538 | minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); 1539 | } 1540 | 1541 | var margins = { 1542 | left: minMargin, 1543 | right: minMargin, 1544 | top: minMargin, 1545 | bottom: minMargin 1546 | }; 1547 | 1548 | // check axis labels, note we don't check the actual 1549 | // labels but instead use the overall width/height to not 1550 | // jump as much around with replots 1551 | $.each(allAxes(), function (_, axis) { 1552 | if (axis.reserveSpace && axis.ticks && axis.ticks.length) { 1553 | if (axis.direction === "x") { 1554 | margins.left = Math.max(margins.left, axis.labelWidth / 2); 1555 | margins.right = Math.max(margins.right, axis.labelWidth / 2); 1556 | } else { 1557 | margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); 1558 | margins.top = Math.max(margins.top, axis.labelHeight / 2); 1559 | } 1560 | } 1561 | }); 1562 | 1563 | plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); 1564 | plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); 1565 | plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); 1566 | plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); 1567 | } 1568 | 1569 | function setupGrid() { 1570 | var i, axes = allAxes(), showGrid = options.grid.show; 1571 | 1572 | // Initialize the plot's offset from the edge of the canvas 1573 | 1574 | for (var a in plotOffset) { 1575 | var margin = options.grid.margin || 0; 1576 | plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; 1577 | } 1578 | 1579 | executeHooks(hooks.processOffset, [plotOffset]); 1580 | 1581 | // If the grid is visible, add its border width to the offset 1582 | 1583 | for (var a in plotOffset) { 1584 | if(typeof(options.grid.borderWidth) == "object") { 1585 | plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; 1586 | } 1587 | else { 1588 | plotOffset[a] += showGrid ? options.grid.borderWidth : 0; 1589 | } 1590 | } 1591 | 1592 | $.each(axes, function (_, axis) { 1593 | var axisOpts = axis.options; 1594 | axis.show = axisOpts.show == null ? axis.used : axisOpts.show; 1595 | axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; 1596 | setRange(axis); 1597 | }); 1598 | 1599 | if (showGrid) { 1600 | 1601 | var allocatedAxes = $.grep(axes, function (axis) { 1602 | return axis.show || axis.reserveSpace; 1603 | }); 1604 | 1605 | $.each(allocatedAxes, function (_, axis) { 1606 | // make the ticks 1607 | setupTickGeneration(axis); 1608 | setTicks(axis); 1609 | snapRangeToTicks(axis, axis.ticks); 1610 | // find labelWidth/Height for axis 1611 | measureTickLabels(axis); 1612 | }); 1613 | 1614 | // with all dimensions calculated, we can compute the 1615 | // axis bounding boxes, start from the outside 1616 | // (reverse order) 1617 | for (i = allocatedAxes.length - 1; i >= 0; --i) 1618 | allocateAxisBoxFirstPhase(allocatedAxes[i]); 1619 | 1620 | // make sure we've got enough space for things that 1621 | // might stick out 1622 | adjustLayoutForThingsStickingOut(); 1623 | 1624 | $.each(allocatedAxes, function (_, axis) { 1625 | allocateAxisBoxSecondPhase(axis); 1626 | }); 1627 | } 1628 | 1629 | plotWidth = surface.width - plotOffset.left - plotOffset.right; 1630 | plotHeight = surface.height - plotOffset.bottom - plotOffset.top; 1631 | 1632 | // now we got the proper plot dimensions, we can compute the scaling 1633 | $.each(axes, function (_, axis) { 1634 | setTransformationHelpers(axis); 1635 | }); 1636 | 1637 | if (showGrid) { 1638 | drawAxisLabels(); 1639 | } 1640 | 1641 | insertLegend(); 1642 | } 1643 | 1644 | function setRange(axis) { 1645 | var opts = axis.options, 1646 | min = +(opts.min != null ? opts.min : axis.datamin), 1647 | max = +(opts.max != null ? opts.max : axis.datamax), 1648 | delta = max - min; 1649 | 1650 | if (delta == 0.0) { 1651 | // degenerate case 1652 | var widen = max == 0 ? 1 : 0.01; 1653 | 1654 | if (opts.min == null) 1655 | min -= widen; 1656 | // always widen max if we couldn't widen min to ensure we 1657 | // don't fall into min == max which doesn't work 1658 | if (opts.max == null || opts.min != null) 1659 | max += widen; 1660 | } 1661 | else { 1662 | // consider autoscaling 1663 | var margin = opts.autoscaleMargin; 1664 | if (margin != null) { 1665 | if (opts.min == null) { 1666 | min -= delta * margin; 1667 | // make sure we don't go below zero if all values 1668 | // are positive 1669 | if (min < 0 && axis.datamin != null && axis.datamin >= 0) 1670 | min = 0; 1671 | } 1672 | if (opts.max == null) { 1673 | max += delta * margin; 1674 | if (max > 0 && axis.datamax != null && axis.datamax <= 0) 1675 | max = 0; 1676 | } 1677 | } 1678 | } 1679 | axis.min = min; 1680 | axis.max = max; 1681 | } 1682 | 1683 | function setupTickGeneration(axis) { 1684 | var opts = axis.options; 1685 | 1686 | // estimate number of ticks 1687 | var noTicks; 1688 | if (typeof opts.ticks == "number" && opts.ticks > 0) 1689 | noTicks = opts.ticks; 1690 | else 1691 | // heuristic based on the model a*sqrt(x) fitted to 1692 | // some data points that seemed reasonable 1693 | noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); 1694 | 1695 | var delta = (axis.max - axis.min) / noTicks, 1696 | dec = -Math.floor(Math.log(delta) / Math.LN10), 1697 | maxDec = opts.tickDecimals; 1698 | 1699 | if (maxDec != null && dec > maxDec) { 1700 | dec = maxDec; 1701 | } 1702 | 1703 | var magn = Math.pow(10, -dec), 1704 | norm = delta / magn, // norm is between 1.0 and 10.0 1705 | size; 1706 | 1707 | if (norm < 1.5) { 1708 | size = 1; 1709 | } else if (norm < 3) { 1710 | size = 2; 1711 | // special case for 2.5, requires an extra decimal 1712 | if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { 1713 | size = 2.5; 1714 | ++dec; 1715 | } 1716 | } else if (norm < 7.5) { 1717 | size = 5; 1718 | } else { 1719 | size = 10; 1720 | } 1721 | 1722 | size *= magn; 1723 | 1724 | if (opts.minTickSize != null && size < opts.minTickSize) { 1725 | size = opts.minTickSize; 1726 | } 1727 | 1728 | axis.delta = delta; 1729 | axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); 1730 | axis.tickSize = opts.tickSize || size; 1731 | 1732 | // Time mode was moved to a plug-in in 0.8, and since so many people use it 1733 | // we'll add an especially friendly reminder to make sure they included it. 1734 | 1735 | if (opts.mode == "time" && !axis.tickGenerator) { 1736 | throw new Error("Time mode requires the flot.time plugin."); 1737 | } 1738 | 1739 | // Flot supports base-10 axes; any other mode else is handled by a plug-in, 1740 | // like flot.time.js. 1741 | 1742 | if (!axis.tickGenerator) { 1743 | 1744 | axis.tickGenerator = function (axis) { 1745 | 1746 | var ticks = [], 1747 | start = floorInBase(axis.min, axis.tickSize), 1748 | i = 0, 1749 | v = Number.NaN, 1750 | prev; 1751 | 1752 | do { 1753 | prev = v; 1754 | v = start + i * axis.tickSize; 1755 | ticks.push(v); 1756 | ++i; 1757 | } while (v < axis.max && v != prev); 1758 | return ticks; 1759 | }; 1760 | 1761 | axis.tickFormatter = function (value, axis) { 1762 | 1763 | var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; 1764 | var formatted = "" + Math.round(value * factor) / factor; 1765 | 1766 | // If tickDecimals was specified, ensure that we have exactly that 1767 | // much precision; otherwise default to the value's own precision. 1768 | 1769 | if (axis.tickDecimals != null) { 1770 | var decimal = formatted.indexOf("."); 1771 | var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; 1772 | if (precision < axis.tickDecimals) { 1773 | return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); 1774 | } 1775 | } 1776 | 1777 | return formatted; 1778 | }; 1779 | } 1780 | 1781 | if ($.isFunction(opts.tickFormatter)) 1782 | axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; 1783 | 1784 | if (opts.alignTicksWithAxis != null) { 1785 | var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; 1786 | if (otherAxis && otherAxis.used && otherAxis != axis) { 1787 | // consider snapping min/max to outermost nice ticks 1788 | var niceTicks = axis.tickGenerator(axis); 1789 | if (niceTicks.length > 0) { 1790 | if (opts.min == null) 1791 | axis.min = Math.min(axis.min, niceTicks[0]); 1792 | if (opts.max == null && niceTicks.length > 1) 1793 | axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); 1794 | } 1795 | 1796 | axis.tickGenerator = function (axis) { 1797 | // copy ticks, scaled to this axis 1798 | var ticks = [], v, i; 1799 | for (i = 0; i < otherAxis.ticks.length; ++i) { 1800 | v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); 1801 | v = axis.min + v * (axis.max - axis.min); 1802 | ticks.push(v); 1803 | } 1804 | return ticks; 1805 | }; 1806 | 1807 | // we might need an extra decimal since forced 1808 | // ticks don't necessarily fit naturally 1809 | if (!axis.mode && opts.tickDecimals == null) { 1810 | var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), 1811 | ts = axis.tickGenerator(axis); 1812 | 1813 | // only proceed if the tick interval rounded 1814 | // with an extra decimal doesn't give us a 1815 | // zero at end 1816 | if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) 1817 | axis.tickDecimals = extraDec; 1818 | } 1819 | } 1820 | } 1821 | } 1822 | 1823 | function setTicks(axis) { 1824 | var oticks = axis.options.ticks, ticks = []; 1825 | if (oticks == null || (typeof oticks == "number" && oticks > 0)) 1826 | ticks = axis.tickGenerator(axis); 1827 | else if (oticks) { 1828 | if ($.isFunction(oticks)) 1829 | // generate the ticks 1830 | ticks = oticks(axis); 1831 | else 1832 | ticks = oticks; 1833 | } 1834 | 1835 | // clean up/labelify the supplied ticks, copy them over 1836 | var i, v; 1837 | axis.ticks = []; 1838 | for (i = 0; i < ticks.length; ++i) { 1839 | var label = null; 1840 | var t = ticks[i]; 1841 | if (typeof t == "object") { 1842 | v = +t[0]; 1843 | if (t.length > 1) 1844 | label = t[1]; 1845 | } 1846 | else 1847 | v = +t; 1848 | if (label == null) 1849 | label = axis.tickFormatter(v, axis); 1850 | if (!isNaN(v)) 1851 | axis.ticks.push({ v: v, label: label }); 1852 | } 1853 | } 1854 | 1855 | function snapRangeToTicks(axis, ticks) { 1856 | if (axis.options.autoscaleMargin && ticks.length > 0) { 1857 | // snap to ticks 1858 | if (axis.options.min == null) 1859 | axis.min = Math.min(axis.min, ticks[0].v); 1860 | if (axis.options.max == null && ticks.length > 1) 1861 | axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); 1862 | } 1863 | } 1864 | 1865 | function draw() { 1866 | 1867 | surface.clear(); 1868 | 1869 | executeHooks(hooks.drawBackground, [ctx]); 1870 | 1871 | var grid = options.grid; 1872 | 1873 | // draw background, if any 1874 | if (grid.show && grid.backgroundColor) 1875 | drawBackground(); 1876 | 1877 | if (grid.show && !grid.aboveData) { 1878 | drawGrid(); 1879 | } 1880 | 1881 | for (var i = 0; i < series.length; ++i) { 1882 | executeHooks(hooks.drawSeries, [ctx, series[i]]); 1883 | drawSeries(series[i]); 1884 | } 1885 | 1886 | executeHooks(hooks.draw, [ctx]); 1887 | 1888 | if (grid.show && grid.aboveData) { 1889 | drawGrid(); 1890 | } 1891 | 1892 | surface.render(); 1893 | 1894 | // A draw implies that either the axes or data have changed, so we 1895 | // should probably update the overlay highlights as well. 1896 | 1897 | triggerRedrawOverlay(); 1898 | } 1899 | 1900 | function extractRange(ranges, coord) { 1901 | var axis, from, to, key, axes = allAxes(); 1902 | 1903 | for (var i = 0; i < axes.length; ++i) { 1904 | axis = axes[i]; 1905 | if (axis.direction == coord) { 1906 | key = coord + axis.n + "axis"; 1907 | if (!ranges[key] && axis.n == 1) 1908 | key = coord + "axis"; // support x1axis as xaxis 1909 | if (ranges[key]) { 1910 | from = ranges[key].from; 1911 | to = ranges[key].to; 1912 | break; 1913 | } 1914 | } 1915 | } 1916 | 1917 | // backwards-compat stuff - to be removed in future 1918 | if (!ranges[key]) { 1919 | axis = coord == "x" ? xaxes[0] : yaxes[0]; 1920 | from = ranges[coord + "1"]; 1921 | to = ranges[coord + "2"]; 1922 | } 1923 | 1924 | // auto-reverse as an added bonus 1925 | if (from != null && to != null && from > to) { 1926 | var tmp = from; 1927 | from = to; 1928 | to = tmp; 1929 | } 1930 | 1931 | return { from: from, to: to, axis: axis }; 1932 | } 1933 | 1934 | function drawBackground() { 1935 | ctx.save(); 1936 | ctx.translate(plotOffset.left, plotOffset.top); 1937 | 1938 | ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); 1939 | ctx.fillRect(0, 0, plotWidth, plotHeight); 1940 | ctx.restore(); 1941 | } 1942 | 1943 | function drawGrid() { 1944 | var i, axes, bw, bc; 1945 | 1946 | ctx.save(); 1947 | ctx.translate(plotOffset.left, plotOffset.top); 1948 | 1949 | // draw markings 1950 | var markings = options.grid.markings; 1951 | if (markings) { 1952 | if ($.isFunction(markings)) { 1953 | axes = plot.getAxes(); 1954 | // xmin etc. is backwards compatibility, to be 1955 | // removed in the future 1956 | axes.xmin = axes.xaxis.min; 1957 | axes.xmax = axes.xaxis.max; 1958 | axes.ymin = axes.yaxis.min; 1959 | axes.ymax = axes.yaxis.max; 1960 | 1961 | markings = markings(axes); 1962 | } 1963 | 1964 | for (i = 0; i < markings.length; ++i) { 1965 | var m = markings[i], 1966 | xrange = extractRange(m, "x"), 1967 | yrange = extractRange(m, "y"); 1968 | 1969 | // fill in missing 1970 | if (xrange.from == null) 1971 | xrange.from = xrange.axis.min; 1972 | if (xrange.to == null) 1973 | xrange.to = xrange.axis.max; 1974 | if (yrange.from == null) 1975 | yrange.from = yrange.axis.min; 1976 | if (yrange.to == null) 1977 | yrange.to = yrange.axis.max; 1978 | 1979 | // clip 1980 | if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || 1981 | yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) 1982 | continue; 1983 | 1984 | xrange.from = Math.max(xrange.from, xrange.axis.min); 1985 | xrange.to = Math.min(xrange.to, xrange.axis.max); 1986 | yrange.from = Math.max(yrange.from, yrange.axis.min); 1987 | yrange.to = Math.min(yrange.to, yrange.axis.max); 1988 | 1989 | var xequal = xrange.from === xrange.to, 1990 | yequal = yrange.from === yrange.to; 1991 | 1992 | if (xequal && yequal) { 1993 | continue; 1994 | } 1995 | 1996 | // then draw 1997 | xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); 1998 | xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); 1999 | yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); 2000 | yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); 2001 | 2002 | if (xequal || yequal) { 2003 | var lineWidth = m.lineWidth || options.grid.markingsLineWidth, 2004 | subPixel = lineWidth % 2 ? 0.5 : 0; 2005 | ctx.beginPath(); 2006 | ctx.strokeStyle = m.color || options.grid.markingsColor; 2007 | ctx.lineWidth = lineWidth; 2008 | if (xequal) { 2009 | ctx.moveTo(xrange.to + subPixel, yrange.from); 2010 | ctx.lineTo(xrange.to + subPixel, yrange.to); 2011 | } else { 2012 | ctx.moveTo(xrange.from, yrange.to + subPixel); 2013 | ctx.lineTo(xrange.to, yrange.to + subPixel); 2014 | } 2015 | ctx.stroke(); 2016 | } else { 2017 | ctx.fillStyle = m.color || options.grid.markingsColor; 2018 | ctx.fillRect(xrange.from, yrange.to, 2019 | xrange.to - xrange.from, 2020 | yrange.from - yrange.to); 2021 | } 2022 | } 2023 | } 2024 | 2025 | // draw the ticks 2026 | axes = allAxes(); 2027 | bw = options.grid.borderWidth; 2028 | 2029 | for (var j = 0; j < axes.length; ++j) { 2030 | var axis = axes[j], box = axis.box, 2031 | t = axis.tickLength, x, y, xoff, yoff; 2032 | if (!axis.show || axis.ticks.length == 0) 2033 | continue; 2034 | 2035 | ctx.lineWidth = 1; 2036 | 2037 | // find the edges 2038 | if (axis.direction == "x") { 2039 | x = 0; 2040 | if (t == "full") 2041 | y = (axis.position == "top" ? 0 : plotHeight); 2042 | else 2043 | y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); 2044 | } 2045 | else { 2046 | y = 0; 2047 | if (t == "full") 2048 | x = (axis.position == "left" ? 0 : plotWidth); 2049 | else 2050 | x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); 2051 | } 2052 | 2053 | // draw tick bar 2054 | if (!axis.innermost) { 2055 | ctx.strokeStyle = axis.options.color; 2056 | ctx.beginPath(); 2057 | xoff = yoff = 0; 2058 | if (axis.direction == "x") 2059 | xoff = plotWidth + 1; 2060 | else 2061 | yoff = plotHeight + 1; 2062 | 2063 | if (ctx.lineWidth == 1) { 2064 | if (axis.direction == "x") { 2065 | y = Math.floor(y) + 0.5; 2066 | } else { 2067 | x = Math.floor(x) + 0.5; 2068 | } 2069 | } 2070 | 2071 | ctx.moveTo(x, y); 2072 | ctx.lineTo(x + xoff, y + yoff); 2073 | ctx.stroke(); 2074 | } 2075 | 2076 | // draw ticks 2077 | 2078 | ctx.strokeStyle = axis.options.tickColor; 2079 | 2080 | ctx.beginPath(); 2081 | for (i = 0; i < axis.ticks.length; ++i) { 2082 | var v = axis.ticks[i].v; 2083 | 2084 | xoff = yoff = 0; 2085 | 2086 | if (isNaN(v) || v < axis.min || v > axis.max 2087 | // skip those lying on the axes if we got a border 2088 | || (t == "full" 2089 | && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) 2090 | && (v == axis.min || v == axis.max))) 2091 | continue; 2092 | 2093 | if (axis.direction == "x") { 2094 | x = axis.p2c(v); 2095 | yoff = t == "full" ? -plotHeight : t; 2096 | 2097 | if (axis.position == "top") 2098 | yoff = -yoff; 2099 | } 2100 | else { 2101 | y = axis.p2c(v); 2102 | xoff = t == "full" ? -plotWidth : t; 2103 | 2104 | if (axis.position == "left") 2105 | xoff = -xoff; 2106 | } 2107 | 2108 | if (ctx.lineWidth == 1) { 2109 | if (axis.direction == "x") 2110 | x = Math.floor(x) + 0.5; 2111 | else 2112 | y = Math.floor(y) + 0.5; 2113 | } 2114 | 2115 | ctx.moveTo(x, y); 2116 | ctx.lineTo(x + xoff, y + yoff); 2117 | } 2118 | 2119 | ctx.stroke(); 2120 | } 2121 | 2122 | 2123 | // draw border 2124 | if (bw) { 2125 | // If either borderWidth or borderColor is an object, then draw the border 2126 | // line by line instead of as one rectangle 2127 | bc = options.grid.borderColor; 2128 | if(typeof bw == "object" || typeof bc == "object") { 2129 | if (typeof bw !== "object") { 2130 | bw = {top: bw, right: bw, bottom: bw, left: bw}; 2131 | } 2132 | if (typeof bc !== "object") { 2133 | bc = {top: bc, right: bc, bottom: bc, left: bc}; 2134 | } 2135 | 2136 | if (bw.top > 0) { 2137 | ctx.strokeStyle = bc.top; 2138 | ctx.lineWidth = bw.top; 2139 | ctx.beginPath(); 2140 | ctx.moveTo(0 - bw.left, 0 - bw.top/2); 2141 | ctx.lineTo(plotWidth, 0 - bw.top/2); 2142 | ctx.stroke(); 2143 | } 2144 | 2145 | if (bw.right > 0) { 2146 | ctx.strokeStyle = bc.right; 2147 | ctx.lineWidth = bw.right; 2148 | ctx.beginPath(); 2149 | ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); 2150 | ctx.lineTo(plotWidth + bw.right / 2, plotHeight); 2151 | ctx.stroke(); 2152 | } 2153 | 2154 | if (bw.bottom > 0) { 2155 | ctx.strokeStyle = bc.bottom; 2156 | ctx.lineWidth = bw.bottom; 2157 | ctx.beginPath(); 2158 | ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); 2159 | ctx.lineTo(0, plotHeight + bw.bottom / 2); 2160 | ctx.stroke(); 2161 | } 2162 | 2163 | if (bw.left > 0) { 2164 | ctx.strokeStyle = bc.left; 2165 | ctx.lineWidth = bw.left; 2166 | ctx.beginPath(); 2167 | ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); 2168 | ctx.lineTo(0- bw.left/2, 0); 2169 | ctx.stroke(); 2170 | } 2171 | } 2172 | else { 2173 | ctx.lineWidth = bw; 2174 | ctx.strokeStyle = options.grid.borderColor; 2175 | ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); 2176 | } 2177 | } 2178 | 2179 | ctx.restore(); 2180 | } 2181 | 2182 | function drawAxisLabels() { 2183 | 2184 | $.each(allAxes(), function (_, axis) { 2185 | var box = axis.box, 2186 | legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", 2187 | layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, 2188 | font = axis.options.font || "flot-tick-label tickLabel", 2189 | tick, x, y, halign, valign; 2190 | 2191 | // Remove text before checking for axis.show and ticks.length; 2192 | // otherwise plugins, like flot-tickrotor, that draw their own 2193 | // tick labels will end up with both theirs and the defaults. 2194 | 2195 | surface.removeText(layer); 2196 | 2197 | if (!axis.show || axis.ticks.length == 0) 2198 | return; 2199 | 2200 | for (var i = 0; i < axis.ticks.length; ++i) { 2201 | 2202 | tick = axis.ticks[i]; 2203 | if (!tick.label || tick.v < axis.min || tick.v > axis.max) 2204 | continue; 2205 | 2206 | if (axis.direction == "x") { 2207 | halign = "center"; 2208 | x = plotOffset.left + axis.p2c(tick.v); 2209 | if (axis.position == "bottom") { 2210 | y = box.top + box.padding; 2211 | } else { 2212 | y = box.top + box.height - box.padding; 2213 | valign = "bottom"; 2214 | } 2215 | } else { 2216 | valign = "middle"; 2217 | y = plotOffset.top + axis.p2c(tick.v); 2218 | if (axis.position == "left") { 2219 | x = box.left + box.width - box.padding; 2220 | halign = "right"; 2221 | } else { 2222 | x = box.left + box.padding; 2223 | } 2224 | } 2225 | 2226 | surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); 2227 | } 2228 | }); 2229 | } 2230 | 2231 | function drawSeries(series) { 2232 | if (series.lines.show) 2233 | drawSeriesLines(series); 2234 | if (series.bars.show) 2235 | drawSeriesBars(series); 2236 | if (series.points.show) 2237 | drawSeriesPoints(series); 2238 | } 2239 | 2240 | function drawSeriesLines(series) { 2241 | function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { 2242 | var points = datapoints.points, 2243 | ps = datapoints.pointsize, 2244 | prevx = null, prevy = null; 2245 | 2246 | ctx.beginPath(); 2247 | for (var i = ps; i < points.length; i += ps) { 2248 | var x1 = points[i - ps], y1 = points[i - ps + 1], 2249 | x2 = points[i], y2 = points[i + 1]; 2250 | 2251 | if (x1 == null || x2 == null) 2252 | continue; 2253 | 2254 | // clip with ymin 2255 | if (y1 <= y2 && y1 < axisy.min) { 2256 | if (y2 < axisy.min) 2257 | continue; // line segment is outside 2258 | // compute new intersection point 2259 | x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; 2260 | y1 = axisy.min; 2261 | } 2262 | else if (y2 <= y1 && y2 < axisy.min) { 2263 | if (y1 < axisy.min) 2264 | continue; 2265 | x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; 2266 | y2 = axisy.min; 2267 | } 2268 | 2269 | // clip with ymax 2270 | if (y1 >= y2 && y1 > axisy.max) { 2271 | if (y2 > axisy.max) 2272 | continue; 2273 | x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; 2274 | y1 = axisy.max; 2275 | } 2276 | else if (y2 >= y1 && y2 > axisy.max) { 2277 | if (y1 > axisy.max) 2278 | continue; 2279 | x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; 2280 | y2 = axisy.max; 2281 | } 2282 | 2283 | // clip with xmin 2284 | if (x1 <= x2 && x1 < axisx.min) { 2285 | if (x2 < axisx.min) 2286 | continue; 2287 | y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; 2288 | x1 = axisx.min; 2289 | } 2290 | else if (x2 <= x1 && x2 < axisx.min) { 2291 | if (x1 < axisx.min) 2292 | continue; 2293 | y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; 2294 | x2 = axisx.min; 2295 | } 2296 | 2297 | // clip with xmax 2298 | if (x1 >= x2 && x1 > axisx.max) { 2299 | if (x2 > axisx.max) 2300 | continue; 2301 | y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; 2302 | x1 = axisx.max; 2303 | } 2304 | else if (x2 >= x1 && x2 > axisx.max) { 2305 | if (x1 > axisx.max) 2306 | continue; 2307 | y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; 2308 | x2 = axisx.max; 2309 | } 2310 | 2311 | if (x1 != prevx || y1 != prevy) 2312 | ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); 2313 | 2314 | prevx = x2; 2315 | prevy = y2; 2316 | ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); 2317 | } 2318 | ctx.stroke(); 2319 | } 2320 | 2321 | function plotLineArea(datapoints, axisx, axisy) { 2322 | var points = datapoints.points, 2323 | ps = datapoints.pointsize, 2324 | bottom = Math.min(Math.max(0, axisy.min), axisy.max), 2325 | i = 0, top, areaOpen = false, 2326 | ypos = 1, segmentStart = 0, segmentEnd = 0; 2327 | 2328 | // we process each segment in two turns, first forward 2329 | // direction to sketch out top, then once we hit the 2330 | // end we go backwards to sketch the bottom 2331 | while (true) { 2332 | if (ps > 0 && i > points.length + ps) 2333 | break; 2334 | 2335 | i += ps; // ps is negative if going backwards 2336 | 2337 | var x1 = points[i - ps], 2338 | y1 = points[i - ps + ypos], 2339 | x2 = points[i], y2 = points[i + ypos]; 2340 | 2341 | if (areaOpen) { 2342 | if (ps > 0 && x1 != null && x2 == null) { 2343 | // at turning point 2344 | segmentEnd = i; 2345 | ps = -ps; 2346 | ypos = 2; 2347 | continue; 2348 | } 2349 | 2350 | if (ps < 0 && i == segmentStart + ps) { 2351 | // done with the reverse sweep 2352 | ctx.fill(); 2353 | areaOpen = false; 2354 | ps = -ps; 2355 | ypos = 1; 2356 | i = segmentStart = segmentEnd + ps; 2357 | continue; 2358 | } 2359 | } 2360 | 2361 | if (x1 == null || x2 == null) 2362 | continue; 2363 | 2364 | // clip x values 2365 | 2366 | // clip with xmin 2367 | if (x1 <= x2 && x1 < axisx.min) { 2368 | if (x2 < axisx.min) 2369 | continue; 2370 | y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; 2371 | x1 = axisx.min; 2372 | } 2373 | else if (x2 <= x1 && x2 < axisx.min) { 2374 | if (x1 < axisx.min) 2375 | continue; 2376 | y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; 2377 | x2 = axisx.min; 2378 | } 2379 | 2380 | // clip with xmax 2381 | if (x1 >= x2 && x1 > axisx.max) { 2382 | if (x2 > axisx.max) 2383 | continue; 2384 | y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; 2385 | x1 = axisx.max; 2386 | } 2387 | else if (x2 >= x1 && x2 > axisx.max) { 2388 | if (x1 > axisx.max) 2389 | continue; 2390 | y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; 2391 | x2 = axisx.max; 2392 | } 2393 | 2394 | if (!areaOpen) { 2395 | // open area 2396 | ctx.beginPath(); 2397 | ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); 2398 | areaOpen = true; 2399 | } 2400 | 2401 | // now first check the case where both is outside 2402 | if (y1 >= axisy.max && y2 >= axisy.max) { 2403 | ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); 2404 | ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); 2405 | continue; 2406 | } 2407 | else if (y1 <= axisy.min && y2 <= axisy.min) { 2408 | ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); 2409 | ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); 2410 | continue; 2411 | } 2412 | 2413 | // else it's a bit more complicated, there might 2414 | // be a flat maxed out rectangle first, then a 2415 | // triangular cutout or reverse; to find these 2416 | // keep track of the current x values 2417 | var x1old = x1, x2old = x2; 2418 | 2419 | // clip the y values, without shortcutting, we 2420 | // go through all cases in turn 2421 | 2422 | // clip with ymin 2423 | if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { 2424 | x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; 2425 | y1 = axisy.min; 2426 | } 2427 | else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { 2428 | x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; 2429 | y2 = axisy.min; 2430 | } 2431 | 2432 | // clip with ymax 2433 | if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { 2434 | x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; 2435 | y1 = axisy.max; 2436 | } 2437 | else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { 2438 | x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; 2439 | y2 = axisy.max; 2440 | } 2441 | 2442 | // if the x value was changed we got a rectangle 2443 | // to fill 2444 | if (x1 != x1old) { 2445 | ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); 2446 | // it goes to (x1, y1), but we fill that below 2447 | } 2448 | 2449 | // fill triangular section, this sometimes result 2450 | // in redundant points if (x1, y1) hasn't changed 2451 | // from previous line to, but we just ignore that 2452 | ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); 2453 | ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); 2454 | 2455 | // fill the other rectangle if it's there 2456 | if (x2 != x2old) { 2457 | ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); 2458 | ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); 2459 | } 2460 | } 2461 | } 2462 | 2463 | ctx.save(); 2464 | ctx.translate(plotOffset.left, plotOffset.top); 2465 | ctx.lineJoin = "round"; 2466 | 2467 | var lw = series.lines.lineWidth, 2468 | sw = series.shadowSize; 2469 | // FIXME: consider another form of shadow when filling is turned on 2470 | if (lw > 0 && sw > 0) { 2471 | // draw shadow as a thick and thin line with transparency 2472 | ctx.lineWidth = sw; 2473 | ctx.strokeStyle = "rgba(0,0,0,0.1)"; 2474 | // position shadow at angle from the mid of line 2475 | var angle = Math.PI/18; 2476 | plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); 2477 | ctx.lineWidth = sw/2; 2478 | plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); 2479 | } 2480 | 2481 | ctx.lineWidth = lw; 2482 | ctx.strokeStyle = series.color; 2483 | var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); 2484 | if (fillStyle) { 2485 | ctx.fillStyle = fillStyle; 2486 | plotLineArea(series.datapoints, series.xaxis, series.yaxis); 2487 | } 2488 | 2489 | if (lw > 0) 2490 | plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); 2491 | ctx.restore(); 2492 | } 2493 | 2494 | function drawSeriesPoints(series) { 2495 | function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { 2496 | var points = datapoints.points, ps = datapoints.pointsize; 2497 | 2498 | for (var i = 0; i < points.length; i += ps) { 2499 | var x = points[i], y = points[i + 1]; 2500 | if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) 2501 | continue; 2502 | 2503 | ctx.beginPath(); 2504 | x = axisx.p2c(x); 2505 | y = axisy.p2c(y) + offset; 2506 | if (symbol == "circle") 2507 | ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); 2508 | else 2509 | symbol(ctx, x, y, radius, shadow); 2510 | ctx.closePath(); 2511 | 2512 | if (fillStyle) { 2513 | ctx.fillStyle = fillStyle; 2514 | ctx.fill(); 2515 | } 2516 | ctx.stroke(); 2517 | } 2518 | } 2519 | 2520 | ctx.save(); 2521 | ctx.translate(plotOffset.left, plotOffset.top); 2522 | 2523 | var lw = series.points.lineWidth, 2524 | sw = series.shadowSize, 2525 | radius = series.points.radius, 2526 | symbol = series.points.symbol; 2527 | 2528 | // If the user sets the line width to 0, we change it to a very 2529 | // small value. A line width of 0 seems to force the default of 1. 2530 | // Doing the conditional here allows the shadow setting to still be 2531 | // optional even with a lineWidth of 0. 2532 | 2533 | if( lw == 0 ) 2534 | lw = 0.0001; 2535 | 2536 | if (lw > 0 && sw > 0) { 2537 | // draw shadow in two steps 2538 | var w = sw / 2; 2539 | ctx.lineWidth = w; 2540 | ctx.strokeStyle = "rgba(0,0,0,0.1)"; 2541 | plotPoints(series.datapoints, radius, null, w + w/2, true, 2542 | series.xaxis, series.yaxis, symbol); 2543 | 2544 | ctx.strokeStyle = "rgba(0,0,0,0.2)"; 2545 | plotPoints(series.datapoints, radius, null, w/2, true, 2546 | series.xaxis, series.yaxis, symbol); 2547 | } 2548 | 2549 | ctx.lineWidth = lw; 2550 | ctx.strokeStyle = series.color; 2551 | plotPoints(series.datapoints, radius, 2552 | getFillStyle(series.points, series.color), 0, false, 2553 | series.xaxis, series.yaxis, symbol); 2554 | ctx.restore(); 2555 | } 2556 | 2557 | function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { 2558 | var left, right, bottom, top, 2559 | drawLeft, drawRight, drawTop, drawBottom, 2560 | tmp; 2561 | 2562 | // in horizontal mode, we start the bar from the left 2563 | // instead of from the bottom so it appears to be 2564 | // horizontal rather than vertical 2565 | if (horizontal) { 2566 | drawBottom = drawRight = drawTop = true; 2567 | drawLeft = false; 2568 | left = b; 2569 | right = x; 2570 | top = y + barLeft; 2571 | bottom = y + barRight; 2572 | 2573 | // account for negative bars 2574 | if (right < left) { 2575 | tmp = right; 2576 | right = left; 2577 | left = tmp; 2578 | drawLeft = true; 2579 | drawRight = false; 2580 | } 2581 | } 2582 | else { 2583 | drawLeft = drawRight = drawTop = true; 2584 | drawBottom = false; 2585 | left = x + barLeft; 2586 | right = x + barRight; 2587 | bottom = b; 2588 | top = y; 2589 | 2590 | // account for negative bars 2591 | if (top < bottom) { 2592 | tmp = top; 2593 | top = bottom; 2594 | bottom = tmp; 2595 | drawBottom = true; 2596 | drawTop = false; 2597 | } 2598 | } 2599 | 2600 | // clip 2601 | if (right < axisx.min || left > axisx.max || 2602 | top < axisy.min || bottom > axisy.max) 2603 | return; 2604 | 2605 | if (left < axisx.min) { 2606 | left = axisx.min; 2607 | drawLeft = false; 2608 | } 2609 | 2610 | if (right > axisx.max) { 2611 | right = axisx.max; 2612 | drawRight = false; 2613 | } 2614 | 2615 | if (bottom < axisy.min) { 2616 | bottom = axisy.min; 2617 | drawBottom = false; 2618 | } 2619 | 2620 | if (top > axisy.max) { 2621 | top = axisy.max; 2622 | drawTop = false; 2623 | } 2624 | 2625 | left = axisx.p2c(left); 2626 | bottom = axisy.p2c(bottom); 2627 | right = axisx.p2c(right); 2628 | top = axisy.p2c(top); 2629 | 2630 | // fill the bar 2631 | if (fillStyleCallback) { 2632 | c.fillStyle = fillStyleCallback(bottom, top); 2633 | c.fillRect(left, top, right - left, bottom - top) 2634 | } 2635 | 2636 | // draw outline 2637 | if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { 2638 | c.beginPath(); 2639 | 2640 | // FIXME: inline moveTo is buggy with excanvas 2641 | c.moveTo(left, bottom); 2642 | if (drawLeft) 2643 | c.lineTo(left, top); 2644 | else 2645 | c.moveTo(left, top); 2646 | if (drawTop) 2647 | c.lineTo(right, top); 2648 | else 2649 | c.moveTo(right, top); 2650 | if (drawRight) 2651 | c.lineTo(right, bottom); 2652 | else 2653 | c.moveTo(right, bottom); 2654 | if (drawBottom) 2655 | c.lineTo(left, bottom); 2656 | else 2657 | c.moveTo(left, bottom); 2658 | c.stroke(); 2659 | } 2660 | } 2661 | 2662 | function drawSeriesBars(series) { 2663 | function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { 2664 | var points = datapoints.points, ps = datapoints.pointsize; 2665 | 2666 | for (var i = 0; i < points.length; i += ps) { 2667 | if (points[i] == null) 2668 | continue; 2669 | drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); 2670 | } 2671 | } 2672 | 2673 | ctx.save(); 2674 | ctx.translate(plotOffset.left, plotOffset.top); 2675 | 2676 | // FIXME: figure out a way to add shadows (for instance along the right edge) 2677 | ctx.lineWidth = series.bars.lineWidth; 2678 | ctx.strokeStyle = series.color; 2679 | 2680 | var barLeft; 2681 | 2682 | switch (series.bars.align) { 2683 | case "left": 2684 | barLeft = 0; 2685 | break; 2686 | case "right": 2687 | barLeft = -series.bars.barWidth; 2688 | break; 2689 | default: 2690 | barLeft = -series.bars.barWidth / 2; 2691 | } 2692 | 2693 | var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; 2694 | plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); 2695 | ctx.restore(); 2696 | } 2697 | 2698 | function getFillStyle(filloptions, seriesColor, bottom, top) { 2699 | var fill = filloptions.fill; 2700 | if (!fill) 2701 | return null; 2702 | 2703 | if (filloptions.fillColor) 2704 | return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); 2705 | 2706 | var c = $.color.parse(seriesColor); 2707 | c.a = typeof fill == "number" ? fill : 0.4; 2708 | c.normalize(); 2709 | return c.toString(); 2710 | } 2711 | 2712 | function insertLegend() { 2713 | 2714 | if (options.legend.container != null) { 2715 | $(options.legend.container).html(""); 2716 | } else { 2717 | placeholder.find(".legend").remove(); 2718 | } 2719 | 2720 | if (!options.legend.show) { 2721 | return; 2722 | } 2723 | 2724 | var fragments = [], entries = [], rowStarted = false, 2725 | lf = options.legend.labelFormatter, s, label; 2726 | 2727 | // Build a list of legend entries, with each having a label and a color 2728 | 2729 | for (var i = 0; i < series.length; ++i) { 2730 | s = series[i]; 2731 | if (s.label) { 2732 | label = lf ? lf(s.label, s) : s.label; 2733 | if (label) { 2734 | entries.push({ 2735 | label: label, 2736 | color: s.color 2737 | }); 2738 | } 2739 | } 2740 | } 2741 | 2742 | // Sort the legend using either the default or a custom comparator 2743 | 2744 | if (options.legend.sorted) { 2745 | if ($.isFunction(options.legend.sorted)) { 2746 | entries.sort(options.legend.sorted); 2747 | } else if (options.legend.sorted == "reverse") { 2748 | entries.reverse(); 2749 | } else { 2750 | var ascending = options.legend.sorted != "descending"; 2751 | entries.sort(function(a, b) { 2752 | return a.label == b.label ? 0 : ( 2753 | (a.label < b.label) != ascending ? 1 : -1 // Logical XOR 2754 | ); 2755 | }); 2756 | } 2757 | } 2758 | 2759 | // Generate markup for the list of entries, in their final order 2760 | 2761 | for (var i = 0; i < entries.length; ++i) { 2762 | 2763 | var entry = entries[i]; 2764 | 2765 | if (i % options.legend.noColumns == 0) { 2766 | if (rowStarted) 2767 | fragments.push(''); 2768 | fragments.push(''); 2769 | rowStarted = true; 2770 | } 2771 | 2772 | fragments.push( 2773 | '
' + 2774 | '' + entry.label + '' 2775 | ); 2776 | } 2777 | 2778 | if (rowStarted) 2779 | fragments.push(''); 2780 | 2781 | if (fragments.length == 0) 2782 | return; 2783 | 2784 | var table = '' + fragments.join("") + '
'; 2785 | if (options.legend.container != null) 2786 | $(options.legend.container).html(table); 2787 | else { 2788 | var pos = "", 2789 | p = options.legend.position, 2790 | m = options.legend.margin; 2791 | if (m[0] == null) 2792 | m = [m, m]; 2793 | if (p.charAt(0) == "n") 2794 | pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; 2795 | else if (p.charAt(0) == "s") 2796 | pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; 2797 | if (p.charAt(1) == "e") 2798 | pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; 2799 | else if (p.charAt(1) == "w") 2800 | pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; 2801 | var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); 2802 | if (options.legend.backgroundOpacity != 0.0) { 2803 | // put in the transparent background 2804 | // separately to avoid blended labels and 2805 | // label boxes 2806 | var c = options.legend.backgroundColor; 2807 | if (c == null) { 2808 | c = options.grid.backgroundColor; 2809 | if (c && typeof c == "string") 2810 | c = $.color.parse(c); 2811 | else 2812 | c = $.color.extract(legend, 'background-color'); 2813 | c.a = 1; 2814 | c = c.toString(); 2815 | } 2816 | var div = legend.children(); 2817 | $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); 2818 | } 2819 | } 2820 | } 2821 | 2822 | 2823 | // interactive features 2824 | 2825 | var highlights = [], 2826 | redrawTimeout = null; 2827 | 2828 | // returns the data item the mouse is over, or null if none is found 2829 | function findNearbyItem(mouseX, mouseY, seriesFilter) { 2830 | var maxDistance = options.grid.mouseActiveRadius, 2831 | smallestDistance = maxDistance * maxDistance + 1, 2832 | item = null, foundPoint = false, i, j, ps; 2833 | 2834 | for (i = series.length - 1; i >= 0; --i) { 2835 | if (!seriesFilter(series[i])) 2836 | continue; 2837 | 2838 | var s = series[i], 2839 | axisx = s.xaxis, 2840 | axisy = s.yaxis, 2841 | points = s.datapoints.points, 2842 | mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster 2843 | my = axisy.c2p(mouseY), 2844 | maxx = maxDistance / axisx.scale, 2845 | maxy = maxDistance / axisy.scale; 2846 | 2847 | ps = s.datapoints.pointsize; 2848 | // with inverse transforms, we can't use the maxx/maxy 2849 | // optimization, sadly 2850 | if (axisx.options.inverseTransform) 2851 | maxx = Number.MAX_VALUE; 2852 | if (axisy.options.inverseTransform) 2853 | maxy = Number.MAX_VALUE; 2854 | 2855 | if (s.lines.show || s.points.show) { 2856 | for (j = 0; j < points.length; j += ps) { 2857 | var x = points[j], y = points[j + 1]; 2858 | if (x == null) 2859 | continue; 2860 | 2861 | // For points and lines, the cursor must be within a 2862 | // certain distance to the data point 2863 | if (x - mx > maxx || x - mx < -maxx || 2864 | y - my > maxy || y - my < -maxy) 2865 | continue; 2866 | 2867 | // We have to calculate distances in pixels, not in 2868 | // data units, because the scales of the axes may be different 2869 | var dx = Math.abs(axisx.p2c(x) - mouseX), 2870 | dy = Math.abs(axisy.p2c(y) - mouseY), 2871 | dist = dx * dx + dy * dy; // we save the sqrt 2872 | 2873 | // use <= to ensure last point takes precedence 2874 | // (last generally means on top of) 2875 | if (dist < smallestDistance) { 2876 | smallestDistance = dist; 2877 | item = [i, j / ps]; 2878 | } 2879 | } 2880 | } 2881 | 2882 | if (s.bars.show && !item) { // no other point can be nearby 2883 | 2884 | var barLeft, barRight; 2885 | 2886 | switch (s.bars.align) { 2887 | case "left": 2888 | barLeft = 0; 2889 | break; 2890 | case "right": 2891 | barLeft = -s.bars.barWidth; 2892 | break; 2893 | default: 2894 | barLeft = -s.bars.barWidth / 2; 2895 | } 2896 | 2897 | barRight = barLeft + s.bars.barWidth; 2898 | 2899 | for (j = 0; j < points.length; j += ps) { 2900 | var x = points[j], y = points[j + 1], b = points[j + 2]; 2901 | if (x == null) 2902 | continue; 2903 | 2904 | // for a bar graph, the cursor must be inside the bar 2905 | if (series[i].bars.horizontal ? 2906 | (mx <= Math.max(b, x) && mx >= Math.min(b, x) && 2907 | my >= y + barLeft && my <= y + barRight) : 2908 | (mx >= x + barLeft && mx <= x + barRight && 2909 | my >= Math.min(b, y) && my <= Math.max(b, y))) 2910 | item = [i, j / ps]; 2911 | } 2912 | } 2913 | } 2914 | 2915 | if (item) { 2916 | i = item[0]; 2917 | j = item[1]; 2918 | ps = series[i].datapoints.pointsize; 2919 | 2920 | return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), 2921 | dataIndex: j, 2922 | series: series[i], 2923 | seriesIndex: i }; 2924 | } 2925 | 2926 | return null; 2927 | } 2928 | 2929 | function onMouseMove(e) { 2930 | if (options.grid.hoverable) 2931 | triggerClickHoverEvent("plothover", e, 2932 | function (s) { return s["hoverable"] != false; }); 2933 | } 2934 | 2935 | function onMouseLeave(e) { 2936 | if (options.grid.hoverable) 2937 | triggerClickHoverEvent("plothover", e, 2938 | function (s) { return false; }); 2939 | } 2940 | 2941 | function onClick(e) { 2942 | triggerClickHoverEvent("plotclick", e, 2943 | function (s) { return s["clickable"] != false; }); 2944 | } 2945 | 2946 | // trigger click or hover event (they send the same parameters 2947 | // so we share their code) 2948 | function triggerClickHoverEvent(eventname, event, seriesFilter) { 2949 | var offset = eventHolder.offset(), 2950 | canvasX = event.pageX - offset.left - plotOffset.left, 2951 | canvasY = event.pageY - offset.top - plotOffset.top, 2952 | pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); 2953 | 2954 | pos.pageX = event.pageX; 2955 | pos.pageY = event.pageY; 2956 | 2957 | var item = findNearbyItem(canvasX, canvasY, seriesFilter); 2958 | 2959 | if (item) { 2960 | // fill in mouse pos for any listeners out there 2961 | item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); 2962 | item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); 2963 | } 2964 | 2965 | if (options.grid.autoHighlight) { 2966 | // clear auto-highlights 2967 | for (var i = 0; i < highlights.length; ++i) { 2968 | var h = highlights[i]; 2969 | if (h.auto == eventname && 2970 | !(item && h.series == item.series && 2971 | h.point[0] == item.datapoint[0] && 2972 | h.point[1] == item.datapoint[1])) 2973 | unhighlight(h.series, h.point); 2974 | } 2975 | 2976 | if (item) 2977 | highlight(item.series, item.datapoint, eventname); 2978 | } 2979 | 2980 | placeholder.trigger(eventname, [ pos, item ]); 2981 | } 2982 | 2983 | function triggerRedrawOverlay() { 2984 | var t = options.interaction.redrawOverlayInterval; 2985 | if (t == -1) { // skip event queue 2986 | drawOverlay(); 2987 | return; 2988 | } 2989 | 2990 | if (!redrawTimeout) 2991 | redrawTimeout = setTimeout(drawOverlay, t); 2992 | } 2993 | 2994 | function drawOverlay() { 2995 | redrawTimeout = null; 2996 | 2997 | // draw highlights 2998 | octx.save(); 2999 | overlay.clear(); 3000 | octx.translate(plotOffset.left, plotOffset.top); 3001 | 3002 | var i, hi; 3003 | for (i = 0; i < highlights.length; ++i) { 3004 | hi = highlights[i]; 3005 | 3006 | if (hi.series.bars.show) 3007 | drawBarHighlight(hi.series, hi.point); 3008 | else 3009 | drawPointHighlight(hi.series, hi.point); 3010 | } 3011 | octx.restore(); 3012 | 3013 | executeHooks(hooks.drawOverlay, [octx]); 3014 | } 3015 | 3016 | function highlight(s, point, auto) { 3017 | if (typeof s == "number") 3018 | s = series[s]; 3019 | 3020 | if (typeof point == "number") { 3021 | var ps = s.datapoints.pointsize; 3022 | point = s.datapoints.points.slice(ps * point, ps * (point + 1)); 3023 | } 3024 | 3025 | var i = indexOfHighlight(s, point); 3026 | if (i == -1) { 3027 | highlights.push({ series: s, point: point, auto: auto }); 3028 | 3029 | triggerRedrawOverlay(); 3030 | } 3031 | else if (!auto) 3032 | highlights[i].auto = false; 3033 | } 3034 | 3035 | function unhighlight(s, point) { 3036 | if (s == null && point == null) { 3037 | highlights = []; 3038 | triggerRedrawOverlay(); 3039 | return; 3040 | } 3041 | 3042 | if (typeof s == "number") 3043 | s = series[s]; 3044 | 3045 | if (typeof point == "number") { 3046 | var ps = s.datapoints.pointsize; 3047 | point = s.datapoints.points.slice(ps * point, ps * (point + 1)); 3048 | } 3049 | 3050 | var i = indexOfHighlight(s, point); 3051 | if (i != -1) { 3052 | highlights.splice(i, 1); 3053 | 3054 | triggerRedrawOverlay(); 3055 | } 3056 | } 3057 | 3058 | function indexOfHighlight(s, p) { 3059 | for (var i = 0; i < highlights.length; ++i) { 3060 | var h = highlights[i]; 3061 | if (h.series == s && h.point[0] == p[0] 3062 | && h.point[1] == p[1]) 3063 | return i; 3064 | } 3065 | return -1; 3066 | } 3067 | 3068 | function drawPointHighlight(series, point) { 3069 | var x = point[0], y = point[1], 3070 | axisx = series.xaxis, axisy = series.yaxis, 3071 | highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); 3072 | 3073 | if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) 3074 | return; 3075 | 3076 | var pointRadius = series.points.radius + series.points.lineWidth / 2; 3077 | octx.lineWidth = pointRadius; 3078 | octx.strokeStyle = highlightColor; 3079 | var radius = 1.5 * pointRadius; 3080 | x = axisx.p2c(x); 3081 | y = axisy.p2c(y); 3082 | 3083 | octx.beginPath(); 3084 | if (series.points.symbol == "circle") 3085 | octx.arc(x, y, radius, 0, 2 * Math.PI, false); 3086 | else 3087 | series.points.symbol(octx, x, y, radius, false); 3088 | octx.closePath(); 3089 | octx.stroke(); 3090 | } 3091 | 3092 | function drawBarHighlight(series, point) { 3093 | var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), 3094 | fillStyle = highlightColor, 3095 | barLeft; 3096 | 3097 | switch (series.bars.align) { 3098 | case "left": 3099 | barLeft = 0; 3100 | break; 3101 | case "right": 3102 | barLeft = -series.bars.barWidth; 3103 | break; 3104 | default: 3105 | barLeft = -series.bars.barWidth / 2; 3106 | } 3107 | 3108 | octx.lineWidth = series.bars.lineWidth; 3109 | octx.strokeStyle = highlightColor; 3110 | 3111 | drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, 3112 | function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); 3113 | } 3114 | 3115 | function getColorOrGradient(spec, bottom, top, defaultColor) { 3116 | if (typeof spec == "string") 3117 | return spec; 3118 | else { 3119 | // assume this is a gradient spec; IE currently only 3120 | // supports a simple vertical gradient properly, so that's 3121 | // what we support too 3122 | var gradient = ctx.createLinearGradient(0, top, 0, bottom); 3123 | 3124 | for (var i = 0, l = spec.colors.length; i < l; ++i) { 3125 | var c = spec.colors[i]; 3126 | if (typeof c != "string") { 3127 | var co = $.color.parse(defaultColor); 3128 | if (c.brightness != null) 3129 | co = co.scale('rgb', c.brightness); 3130 | if (c.opacity != null) 3131 | co.a *= c.opacity; 3132 | c = co.toString(); 3133 | } 3134 | gradient.addColorStop(i / (l - 1), c); 3135 | } 3136 | 3137 | return gradient; 3138 | } 3139 | } 3140 | } 3141 | 3142 | // Add the plot function to the top level of the jQuery object 3143 | 3144 | $.plot = function(placeholder, data, options) { 3145 | //var t0 = new Date(); 3146 | var plot = new Plot($(placeholder), data, options, $.plot.plugins); 3147 | //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); 3148 | return plot; 3149 | }; 3150 | 3151 | $.plot.version = "0.8.3"; 3152 | 3153 | $.plot.plugins = []; 3154 | 3155 | // Also add the plot function as a chainable property 3156 | 3157 | $.fn.plot = function(data, options) { 3158 | return this.each(function() { 3159 | $.plot(this, data, options); 3160 | }); 3161 | }; 3162 | 3163 | // round to nearby lower multiple of base 3164 | function floorInBase(n, base) { 3165 | return base * Math.floor(n / base); 3166 | } 3167 | 3168 | })(jQuery); 3169 | -------------------------------------------------------------------------------- /src/Autoloader.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Hongbao; 13 | 14 | /** 15 | * Implements a lightweight PSR-0 compliant autoloader for Predis. 16 | * 17 | * @author Eric Naeseth 18 | * @author Daniele Alessandri 19 | */ 20 | class Autoloader 21 | { 22 | private $directory; 23 | private $prefix; 24 | private $prefixLength; 25 | 26 | /** 27 | * @param string $baseDirectory Base directory where the source files are located. 28 | */ 29 | public function __construct($baseDirectory = __DIR__) 30 | { 31 | $this->directory = $baseDirectory; 32 | $this->prefix = __NAMESPACE__.'\\'; 33 | $this->prefixLength = strlen($this->prefix); 34 | } 35 | 36 | /** 37 | * Registers the autoloader class with the PHP SPL autoloader. 38 | * 39 | * @param bool $prepend Prepend the autoloader on the stack instead of appending it. 40 | */ 41 | public static function register($prepend = false) 42 | { 43 | spl_autoload_register(array(new self(), 'autoload'), true, $prepend); 44 | } 45 | 46 | /** 47 | * Loads a class from a file using its fully qualified name. 48 | * 49 | * @param string $className Fully qualified name of a class. 50 | */ 51 | public function autoload($className) 52 | { 53 | if (0 === strpos($className, $this->prefix)) { 54 | $parts = explode('\\', substr($className, $this->prefixLength)); 55 | $filepath = $this->directory.DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $parts).'.php'; 56 | 57 | if (is_file($filepath)) { 58 | require $filepath; 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Contracts/HongbaoContract.php: -------------------------------------------------------------------------------- 1 | setOptions($options)->validate()->checkData(); 37 | } 38 | 39 | // 验证输入参数 40 | public function validate() 41 | { 42 | if ( (int)$this->total_number < 1) { 43 | throw new \Exception("红包总数必须是大于等于1的正整数"); 44 | } 45 | if ( strcmp((string)$this->total_money, (string)0.01) < 0 ) { 46 | throw new \Exception("红包总金额必须大于等于0.01"); 47 | } 48 | if ( strcmp((string)$this->val, (string)0.01) < 0 ) { 49 | throw new \Exception("单个红包金额必须大于等于0.01"); 50 | } 51 | return $this; 52 | } 53 | 54 | // 接受参数 55 | public function setOptions(array $options = []) 56 | { 57 | if (array_filter($options)) { 58 | foreach ( $options as $key => $option ) { 59 | if ( ! is_numeric($option) ) { 60 | throw new \Exception("{$key} 必须为数字"); 61 | } 62 | if ( isset($this->$key) ) { 63 | $this->$key = $option; 64 | } 65 | } 66 | } 67 | return $this; 68 | } 69 | 70 | // 验证数据有效性 71 | public function checkData() 72 | { 73 | if ( ($this->total_money / $this->total_number) < $this->val ) { 74 | throw new \Exception("设置的红包个数与总金额不满足单个红包金额{$this->val} 元的要求"); 75 | } 76 | return $this; 77 | } 78 | 79 | /** 80 | * 分页发红包 81 | * @brief [description] 82 | * @author zicai 83 | * @date 2018-01-29T15:19:36+080 84 | * 85 | * @return [array] 86 | */ 87 | public function create() 88 | { 89 | $current_page = 1; // 当前页 90 | $page_count = ceil( $this->total_number / $this->limit ); // 总页数 91 | $this->left_row_count = $this->total_number; // 剩余总条数 92 | $this->money_left = $this->total_money; // 剩余金额 93 | 94 | while ( $current_page <= $page_count ) { 95 | $data = []; 96 | $this->page_row_num = ($this->left_row_count - $this->limit) > 0 ? $this->limit : $this->left_row_count; // 当前页生成记录条数 97 | $data = $this->hb(); 98 | $stop = yield [ 'data' => $data, 'money_left' => $this->money_left]; 99 | if ($stop === true) { 100 | return; 101 | } 102 | 103 | $current_page++; 104 | } 105 | 106 | } 107 | 108 | //生成红包 109 | public function hb() 110 | { 111 | $data = []; 112 | while ($this->page_row_num > 0) { 113 | $data[] = $this->val; 114 | $this->page_row_num--; 115 | $this->left_row_count--; 116 | $this->money_left = bcsub($this->money_left, $this->val, 2); // 当前剩余金额 117 | } 118 | return $data; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Handlers/RandomHongbaoHandler.php: -------------------------------------------------------------------------------- 1 | setOptions($options)->validate()->checkData(); 55 | } 56 | 57 | // 验证输入参数 58 | public function validate() 59 | { 60 | if ( (int)$this->total_number < 1) { 61 | throw new \Exception("输入的红包总数必须是大于等于1的正整数"); 62 | } 63 | if ( strcmp((string)$this->total_money, (string)self::EPSILON) < 0 ) { 64 | throw new \Exception("输入的红包总金额必须大于等于".self::EPSILON); 65 | } 66 | if ( strcmp((string)$this->minimum_val, (string)self::EPSILON) < 0 ) { 67 | throw new \Exception("输入的单个红包金额最小值必须大于等于".self::EPSILON); 68 | } 69 | if ( strcmp((string)$this->maximum_val, (string)self::EPSILON) < 0 ) { 70 | throw new \Exception("输入的单个红包金额最大值必须大于等于".self::EPSILON); 71 | } 72 | return $this; 73 | } 74 | 75 | // 接受参数 76 | public function setOptions(array $options = []) 77 | { 78 | if (array_filter($options)) { 79 | foreach ( $options as $key => $option ) { 80 | if ( ! is_numeric($option) ) { 81 | throw new \Exception("{$key} 必须为数字"); 82 | } 83 | if ( isset($this->$key) ) { 84 | $this->$key = $option; 85 | } 86 | } 87 | } 88 | return $this; 89 | } 90 | 91 | // 验证数据有效性 92 | public function checkData() 93 | { 94 | if ( $this->maximum_val == $this->minimum_val ) { 95 | throw new \Exception("设置的红包金额最大值与最小值相等,不可使用随机生成"); 96 | } 97 | if ( ($this->total_money / $this->total_number) < $this->minimum_val ) { 98 | throw new \Exception("设置的红包个数与总金额不满足单个红包最小金额{$this->minimum_val} 元的要求"); 99 | } 100 | if ( $this->minimum_val > $this->maximum_val ) { 101 | throw new \Exception("设置的红包金额最小值不能大于最大值"); 102 | } 103 | return $this; 104 | } 105 | 106 | /** 107 | * 分页发红包 108 | * @brief [description] 109 | * @author zicai 110 | * @date 2018-01-29T15:19:36+080 111 | * 112 | * @return [array] 113 | */ 114 | public function create() 115 | { 116 | $this->current_page = 1; // 当前页 117 | $this->page_count = (int)ceil( $this->total_number / $this->limit ); // 总页数 118 | $this->left_row_count = $this->total_number; // 剩余总条数 119 | $this->money_left = $this->total_money; // 剩余金额 120 | 121 | while ( $this->current_page <= $this->page_count ) { 122 | $data = []; 123 | $this->page_row_num = ($this->left_row_count - $this->limit) > 0 ? $this->limit : $this->left_row_count; // 当前页生成记录条数 124 | $data = $this->hb(); 125 | $stop = yield [ 'data' => $data, 'money_left' => $this->money_left]; 126 | if ($stop === true) { 127 | return; 128 | } 129 | 130 | $this->current_page++; 131 | } 132 | 133 | } 134 | 135 | //生成红包 136 | public function hb() 137 | { 138 | $data = []; 139 | $mu = 0;//实时剩余金额均值 140 | $sigma = 0;//均值修正指数 141 | $noise_value = 0;//当前红包金额 142 | while ($this->page_row_num > 0 && $this->money_left > 0) { 143 | $this->money_left_avg = MathHelper::mbcsub($this->money_left, MathHelper::mbcmul($this->left_row_count, $this->minimum_val));//实时剩余金额平均值 144 | $mu = MathHelper::mbcdiv($this->money_left_avg, $this->left_row_count, 2); 145 | $sigma = MathHelper::mbcdiv($mu, 2); 146 | $noise_value = $this->gaussNoise($mu, $sigma); 147 | 148 | $val = MathHelper::mbcadd($noise_value, $this->minimum_val); 149 | $val = $val > $this->maximum_val ? $this->maximum_val : $val; 150 | $val = $val < $this->minimum_val ? $this->minimum_val : $val; 151 | $val = (string)round(MathHelper::mbcsub($this->money_left, $val), 2) < 0 ? $this->money_left : $val; 152 | 153 | $data[] = (string)round($val, 2); 154 | $this->money_left = (string)round(MathHelper::mbcsub($this->money_left, $val), 2); // 当前剩余金额 155 | 156 | $this->page_row_num--; 157 | $this->left_row_count--; // 更新剩余记录数 158 | } 159 | 160 | // 补齐数据 161 | if ( $this->current_page === $this->page_count ) { 162 | $data = $this->makeUp($data); 163 | } 164 | 165 | return $data; 166 | } 167 | 168 | function gaussNoise($mu, $sigma) 169 | { 170 | static $rand0; 171 | static $rand1; 172 | 173 | if (self::$generate) 174 | { 175 | self::$generate = false; 176 | return (string)round(MathHelper::mbcadd(MathHelper::mbcmul($rand1, $sigma), $mu), 2); 177 | } 178 | 179 | $u1 = 0; 180 | $u2 = 0; 181 | do 182 | { 183 | $u1 = mt_rand() * (1.0 / mt_getrandmax()); 184 | $u2 = mt_rand() * (1.0 / mt_getrandmax()); 185 | } while ($u1 <= self::EPSILON); 186 | 187 | $rand0 = sqrt(-2.0 * log($u1)) * cos(MathHelper::mbcmul(self::TOW_PI, $u2)); 188 | $rand1 = sqrt(-2.0 * log($u1)) * sin(MathHelper::mbcmul(self::TOW_PI, $u2)); 189 | self::$generate = true; 190 | 191 | return (string)round(MathHelper::mbcadd(MathHelper::mbcmul($rand0, $sigma), $mu), 2); 192 | } 193 | 194 | /** 195 | * 补齐数据 196 | * 197 | * @param array $data 198 | * @return array 199 | */ 200 | public function makeUp(array $data) :array 201 | { 202 | if ( ! empty($this->page_row_num) && $this->money_left != 0 ) { 203 | $avg_left = (string)round(MathHelper::mbcdiv($this->money_left, $this->page_row_num), 2); 204 | do { 205 | $index_min = array_search(min($data), $data); 206 | $data[$index_min] = (string)round(MathHelper::mbcadd($data[$index_min], $avg_left), 2); 207 | $this->page_row_num--; 208 | $this->money_left = (string)round(MathHelper::mbcsub($this->money_left, $avg_left), 2); 209 | } while ($this->page_row_num > 0); 210 | } 211 | 212 | if ( empty($this->page_row_num) && $this->money_left != 0 ) { 213 | $index_min = array_search(min($data), $data); 214 | $data[$index_min] = (string)round(MathHelper::mbcadd($data[$index_min], $this->money_left), 2); 215 | $this->money_left = "0"; 216 | } 217 | 218 | return $data; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/Hongbao.php: -------------------------------------------------------------------------------- 1 | handlers = $this->getHandlers(); 21 | } 22 | 23 | /** 24 | * 获取handler对象 25 | * @brief [description] 26 | * @author zicai 27 | * @date 2018-01-26T17:41:30+080 28 | * 29 | * @return [array] 30 | */ 31 | protected function getHandlers() 32 | { 33 | return array( 34 | 'fixedAmount' => 'Hongbao\Handlers\HongbaoHandler', // 固定红包 35 | 'randomAmount' => 'Hongbao\Handlers\RandomHongbaoHandler', // 随机红包 36 | ); 37 | } 38 | 39 | public function __call( string $name , array $args ) 40 | { 41 | if (isset($this->handlers[$name])) { 42 | $handler = $this->handlers[$name]; 43 | $handler = new $handler($args[0]); 44 | return $handler->create(); 45 | } 46 | 47 | throw new \Exception("{$name} 不存在"); 48 | } 49 | 50 | // 获取实例 51 | public static function getInstance() 52 | { 53 | if (null === self::$instance) { 54 | self::$instance = new self(); 55 | } 56 | return self::$instance; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Library/Helpers/MathHelper.php: -------------------------------------------------------------------------------- 1 | fixedAmount($options['baseConfig']); 32 | foreach ($hongbao as $result) { 33 | $hbs = array_merge($hbs, $result['data']); 34 | $money_left = $result['money_left']; 35 | } 36 | } catch (\Exception $e) { 37 | throw new Exception($e->getMessage()); 38 | } 39 | } 40 | 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /tests/TestCaseOnBase.php: -------------------------------------------------------------------------------- 1 | fixedAmount($options); 30 | foreach ($hongbao as $result) { 31 | $hbs = array_merge($hbs, $result['data']); 32 | $money_left = $result['money_left']; 33 | } 34 | } catch (\Exception $e) { 35 | $error = $e->getMessage(); 36 | } 37 | $this->assertCount($options['total_number'], $hbs); 38 | 39 | return [ 40 | 'data' => $hbs, 41 | 'money_left'=> $money_left 42 | ]; 43 | } 44 | 45 | /** 46 | * 测试验证总金额是否一致 47 | * 48 | * @depends testCheckCount 49 | */ 50 | public function testCheckTotal(array $data) 51 | { 52 | $total = $data['money_left']; 53 | $options = self::getParam('baseConfig',[]); 54 | foreach ($data['data'] as $hb) { 55 | $total = bcadd($hb, $total, 2); 56 | } 57 | $this->assertEquals($options['total_money'], $total); 58 | } 59 | 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /tests/TestCaseOnBaseRandom.php: -------------------------------------------------------------------------------- 1 | randomAmount($options); 37 | foreach ($hongbao as $result) { 38 | $hbs = array_merge($hbs, $result['data']); 39 | $money_left = $result['money_left']; 40 | } 41 | } catch (\Exception $e) { 42 | $error = $e->getMessage(); 43 | } 44 | $this->assertCount($options['total_number'], $hbs); 45 | 46 | return [ 47 | 'data' => $hbs, 48 | 'money_left'=> $money_left 49 | ]; 50 | } 51 | 52 | /** 53 | * 测试验证总金额是否一致 54 | * 55 | * @depends testCheckCount 56 | */ 57 | public function testCheckTotal(array $data) 58 | { 59 | $total_money = 0; 60 | $options = self::getParam('baseRandomConfig',[]); 61 | foreach ($data['data'] as $hb) { 62 | $total_money = bcadd($hb, $total_money, 2); 63 | } 64 | $this->assertEquals($options['total_money'], $total_money); 65 | } 66 | 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /tests/data/BaseConfig.php: -------------------------------------------------------------------------------- 1 | [//固定红包 5 | 'total_money' => 10000, // 总金额 6 | 'total_number' => 1009, // 总数量 7 | 'val' => 0.01, // 单个金额 8 | 'limit'=>100, //每次生成100个金额 9 | ], 10 | 'baseRandomConfig' => [//随机红包 11 | 'total_money' => 2000, // 总金额 12 | 'total_number' => 1009, // 总数量 13 | 'minimum_val' => 0.01, // 最小随机金额 14 | 'maximum_val' => 20, // 最大随机金额 15 | 'limit'=>100, //每次生成100个金额 16 | ], 17 | 'exception' => [ 18 | 'baseConfig' => [//固定红包 19 | 'total_money' => 10000, // 总金额 20 | 'total_number' => 10000, // 总数量 21 | 'val' => 0.001, // 单个金额 22 | 'limit'=>100, //每次生成100个金额 23 | ], 24 | 'baseRandomConfig' => [//随机红包 25 | 'total_money' => 2000, // 总金额 26 | 'total_number' => 3009, // 总数量 27 | 'minimum_val' => 20, // 最小随机金额 28 | 'maximum_val' => 20, // 最大随机金额 29 | 'limit'=>100, //每次生成100个金额 30 | ], 31 | ], 32 | ]; 33 | 34 | return $params; --------------------------------------------------------------------------------