├── .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 | 
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 |
104 |
105 |
106 |
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;
--------------------------------------------------------------------------------