├── .gitignore
├── .settings
└── org.eclipse.php.core.prefs
├── ABOUT.txt
├── README.markdown
├── TODO.txt
├── config
├── configure.php
├── metrics.php
└── tests.php
├── core.php
├── css
├── bg_button_a.gif
├── bg_button_span.gif
├── buttons.css
├── report.css
├── secondary-newer.png
├── secondary.png
└── secondary_light.png
├── example.php
├── js
├── flot
│ ├── API.txt
│ ├── FAQ.txt
│ ├── LICENSE.txt
│ ├── Makefile
│ ├── NEWS.txt
│ ├── PLUGINS.txt
│ ├── README.txt
│ ├── examples
│ │ ├── ajax.html
│ │ ├── annotating.html
│ │ ├── arrow-down.gif
│ │ ├── arrow-left.gif
│ │ ├── arrow-right.gif
│ │ ├── arrow-up.gif
│ │ ├── basic.html
│ │ ├── data-eu-gdp-growth-1.json
│ │ ├── data-eu-gdp-growth-2.json
│ │ ├── data-eu-gdp-growth-3.json
│ │ ├── data-eu-gdp-growth-4.json
│ │ ├── data-eu-gdp-growth-5.json
│ │ ├── data-eu-gdp-growth.json
│ │ ├── data-japan-gdp-growth.json
│ │ ├── data-usa-gdp-growth.json
│ │ ├── dual-axis.html
│ │ ├── graph-types.html
│ │ ├── hs-2004-27-a-large_web.jpg
│ │ ├── image.html
│ │ ├── index.html
│ │ ├── interacting.html
│ │ ├── layout.css
│ │ ├── navigate.html
│ │ ├── selection.html
│ │ ├── setting-options.html
│ │ ├── stacking.html
│ │ ├── thresholding.html
│ │ ├── time.html
│ │ ├── tracking.html
│ │ ├── turning-series.html
│ │ ├── visitors.html
│ │ └── zooming.html
│ ├── excanvas.js
│ ├── excanvas.min.js
│ ├── jquery.colorhelpers.js
│ ├── jquery.colorhelpers.min.js
│ ├── jquery.flot.crosshair.js
│ ├── jquery.flot.crosshair.min.js
│ ├── jquery.flot.image.js
│ ├── jquery.flot.image.min.js
│ ├── jquery.flot.js
│ ├── jquery.flot.min.js
│ ├── jquery.flot.navigate.js
│ ├── jquery.flot.navigate.min.js
│ ├── jquery.flot.selection.js
│ ├── jquery.flot.selection.min.js
│ ├── jquery.flot.stack.js
│ ├── jquery.flot.stack.min.js
│ ├── jquery.flot.threshold.js
│ ├── jquery.flot.threshold.min.js
│ ├── jquery.js
│ └── jquery.min.js
└── jquery-1.4.2.min.js
├── lib
├── common.php
├── metrics.php
├── report.php
├── tests.php
└── z_scores_table.php
├── redis
└── redis.php
└── report.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .svn
2 | .DS_Store
3 |
4 |
--------------------------------------------------------------------------------
/.settings/org.eclipse.php.core.prefs:
--------------------------------------------------------------------------------
1 | #Sat Feb 20 14:02:16 EST 2010
2 | eclipse.preferences.version=1
3 | include_path=0;/php_ab_testing_redis
4 |
--------------------------------------------------------------------------------
/ABOUT.txt:
--------------------------------------------------------------------------------
1 | Author:
2 | This project is by Ali Asaria created for Well.ca Inc. 2010. The idea for the program is based on the
3 | project "Vanity" by Assaf Arkin -- a Rails A/B testing tool.
4 |
5 | The philosophy behind this program is
6 |
7 |
8 | About Well.ca:
9 | Well.ca is Canada's largest online health and beauty store.
10 |
11 |
12 | License:
13 |
14 | - GPLv3
15 |
16 | Copyright Ali Asaria 2010
17 |
18 | ------------
19 | This program is free software: you can redistribute it and/or modify
20 | it under the terms of the GNU General Public License as published by
21 | the Free Software Foundation, either version 3 of the License, or
22 | (at your option) any later version.
23 |
24 | This program is distributed in the hope that it will be useful,
25 | but WITHOUT ANY WARRANTY; without even the implied warranty of
26 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27 | GNU General Public License for more details.
28 |
29 | You should have received a copy of the GNU General Public License
30 | along with this program. If not, see .
31 | ------------
32 |
33 |
34 | Credits:
35 |
36 | - Idea and style inspired by Vanity for Rails by Assaf Arkin http://vanity.labnotes.org/
37 | - See Assaf's credits here: http://vanity.labnotes.org/credits.html
38 | - Redis client library - Redis PHP Bindings - http://code.google.com/p/redis/ - Copyright 2009 Ludovico Magnocavallo, Copyright 2009 Salvatore Sanfilippo - Released under the same license as Redis.
39 | - jQuery by John Resig - http://jquery.com/ - MIT License or (GPL) Version 2.
40 | - Flot copyright of IOLA and Ole Laursen, released under the MIT license.
41 | - Buttons http://www.halmatferello.com/lab/pure-css-buttons/ Licensed under GPL and MIT.
42 |
43 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | A/B TESTING FOR PHP USING REDIS
2 | ===============================
3 |
4 | Introduction
5 | ------------
6 |
7 | PHP A/B Testing with Redis is code you can include in your PHP web application to A/B test
8 |
9 | To set up this project:
10 |
11 | 1. Start up redis. Specify the host name and db number in config/configure.php
12 | 2. Define things to measure in config/metrics.php following the declaration pattern in the file's example
13 | 3. Define the tests you'd like to perform in config/tests.php following the pattern there. Specify a metric for each test as shown in the example
14 | 4. include core.php in your code.
15 | 5. make sure to set ab_participant_specify_id("a_unique_id_for_this_user") at least once
16 | 6. for every metric, call: ab_track("name_of_your_metric");
17 | 7. every time you need a choice, call: ab_test("name_of_your_ab_test"); and it will return a string represing the alternative to use
18 |
19 | that is all.
--------------------------------------------------------------------------------
/TODO.txt:
--------------------------------------------------------------------------------
1 |
2 | - Add author and credit information
3 | - Add IP restriction on report.php
4 | - Have metrics that can have size, and related results that would be distributions : E.g. # of Pages viewed. This would be more complicated.
5 | - Fix date_time issues where I just use unicode time and not proper date math (can't find docs for date match in PHP 5.2)
6 | - Allow alternatives and tests that have spaces in their titles (!)
7 |
8 | Suggestions
9 | - Have an option on a metric so that it is only initialized upon request
10 | - Have a flag where this runs without redis for debugging (e.g. file-based or sqlite)
11 | - We can save A LOT of space if we don't add members to the set of participants in line 55 of tests.php, but instead just incr a number
--------------------------------------------------------------------------------
/config/configure.php:
--------------------------------------------------------------------------------
1 | "conversion", //this must be the same as the key above. Whitespace is not allowed
8 | "description" => "When someone completes checkout"
9 | );
10 | */
11 |
12 | $ab_metrics['conversion'] = array(
13 | "name" => "conversion",
14 | "description" => "When someone completes checkout"
15 | );
16 |
17 | $ab_metrics['signup'] = array(
18 | "name" => "signup",
19 | "description" => "When someone signsup for a newsletter"
20 | );
--------------------------------------------------------------------------------
/config/tests.php:
--------------------------------------------------------------------------------
1 | "checkout_button_color", //must be the same as the key above. Whitespace is not allowed
7 | "description" => "What colour should we make the checkout button",
8 | "alternatives" => array("green", "red", "blue"), //all possible alternatives
9 | //as an array of strings
10 | "metrics" => array('conversion') //what metrics refer to a conversion here? this is an array of strings
11 | //that correspond to metrics in the config/metrics file
12 | );
13 | */
14 |
15 | $ab_tests['checkout_button_color'] = array(
16 | "name" => "checkout_button_color",
17 | "description" => "What colour should we make the checkout button",
18 | "alternatives" => array("green", "red", "blue"),
19 | "metrics" => array('conversion', 'signup')
20 | );
21 |
22 | $ab_tests['checkout_button_size'] = array(
23 | "name" => "checkout_button_size",
24 | "description" => "How big should we make it?",
25 | "alternatives" => array("small", "medium", "large"),
26 | "metrics" => array('conversion')
27 | );
--------------------------------------------------------------------------------
/core.php:
--------------------------------------------------------------------------------
1 | connect();
96 |
97 | if ($redis_connected)
98 | {
99 | $r->select_db($ab_config['redis_db_number']);
100 |
101 | //set up the metrics (this should loop through them and link them to associated ab tests
102 | ab_metrics_initialize();
103 | ab_tests_initialize();
104 |
105 | }
106 | }
107 | }
108 |
109 | //deprecated -- do not use this function. replaced by init
110 | //this function will be dropped in the next release
111 | function ab_participant_id ($id)
112 | {
113 | trigger_error("ab_test - function ab_participant_id() is DEPRECATED. please us ab_init() instead.", E_USER_WARNING);
114 | ab_init($id, false);
115 | }
116 |
117 | /**
118 | * Runs a test.
119 | * @param $test is a string that specifieds the test to run
120 | * @return a string representing the alternative to run. will return null if the test isn't found
121 | */
122 | function ab_test($test)
123 | {
124 | global $redis_connected;
125 | global $ab_tests;
126 |
127 | //test if the test exists, otherwise return null
128 | if (!array_key_exists($test, $ab_tests)) return null;
129 |
130 | if ($redis_connected)
131 | {
132 | return ab_tests_test($test);
133 | }
134 | else
135 | {
136 | //the following function will still work, even without
137 | //a connection to redis
138 | return ab_tests_test($test);
139 | }
140 | }
141 |
142 | /**
143 | * Track a metric.
144 | * @param $metric : a string representing the metric to track
145 | * @param $value : (optional) how many conversions happened (e.g. use this
146 | * for add to cart if the person adds 10 to the cart) default = 1
147 | * @return nothing
148 | */
149 | function ab_track($metric, $value = 1)
150 | {
151 | global $redis_connected, $ab_participant_id;
152 |
153 | if ($redis_connected)
154 | {
155 | if ($ab_participant_id != -1)
156 | {
157 | ab_metrics_track($metric, $value);
158 | }
159 | }
160 | else
161 | {
162 | //do nothing
163 | }
164 | }
165 |
166 |
167 |
168 |
169 |
170 |
--------------------------------------------------------------------------------
/css/bg_button_a.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliasaria/PHP-redis-A-B-Testing/091515ab6304781bbb99dda215b9b53cf1a4532b/css/bg_button_a.gif
--------------------------------------------------------------------------------
/css/bg_button_span.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliasaria/PHP-redis-A-B-Testing/091515ab6304781bbb99dda215b9b53cf1a4532b/css/bg_button_span.gif
--------------------------------------------------------------------------------
/css/buttons.css:
--------------------------------------------------------------------------------
1 | /*
2 | Pure CSS Buttons.
3 | Learn more ~ http://www.halmatferello.com/lab/pure-css-buttons/
4 |
5 | Licensed under GPL and MIT.
6 | */
7 |
8 | .pcb, .pcb span {
9 | background: url('secondary_light.png') no-repeat;
10 | height: 23px;
11 | line-height: 23px;
12 | padding: 3px 0 7px 0;
13 |
14 | font-family: "Helvetica Neue", Helvetica, Frutiger, "Frutiger Linotype", Univers, Calibri, "Myriad Pro", Myriad, "DejaVu Sans Condensed", "Liberation Sans", "Nimbus Sans L", Arial, sans-serif;
15 | xfont-size: 62.5%;
16 | color: #333;
17 | }
18 |
19 | .pcb, a.pcb:link, a.pcb:visited {
20 | color: #333;
21 | font-size: 11px;
22 | padding-left: 14px;
23 | text-decoration: none !important;
24 | }
25 | /* ie 6 hack */
26 | * html div#frame .pcb {
27 | color: #333;
28 | padding-top: 0px;
29 | padding-bottom: 0px;
30 | text-decoration: none;
31 | }
32 | /* ie 7 hack */
33 | *:first-child+html .pcb {
34 | color: #333;
35 | padding-top: 0px;
36 | padding-bottom: 0px;
37 | text-decoration: none;
38 | }
39 |
40 | .pcb span {
41 | background-position: right -326px;
42 | padding-right: 14px;
43 | }
44 |
45 | a.green-button, a.green-button:link, a.green-button:visited, .green-active-button, .green-disabled-button {
46 | color: #fff !important;
47 | font-size: 12px;
48 | font-weight: bold;
49 | }
50 | a.green-button:hover {
51 | background-position: left -27px;
52 | }
53 | a.green-button:hover span {
54 | background-position: right -353px;
55 | }
56 | a.green-button:active, .green-active-button {
57 | background-position: left -54px;
58 | }
59 | a.green-button:active span, .green-active-button span {
60 | background-position: right -380px;
61 | }
62 | body .green-disabled-button {
63 | color: #A8BE69 !important;
64 | background-position: left -81px !important;
65 | }
66 | body .green-disabled-button span {
67 | background-position: right -407px;
68 | }
69 |
70 | a.grey-button {
71 | background-position: left -219px;
72 | padding-top: 3px;
73 | }
74 | a.grey-button span {
75 | background-position: right -545px;
76 | padding-top: 3px;
77 | }
78 | a.grey-button:hover {
79 | background-position: left -246px;
80 | }
81 | a.grey-button:hover span {
82 | background-position: right -572px;
83 | }
84 | a.grey-button:active, .grey-active-button {
85 | background-position: left -273px;
86 | }
87 | a.grey-button:active span, .grey-active-button span {
88 | background-position: right -599px;
89 | }
90 | body .grey-disabled-button {
91 | background-position: left -300px;
92 | color: #bbb !important;
93 | }
94 | body .grey-disabled-button span {
95 | background-position: right -626px;
96 | }
97 |
98 | a.red-button, .red-active-button, .red-disabled-button {
99 | background-position: left -109px;
100 | color: #fff !important;
101 | padding-top: 3px;
102 | font-weight: bold;
103 | }
104 | a.red-button span {
105 | background-position: right -435px;
106 | padding-top: 3px;
107 | }
108 | a.red-button:hover {
109 | background-position: left -137px;
110 | }
111 | a.red-button:hover span {
112 | background-position: right -463px;
113 | }
114 | a.red-button:active, .red-active-button {
115 | background-position: left -165px;
116 | }
117 | a.red-button:active span, .red-active-button span {
118 | background-position: right -491px;
119 | }
120 | body .red-disabled-button {
121 | background-position: left -192px;
122 | color: #DC4143 !important;
123 | }
124 | body .red-disabled-button span {
125 | background-position: right -518px;
126 | }
--------------------------------------------------------------------------------
/css/report.css:
--------------------------------------------------------------------------------
1 | @CHARSET "UTF-8";
2 |
3 |
4 | body { xfont-family: Zapfino, cursive; font-family: “Helvetica Neue”, Helvetica, Arial, sans-serif}
5 | xh1 { padding-left: 0.5em; font-family: 'HelveticaNeue-UltraLight', 'Helvetica Neue UltraLight', 'Helvetica Neue', Arial, Helvetica, sans-serif; font-size: 24px; font-weight: 100; letter-spacing: 1px; color: #eee; background-color: #333;}
6 |
7 | h1 { padding-left: 0.5em; font-family: Zapfino, cursive; font-size: 24px; font-weight: 100; letter-spacing: 1px; color: #eee; background-color: #333;}
8 | h2 { padding-left: 0px; font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif; font-size: 50px; font-weight: bold; letter-spacing: -6px; color: #999; margin: 0}
9 | h3 { margin: 0; padding: 0; padding-top: 20px; font-family: Zapfino, cursive; line-height: 1.1em; font-weight: normal; font-size: 24px;}
10 | p { margin: 0; padding: 0; padding-top: 5px; font-family: Zapfino, cursive; line-height: 1.1em;}
11 |
12 | hr { border: 0px solid white; border-top: 2px dotted #666;
13 | color: white;
14 | height: 1px;}
15 |
16 | .indent {padding-left: 1em}
17 | .results { margin: 0; padding: 0; padding-left: 1em; font-family: Gotham, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; font-weight: normal; letter-spacing: -0px; color: #444; line-height: 1.1em}
18 | .alternative { text-decoration: underline; color: #666; }
19 | .alternatives { xfloat: left; }
20 |
21 | .clear { /* generic container (i.e. div) for floating buttons */
22 | overflow: hidden;
23 | width: 100%;
24 | }
25 |
26 | a.button {
27 | background: transparent url('bg_button_a.gif') no-repeat scroll top right;
28 | color: #444;
29 | display: block;
30 | float: left;
31 | font: normal 12px arial, sans-serif;
32 | height: 24px;
33 | margin-right: 6px;
34 | padding-right: 18px; /* sliding doors padding */
35 | text-decoration: none;
36 | }
37 |
38 | a.button span {
39 | background: transparent url('bg_button_span.gif') no-repeat;
40 | display: block;
41 | line-height: 14px;
42 | padding: 5px 0 5px 18px;
43 | }
44 |
45 | a.button:active {
46 | background-position: bottom right;
47 | color: #000;
48 | outline: none; /* hide dotted outline in Firefox */
49 | }
50 |
51 | a.button:active span {
52 | background-position: bottom left;
53 | padding: 6px 0 4px 18px; /* push text down 1px */
54 | }
55 |
56 | .forced-text {
57 | font-family: “Helvetica Neue”, Helvetica, Arial, sans-serif;
58 | font-weight: normal;
59 | font-size: 12px;
60 | color: #999;
61 | }
--------------------------------------------------------------------------------
/css/secondary-newer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliasaria/PHP-redis-A-B-Testing/091515ab6304781bbb99dda215b9b53cf1a4532b/css/secondary-newer.png
--------------------------------------------------------------------------------
/css/secondary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliasaria/PHP-redis-A-B-Testing/091515ab6304781bbb99dda215b9b53cf1a4532b/css/secondary.png
--------------------------------------------------------------------------------
/css/secondary_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliasaria/PHP-redis-A-B-Testing/091515ab6304781bbb99dda215b9b53cf1a4532b/css/secondary_light.png
--------------------------------------------------------------------------------
/example.php:
--------------------------------------------------------------------------------
1 | Success: Conversion Tracked!";
20 |
21 | exit;
22 | }
23 |
24 |
25 | //STEP 2: DO THE TEST BY CALLING ab_test(..)
26 | ?>
27 |
28 |
29 |
30 |
31 | Checkout Button Color Test
32 |
33 |
34 |
54 |
55 |
--------------------------------------------------------------------------------
/js/flot/FAQ.txt:
--------------------------------------------------------------------------------
1 | Frequently asked questions
2 | --------------------------
3 |
4 | Q: How much data can Flot cope with?
5 |
6 | A: Flot will happily draw everything you send to it so the answer
7 | depends on the browser. The excanvas emulation used for IE (built with
8 | VML) makes IE by far the slowest browser so be sure to test with that
9 | if IE users are in your target group.
10 |
11 | 1000 points is not a problem, but as soon as you start having more
12 | points than the pixel width, you should probably start thinking about
13 | downsampling/aggregation as this is near the resolution limit of the
14 | chart anyway. If you downsample server-side, you also save bandwidth.
15 |
16 |
17 | Q: Flot isn't working when I'm using JSON data as source!
18 |
19 | A: Actually, Flot loves JSON data, you just got the format wrong.
20 | Double check that you're not inputting strings instead of numbers,
21 | like [["0", "-2.13"], ["5", "4.3"]]. This is most common mistake, and
22 | the error might not show up immediately because Javascript can do some
23 | conversion automatically.
24 |
25 |
26 | Q: Can I export the graph?
27 |
28 | A: This is a limitation of the canvas technology. There's a hook in
29 | the canvas object for getting an image out, but you won't get the tick
30 | labels. And it's not likely to be supported by IE. At this point, your
31 | best bet is probably taking a screenshot, e.g. with PrtScn.
32 |
33 |
34 | Q: The bars are all tiny in time mode?
35 |
36 | A: It's not really possible to determine the bar width automatically.
37 | So you have to set the width with the barWidth option which is NOT in
38 | pixels, but in the units of the x axis (or the y axis for horizontal
39 | bars). For time mode that's milliseconds so the default value of 1
40 | makes the bars 1 millisecond wide.
41 |
42 |
43 | Q: Can I use Flot with libraries like Mootools or Prototype?
44 |
45 | A: Yes, Flot supports it out of the box and it's easy! Just use jQuery
46 | instead of $, e.g. call jQuery.plot instead of $.plot and use
47 | jQuery(something) instead of $(something). As a convenience, you can
48 | put in a DOM element for the graph placeholder where the examples and
49 | the API documentation are using jQuery objects.
50 |
51 | Depending on how you include jQuery, you may have to add one line of
52 | code to prevent jQuery from overwriting functions from the other
53 | libraries, see the documentation in jQuery ("Using jQuery with other
54 | libraries") for details.
55 |
56 |
57 | Q: Flot doesn't work with [widget framework xyz]!
58 |
59 | A: The problem is most likely within the framework, or your use of the
60 | framework.
61 |
62 | The only non-standard thing used by Flot is the canvas tag; otherwise
63 | it is simply a series of absolute positioned divs within the
64 | placeholder tag you put in. If this is not working, it's probably
65 | because the framework you're using is doing something weird with the
66 | DOM. As a last resort, you might try replotting and see if it helps.
67 |
68 | If you find there's a specific thing we can do to Flot to help, feel
69 | free to submit a bug report. Otherwise, you're welcome to ask for help
70 | on the mailing list, but please don't submit a bug report to Flot -
71 | try the framework instead.
72 |
--------------------------------------------------------------------------------
/js/flot/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2007-2009 IOLA and Ole Laursen
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/js/flot/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for generating minified files
2 |
3 | YUICOMPRESSOR_PATH=../yuicompressor-2.3.5.jar
4 |
5 | # if you need another compressor path, just copy the above line to a
6 | # file called Makefile.local, customize it and you're good to go
7 | -include Makefile.local
8 |
9 | .PHONY: all
10 |
11 | # we cheat and process all .js files instead of listing them
12 | all: $(patsubst %.js,%.min.js,$(filter-out %.min.js,$(wildcard *.js)))
13 |
14 | %.min.js: %.js
15 | java -jar $(YUICOMPRESSOR_PATH) $< -o $@
16 |
--------------------------------------------------------------------------------
/js/flot/NEWS.txt:
--------------------------------------------------------------------------------
1 | Flot 0.6
2 | --------
3 |
4 | API changes:
5 |
6 | 1. Selection support has been moved to a plugin. Thus if you're
7 | passing selection: { mode: something }, you MUST include the file
8 | jquery.flot.selection.js after jquery.flot.js. This reduces the size
9 | of base Flot and makes it easier to customize the selection as well as
10 | improving code clarity. The change is based on patch from andershol.
11 |
12 | 2. In the global options specified in the $.plot command,
13 | "lines", "points", "bars" and "shadowSize" have been moved to a
14 | sub-object called "series", i.e.
15 |
16 | $.plot(placeholder, data, { lines: { show: true }})
17 |
18 | should be changed to
19 |
20 | $.plot(placeholder, data, { series: { lines: { show: true }}})
21 |
22 | All future series-specific options will go into this sub-object to
23 | simplify plugin writing. Backward-compatibility code is in place, so
24 | old code should not break.
25 |
26 | 3. "plothover" no longer provides the original data point, but instead
27 | a normalized one, since there may be no corresponding original point.
28 |
29 | 4. Due to a bug in previous versions of jQuery, you now need at least
30 | jQuery 1.2.6. But if you can, try jQuery 1.3.2 as it got some
31 | improvements in event handling speed.
32 |
33 |
34 | Changes:
35 |
36 | - Added support for disabling interactivity for specific data series
37 | (request from Ronald Schouten and Steve Upton).
38 |
39 | - Flot now calls $() on the placeholder and optional legend container
40 | passed in so you can specify DOM elements or CSS expressions to make
41 | it easier to use Flot with libraries like Prototype or Mootools or
42 | through raw JSON from Ajax responses.
43 |
44 | - A new "plotselecting" event is now emitted while the user is making
45 | a selection.
46 |
47 | - The "plothover" event is now emitted immediately instead of at most
48 | 10 times per second, you'll have to put in a setTimeout yourself if
49 | you're doing something really expensive on this event.
50 |
51 | - The built-in date formatter can now be accessed as
52 | $.plot.formatDate(...) (suggestion by Matt Manela) and even
53 | replaced.
54 |
55 | - Added "borderColor" option to the grid (patch from Amaury Chamayou
56 | and patch from Mike R. Williamson).
57 |
58 | - Added support for gradient backgrounds for the grid, take a look at
59 | the "setting options" example (based on patch from Amaury Chamayou,
60 | issue 90).
61 |
62 | - Gradient bars (suggestion by stefpet).
63 |
64 | - Added a "plotunselected" event which is triggered when the selection
65 | is removed, see "selection" example (suggestion by Meda Ugo);
66 |
67 | - The option legend.margin can now specify horizontal and vertical
68 | margins independently (suggestion by someone who's annoyed).
69 |
70 | - Data passed into Flot is now copied to a new canonical format to
71 | enable further processing before it hits the drawing routines. As a
72 | side-effect, this should make Flot more robust in the face of bad
73 | data (and fixes issue 112).
74 |
75 | - Step-wise charting: line charts have a new option "steps" that when
76 | set to true connects the points with horizontal/vertical steps
77 | instead of diagonal lines.
78 |
79 | - The legend labelFormatter now passes the series in addition to just
80 | the label (suggestion by Vincent Lemeltier).
81 |
82 | - Horizontal bars (based on patch by Jason LeBrun).
83 |
84 | - Support for partial bars by specifying a third coordinate, i.e. they
85 | don't have to start from the axis. This can be used to make stacked
86 | bars.
87 |
88 | - New option to disable the (grid.show).
89 |
90 | - Added pointOffset method for converting a point in data space to an
91 | offset within the placeholder.
92 |
93 | - Plugin system: register an init method in the $.flot.plugins array
94 | to get started, see PLUGINS.txt for details on how to write plugins
95 | (it's easy). There are also some extra methods to enable access to
96 | internal state.
97 |
98 | - Hooks: you can register functions that are called while Flot is
99 | crunching the data and doing the plot. This can be used to modify
100 | Flot without changing the source, useful for writing plugins. Some
101 | hooks are defined, more are likely to come.
102 |
103 | - Threshold plugin: you can set a threshold and a color, and the data
104 | points below that threshold will then get the color. Useful for
105 | marking data below 0, for instance.
106 |
107 | - Stack plugin: you can specify a stack key for each series to have
108 | them summed. This is useful for drawing additive/cumulative graphs
109 | with bars and (currently unfilled) lines.
110 |
111 | - Crosshairs plugin: trace the mouse position on the axes, enable with
112 | crosshair: { mode: "x"} (see the new tracking example for a use).
113 |
114 | - Image plugin: plot prerendered images.
115 |
116 | - Navigation plugin for panning and zooming a plot.
117 |
118 | - More configurable grid.
119 |
120 | - Axis transformation support, useful for non-linear plots, e.g. log
121 | axes and compressed time axes (like omitting weekends).
122 |
123 | - Support for twelve-hour date formatting (patch by Forrest Aldridge).
124 |
125 | - The color parsing code in Flot has been cleaned up and split out so
126 | it's now available as a separate jQuery plugin. It's included inline
127 | in the Flot source to make dependency managing easier. This also
128 | makes it really easy to use the color helpers in Flot plugins.
129 |
130 | Bug fixes:
131 |
132 | - Fixed two corner-case bugs when drawing filled curves (report and
133 | analysis by Joshua Varner).
134 | - Fix auto-adjustment code when setting min to 0 for an axis where the
135 | dataset is completely flat on that axis (report by chovy).
136 | - Fixed a bug with passing in data from getData to setData when the
137 | secondary axes are used (issue 65, reported by nperelman).
138 | - Fixed so that it is possible to turn lines off when no other chart
139 | type is shown (based on problem reported by Glenn Vanderburg), and
140 | fixed so that setting lineWidth to 0 also hides the shadow (based on
141 | problem reported by Sergio Nunes).
142 | - Updated mousemove position expression to the latest from jQuery (bug
143 | reported by meyuchas).
144 | - Use CSS borders instead of background in legend (fix printing issue 25
145 | and 45).
146 | - Explicitly convert axis min/max to numbers.
147 | - Fixed a bug with drawing marking lines with different colors
148 | (reported by Khurram).
149 | - Fixed a bug with returning y2 values in the selection event (fix
150 | by exists, issue 75).
151 | - Only set position relative on placeholder if it hasn't already a
152 | position different from static (reported by kyberneticist, issue 95).
153 | - Don't round markings to prevent sub-pixel problems (reported by Dan
154 | Lipsitt).
155 | - Make the grid border act similarly to a regular CSS border, i.e.
156 | prevent it from overlapping the plot itself. This also fixes a
157 | problem with anti-aliasing when the width is 1 pixel (reported by
158 | Anthony Ettinger).
159 | - Imported version 3 of excanvas and fixed two issues with the newer
160 | version. Hopefully, this will make Flot work with IE8 (nudge by
161 | Fabien Menager, further analysis by Booink, issue 133).
162 | - Changed the shadow code for lines to hopefully look a bit better
163 | with vertical lines.
164 | - Round tick positions to avoid possible problems with fractions
165 | (suggestion by Fred, issue 130).
166 | - Made the heuristic for determining how many ticks to aim for a bit
167 | smarter.
168 | - Fix for uneven axis margins (report and patch by Paul Kienzle) and
169 | snapping to ticks (concurrent report and patch by lifthrasiir).
170 | - Fixed bug with slicing in findNearbyItems (patch by zollman).
171 | - Make heuristic for x axis label widths more dynamic (patch by
172 | rickinhethuis).
173 | - Make sure points on top take precedence when finding nearby points
174 | when hovering (reported by didroe, issue 224).
175 |
176 | Flot 0.5
177 | --------
178 |
179 | Backwards API change summary: Timestamps are now in UTC. Also
180 | "selected" event -> becomes "plotselected" with new data, the
181 | parameters for setSelection are now different (but backwards
182 | compatibility hooks are in place), coloredAreas becomes markings with
183 | a new interface (but backwards compatibility hooks are in place).
184 |
185 |
186 | Interactivity: added a new "plothover" event and this and the
187 | "plotclick" event now returns the closest data item (based on patch by
188 | /david, patch by Mark Byers for bar support). See the revamped
189 | "interacting with the data" example for some hints on what you can do.
190 |
191 | Highlighting: you can now highlight points and datapoints are
192 | autohighlighted when you hover over them (if hovering is turned on).
193 |
194 | Support for dual axis has been added (based on patch by someone who's
195 | annoyed and /david). For each data series you can specify which axes
196 | it belongs to, and there are two more axes, x2axis and y2axis, to
197 | customize. This affects the "selected" event which has been renamed to
198 | "plotselected" and spews out { xaxis: { from: -10, to: 20 } ... },
199 | setSelection in which the parameters are on a new form (backwards
200 | compatible hooks are in place so old code shouldn't break) and
201 | markings (formerly coloredAreas).
202 |
203 | Timestamps in time mode are now displayed according to
204 | UTC instead of the time zone of the visitor. This affects the way the
205 | timestamps should be input; you'll probably have to offset the
206 | timestamps according to your local time zone. It also affects any
207 | custom date handling code (which basically now should use the
208 | equivalent UTC date mehods, e.g. .setUTCMonth() instead of
209 | .setMonth().
210 |
211 | Added support for specifying the size of tick labels (axis.labelWidth,
212 | axis.labelHeight). Useful for specifying a max label size to keep
213 | multiple plots aligned.
214 |
215 | Markings, previously coloredAreas, are now specified as ranges on the
216 | axes, like { xaxis: { from: 0, to: 10 }}. Furthermore with markings
217 | you can now draw horizontal/vertical lines by setting from and to to
218 | the same coordinate (idea from line support patch by by Ryan Funduk).
219 |
220 | The "fill" option can now be a number that specifies the opacity of
221 | the fill.
222 |
223 | You can now specify a coordinate as null (like [2, null]) and Flot
224 | will take the other coordinate into account when scaling the axes
225 | (based on patch by joebno).
226 |
227 | New option for bars "align". Set it to "center" to center the bars on
228 | the value they represent.
229 |
230 | setSelection now takes a second parameter which you can use to prevent
231 | the method from firing the "plotselected" handler.
232 |
233 | Using the "container" option in legend now overwrites the container
234 | element instead of just appending to it (fixes infinite legend bug,
235 | reported by several people, fix by Brad Dewey).
236 |
237 | Fixed a bug in calculating spacing around the plot (reported by
238 | timothytoe). Fixed a bug in finding max values for all-negative data
239 | sets. Prevent the possibility of eternal looping in tick calculations.
240 | Fixed a bug when borderWidth is set to 0 (reported by
241 | Rob/sanchothefat). Fixed a bug with drawing bars extending below 0
242 | (reported by James Hewitt, patch by Ryan Funduk). Fixed a
243 | bug with line widths of bars (reported by MikeM). Fixed a bug with
244 | 'nw' and 'sw' legend positions. Improved the handling of axis
245 | auto-scaling with bars. Fixed a bug with multi-line x-axis tick
246 | labels (reported by Luca Ciano). IE-fix help by Savage Zhang.
247 |
248 |
249 | Flot 0.4
250 | --------
251 |
252 | API changes: deprecated axis.noTicks in favor of just specifying the
253 | number as axis.ticks. So "xaxis: { noTicks: 10 }" becomes
254 | "xaxis: { ticks: 10 }"
255 |
256 | Time series support. Specify axis.mode: "time", put in Javascript
257 | timestamps as data, and Flot will automatically spit out sensible
258 | ticks. Take a look at the two new examples. The format can be
259 | customized with axis.timeformat and axis.monthNames, or if that fails
260 | with axis.tickFormatter.
261 |
262 | Support for colored background areas via grid.coloredAreas. Specify an
263 | array of { x1, y1, x2, y2 } objects or a function that returns these
264 | given { xmin, xmax, ymin, ymax }.
265 |
266 | More members on the plot object (report by Chris Davies and others).
267 | "getData" for inspecting the assigned settings on data series (e.g.
268 | color) and "setData", "setupGrid" and "draw" for updating the contents
269 | without a total replot.
270 |
271 | The default number of ticks to aim for is now dependent on the size of
272 | the plot in pixels. Support for customizing tick interval sizes
273 | directly with axis.minTickSize and axis.tickSize.
274 |
275 | Cleaned up the automatic axis scaling algorithm and fixed how it
276 | interacts with ticks. Also fixed a couple of tick-related corner case
277 | bugs (one reported by mainstreetmark, another reported by timothytoe).
278 |
279 | The option axis.tickFormatter now takes a function with two
280 | parameters, the second parameter is an optional object with
281 | information about the axis. It has min, max, tickDecimals, tickSize.
282 |
283 | Added support for segmented lines (based on patch from Michael
284 | MacDonald) and for ignoring null and bad values (suggestion from Nick
285 | Konidaris and joshwaihi).
286 |
287 | Added support for changing the border width (joebno and safoo).
288 | Label colors can be changed via CSS by selecting the tickLabel class.
289 |
290 | Fixed a bug in handling single-item bar series (reported by Emil
291 | Filipov). Fixed erratic behaviour when interacting with the plot
292 | with IE 7 (reported by Lau Bech Lauritzen). Prevent IE/Safari text
293 | selection when selecting stuff on the canvas.
294 |
295 |
296 |
297 | Flot 0.3
298 | --------
299 |
300 | This is mostly a quick-fix release because jquery.js wasn't included
301 | in the previous zip/tarball.
302 |
303 | Support clicking on the plot. Turn it on with grid: { clickable: true },
304 | then you get a "plotclick" event on the graph placeholder with the
305 | position in units of the plot.
306 |
307 | Fixed a bug in dealing with data where min = max, thanks to Michael
308 | Messinides.
309 |
310 | Include jquery.js in the zip/tarball.
311 |
312 |
313 | Flot 0.2
314 | --------
315 |
316 | Added support for putting a background behind the default legend. The
317 | default is the partly transparent background color. Added
318 | backgroundColor and backgroundOpacity to the legend options to control
319 | this.
320 |
321 | The ticks options can now be a callback function that takes one
322 | parameter, an object with the attributes min and max. The function
323 | should return a ticks array.
324 |
325 | Added labelFormatter option in legend, useful for turning the legend
326 | labels into links.
327 |
328 | Fixed a couple of bugs.
329 |
330 | The API should now be fully documented.
331 |
332 | Patch from Guy Fraser to make parts of the code smaller.
333 |
334 | API changes: Moved labelMargin option to grid from x/yaxis.
335 |
336 |
337 | Flot 0.1
338 | --------
339 |
340 | First public release.
341 |
--------------------------------------------------------------------------------
/js/flot/PLUGINS.txt:
--------------------------------------------------------------------------------
1 | Writing plugins
2 | ---------------
3 |
4 | To make a new plugin, create an init function and a set of options (if
5 | needed), stuff it into an object and put it in the $.plot.plugins
6 | array. For example:
7 |
8 | function myCoolPluginInit(plot) { plot.coolstring = "Hello!" };
9 | var myCoolOptions = { coolstuff: { show: true } }
10 | $.plot.plugins.push({ init: myCoolPluginInit, options: myCoolOptions });
11 |
12 | // now when $.plot is called, the returned object will have the
13 | // attribute "coolstring"
14 |
15 | Now, given that the plugin might run in many different places, it's
16 | a good idea to avoid leaking names. We can avoid this by wrapping the
17 | above lines in an anonymous function which we call immediately, like
18 | this: (function () { inner code ... })(). To make it even more robust
19 | in case $ is not bound to jQuery but some other Javascript library, we
20 | can write it as
21 |
22 | (function ($) {
23 | // plugin definition
24 | // ...
25 | })(jQuery);
26 |
27 | Here is a simple debug plugin which alerts each of the series in the
28 | plot. It has a single option that control whether it is enabled and
29 | how much info to output:
30 |
31 | (function ($) {
32 | function init(plot) {
33 | var debugLevel = 1;
34 |
35 | function checkDebugEnabled(plot, options) {
36 | if (options.debug) {
37 | debugLevel = options.debug;
38 |
39 | plot.hooks.processDatapoints.push(alertSeries);
40 | }
41 | }
42 |
43 | function alertSeries(plot, series, datapoints) {
44 | var msg = "series " + series.label;
45 | if (debugLevel > 1)
46 | msg += " with " + series.data.length + " points";
47 | alert(msg);
48 | }
49 |
50 | plot.hooks.processOptions.push(checkDebugEnabled);
51 | }
52 |
53 | var options = { debug: 0 };
54 |
55 | $.plot.plugins.push({
56 | init: init,
57 | options: options,
58 | name: "simpledebug",
59 | version: "0.1"
60 | });
61 | })(jQuery);
62 |
63 | We also define "name" and "version". It's not used by Flot, but might
64 | be helpful for other plugins in resolving dependencies.
65 |
66 | Put the above in a file named "jquery.flot.debug.js", include it in an
67 | HTML page and then it can be used with:
68 |
69 | $.plot($("#placeholder"), [...], { debug: 2 });
70 |
71 | This simple plugin illustrates a couple of points:
72 |
73 | - It uses the anonymous function trick to avoid name pollution.
74 | - It can be enabled/disabled through an option.
75 | - Variables in the init function can be used to store plot-specific
76 | state between the hooks.
77 |
78 |
79 | Options guidelines
80 | ==================
81 |
82 | Plugins should always support appropriate options to enable/disable
83 | them because the plugin user may have several plots on the same page
84 | where only one should use the plugin.
85 |
86 | If the plugin needs series-specific options, you can put them in
87 | "series" in the options object, e.g.
88 |
89 | var options = {
90 | series: {
91 | downsample: {
92 | algorithm: null,
93 | maxpoints: 1000
94 | }
95 | }
96 | }
97 |
98 | Then they will be copied by Flot into each series, providing the
99 | defaults in case the plugin user doesn't specify any. Again, in most
100 | cases it's probably a good idea if the plugin is turned off rather
101 | than on per default, just like most of the powerful features in Flot.
102 |
103 | Think hard and long about naming the options. These names are going to
104 | be public API, and code is going to depend on them if the plugin is
105 | successful.
106 |
--------------------------------------------------------------------------------
/js/flot/README.txt:
--------------------------------------------------------------------------------
1 | About
2 | -----
3 |
4 | Flot is a Javascript plotting library for jQuery. Read more at the
5 | website:
6 |
7 | http://code.google.com/p/flot/
8 |
9 | Take a look at the examples linked from above, they should give a good
10 | impression of what Flot can do and the source code of the examples is
11 | probably the fastest way to learn how to use Flot.
12 |
13 |
14 | Installation
15 | ------------
16 |
17 | Just include the Javascript file after you've included jQuery.
18 |
19 | Note that you need to get a version of Excanvas (e.g. the one bundled
20 | with Flot) which is canvas emulation on Internet Explorer. You can
21 | include the excanvas script like this:
22 |
23 |
24 |
25 | If it's not working on your development IE 6.0, check that it has
26 | support for VML which excanvas is relying on. It appears that some
27 | stripped down versions used for test environments on virtual machines
28 | lack the VML support.
29 |
30 | Also note that you need at least jQuery 1.2.6 (but at least jQuery
31 | 1.3.2 is recommended for interactive charts because of performance
32 | improvements in event handling).
33 |
34 |
35 | Basic usage
36 | -----------
37 |
38 | Create a placeholder div to put the graph in:
39 |
40 |
41 |
42 | You need to set the width and height of this div, otherwise the plot
43 | library doesn't know how to scale the graph. You can do it inline like
44 | this:
45 |
46 |
47 |
48 | You can also do it with an external stylesheet. Make sure that the
49 | placeholder isn't within something with a display:none CSS property -
50 | in that case, Flot has trouble measuring label dimensions which
51 | results in garbled looks and might have trouble measuring the
52 | placeholder dimensions which is fatal (it'll throw an exception).
53 |
54 | Then when the div is ready in the DOM, which is usually on document
55 | ready, run the plot function:
56 |
57 | $.plot($("#placeholder"), data, options);
58 |
59 | Here, data is an array of data series and options is an object with
60 | settings if you want to customize the plot. Take a look at the
61 | examples for some ideas of what to put in or look at the reference
62 | in the file "API.txt". Here's a quick example that'll draw a line from
63 | (0, 0) to (1, 1):
64 |
65 | $.plot($("#placeholder"), [ [[0, 0], [1, 1]] ], { yaxis: { max: 1 } });
66 |
67 | The plot function immediately draws the chart and then returns a plot
68 | object with a couple of methods.
69 |
70 |
71 | What's with the name?
72 | ---------------------
73 |
74 | First: it's pronounced with a short o, like "plot". Not like "flawed".
75 |
76 | So "Flot" rhymes with "plot".
77 |
78 | And if you look up "flot" in a Danish-to-English dictionary, some up
79 | the words that come up are "good-looking", "attractive", "stylish",
80 | "smart", "impressive", "extravagant". One of the main goals with Flot
81 | is pretty looks.
82 |
--------------------------------------------------------------------------------
/js/flot/examples/ajax.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Flot Examples
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Flot Examples
13 |
14 |
15 |
16 |
Example of loading data dynamically with AJAX. Percentage change in GDP (source: Eurostat). Click the buttons below.
17 |
18 |
The data is fetched over HTTP, in this case directly from text
19 | files. Usually the URL would point to some web server handler
20 | (e.g. a PHP page or Java/.NET/Python/Ruby on Rails handler) that
21 | extracts it from a database and serializes it to JSON.
Flot has support for simple background decorations such as
17 | lines and rectangles. They can be useful for marking up certain
18 | areas. You can easily add any HTML you need with standard DOM
19 | manipulation, e.g. for labels. For drawing custom shapes there is
20 | also direct access to the canvas.
Simple example. You don't need to specify much to get an
17 | attractive look. Put in a placeholder, make sure you set its
18 | dimensions (otherwise the plot library will barf) and call the
19 | plot function with the data. The axes are automatically
20 | scaled.
With the image plugin, you can plot images. This is for example
20 | useful for getting ticks on complex prerendered visualizations.
21 | Instead of inputting data points, you put in the images and where
22 | their two opposite corners are supposed to be in plot space.
23 |
24 |
Images represent a little further complication because you need
25 | to make sure they are loaded before you can use them (Flot skips
26 | incomplete images). The plugin comes with a couple of helpers
27 | for doing that.
With the navigate plugin it is easy to add panning and zooming.
36 | Drag to pan, double click to zoom (or use the mouse scrollwheel).
37 |
38 |
The plugin fires events (useful for synchronizing several
39 | plots) and adds a couple of public methods so you can easily build
40 | a little user interface around it, like the little buttons at the
41 | top right in the plot.
1000 kg. CO2 emissions per year per capita for various countries (source: Wikipedia).
18 |
19 |
Flot supports selections through the selection plugin.
20 | You can enable rectangular selection
21 | or one-dimensional selection if the user should only be able to
22 | select on one axis. Try left-click and drag on the plot above
23 | where selection on the x axis is enabled.
24 |
25 |
You selected:
26 |
27 |
The plot command returns a plot object you can use to control
28 | the selection. Click the buttons below.
29 |
30 |
31 |
32 |
33 |
Selections are really useful for zooming. Just replot the
34 | chart with min and max values for the axes set to the values
35 | in the "plotselected" event triggered. Enable the checkbox
36 | below and select a region again.
There are plenty of options you can set to control the precise
17 | looks of your plot. You can control the axes, the legend, the
18 | default graph type, the look of grid, etc.
19 |
20 |
The idea is that Flot goes to great lengths to provide sensible
21 | defaults which you can then customize as needed for your
22 | particular application. If you've found a use case where the
23 | defaults can be improved, please don't hesitate to give your
24 | feedback.
With the stack plugin, you can have Flot stack the
18 | series. This is useful if you wish to display both a total and the
19 | constituents it is made of. The only requirement is that you provide
20 | the input sorted on x.
With the threshold plugin, you can apply a specific color to
18 | the part of a data series below a threshold. This is can be useful
19 | for highlighting negative values, e.g. when displaying net results
20 | or what's in stock.
You can add crosshairs that'll track the mouse position, either
18 | on both axes or as here on only one.
19 |
20 |
If you combine it with listening on hover events, you can use
21 | it to track the intersection on the curves by interpolating
22 | the data points (look at the legend).
Here is an example with real data: military budgets for
17 | various countries in constant (2005) million US dollars (source: SIPRI).
18 |
19 |
Since all data is available client-side, it's pretty easy to
20 | make the plot interactive. Try turning countries on/off with the
21 | checkboxes below.
The selection support makes
26 | pretty advanced zooming schemes possible. With a few lines of code,
27 | the small overview plot to the right has been connected to the large
28 | plot. Try selecting a rectangle on either of them.
29 |
30 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/js/flot/jquery.colorhelpers.js:
--------------------------------------------------------------------------------
1 | /* Plugin for jQuery for working with colors.
2 | *
3 | * Version 1.0.
4 | *
5 | * Inspiration from jQuery color animation plugin by John Resig.
6 | *
7 | * Released under the MIT license by Ole Laursen, October 2009.
8 | *
9 | * Examples:
10 | *
11 | * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
12 | * var c = $.color.extract($("#mydiv"), 'background-color');
13 | * console.log(c.r, c.g, c.b, c.a);
14 | * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
15 | *
16 | * Note that .scale() and .add() work in-place instead of returning
17 | * new objects.
18 | */
19 |
20 | (function() {
21 | jQuery.color = {};
22 |
23 | // construct color object with some convenient chainable helpers
24 | jQuery.color.make = function (r, g, b, a) {
25 | var o = {};
26 | o.r = r || 0;
27 | o.g = g || 0;
28 | o.b = b || 0;
29 | o.a = a != null ? a : 1;
30 |
31 | o.add = function (c, d) {
32 | for (var i = 0; i < c.length; ++i)
33 | o[c.charAt(i)] += d;
34 | return o.normalize();
35 | };
36 |
37 | o.scale = function (c, f) {
38 | for (var i = 0; i < c.length; ++i)
39 | o[c.charAt(i)] *= f;
40 | return o.normalize();
41 | };
42 |
43 | o.toString = function () {
44 | if (o.a >= 1.0) {
45 | return "rgb("+[o.r, o.g, o.b].join(",")+")";
46 | } else {
47 | return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")";
48 | }
49 | };
50 |
51 | o.normalize = function () {
52 | function clamp(min, value, max) {
53 | return value < min ? min: (value > max ? max: value);
54 | }
55 |
56 | o.r = clamp(0, parseInt(o.r), 255);
57 | o.g = clamp(0, parseInt(o.g), 255);
58 | o.b = clamp(0, parseInt(o.b), 255);
59 | o.a = clamp(0, o.a, 1);
60 | return o;
61 | };
62 |
63 | o.clone = function () {
64 | return jQuery.color.make(o.r, o.b, o.g, o.a);
65 | };
66 |
67 | return o.normalize();
68 | }
69 |
70 | // extract CSS color property from element, going up in the DOM
71 | // if it's "transparent"
72 | jQuery.color.extract = function (elem, css) {
73 | var c;
74 | do {
75 | c = elem.css(css).toLowerCase();
76 | // keep going until we find an element that has color, or
77 | // we hit the body
78 | if (c != '' && c != 'transparent')
79 | break;
80 | elem = elem.parent();
81 | } while (!jQuery.nodeName(elem.get(0), "body"));
82 |
83 | // catch Safari's way of signalling transparent
84 | if (c == "rgba(0, 0, 0, 0)")
85 | c = "transparent";
86 |
87 | return jQuery.color.parse(c);
88 | }
89 |
90 | // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"),
91 | // returns color object
92 | jQuery.color.parse = function (str) {
93 | var res, m = jQuery.color.make;
94 |
95 | // Look for rgb(num,num,num)
96 | if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))
97 | return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10));
98 |
99 | // Look for rgba(num,num,num,num)
100 | 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))
101 | return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4]));
102 |
103 | // Look for rgb(num%,num%,num%)
104 | if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))
105 | return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55);
106 |
107 | // Look for rgba(num%,num%,num%,num)
108 | 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))
109 | return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4]));
110 |
111 | // Look for #a0b1c2
112 | if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))
113 | return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16));
114 |
115 | // Look for #fff
116 | if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))
117 | return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16));
118 |
119 | // Otherwise, we're most likely dealing with a named color
120 | var name = jQuery.trim(str).toLowerCase();
121 | if (name == "transparent")
122 | return m(255, 255, 255, 0);
123 | else {
124 | res = lookupColors[name];
125 | return m(res[0], res[1], res[2]);
126 | }
127 | }
128 |
129 | var lookupColors = {
130 | aqua:[0,255,255],
131 | azure:[240,255,255],
132 | beige:[245,245,220],
133 | black:[0,0,0],
134 | blue:[0,0,255],
135 | brown:[165,42,42],
136 | cyan:[0,255,255],
137 | darkblue:[0,0,139],
138 | darkcyan:[0,139,139],
139 | darkgrey:[169,169,169],
140 | darkgreen:[0,100,0],
141 | darkkhaki:[189,183,107],
142 | darkmagenta:[139,0,139],
143 | darkolivegreen:[85,107,47],
144 | darkorange:[255,140,0],
145 | darkorchid:[153,50,204],
146 | darkred:[139,0,0],
147 | darksalmon:[233,150,122],
148 | darkviolet:[148,0,211],
149 | fuchsia:[255,0,255],
150 | gold:[255,215,0],
151 | green:[0,128,0],
152 | indigo:[75,0,130],
153 | khaki:[240,230,140],
154 | lightblue:[173,216,230],
155 | lightcyan:[224,255,255],
156 | lightgreen:[144,238,144],
157 | lightgrey:[211,211,211],
158 | lightpink:[255,182,193],
159 | lightyellow:[255,255,224],
160 | lime:[0,255,0],
161 | magenta:[255,0,255],
162 | maroon:[128,0,0],
163 | navy:[0,0,128],
164 | olive:[128,128,0],
165 | orange:[255,165,0],
166 | pink:[255,192,203],
167 | purple:[128,0,128],
168 | violet:[128,0,128],
169 | red:[255,0,0],
170 | silver:[192,192,192],
171 | white:[255,255,255],
172 | yellow:[255,255,0]
173 | };
174 | })();
175 |
--------------------------------------------------------------------------------
/js/flot/jquery.colorhelpers.min.js:
--------------------------------------------------------------------------------
1 | (function(){jQuery.color={};jQuery.color.make=function(E,D,B,C){var F={};F.r=E||0;F.g=D||0;F.b=B||0;F.a=C!=null?C:1;F.add=function(I,H){for(var G=0;G=1){return"rgb("+[F.r,F.g,F.b].join(",")+")"}else{return"rgba("+[F.r,F.g,F.b,F.a].join(",")+")"}};F.normalize=function(){function G(I,J,H){return JH?H:J)}F.r=G(0,parseInt(F.r),255);F.g=G(0,parseInt(F.g),255);F.b=G(0,parseInt(F.b),255);F.a=G(0,F.a,1);return F};F.clone=function(){return jQuery.color.make(F.r,F.b,F.g,F.a)};return F.normalize()};jQuery.color.extract=function(C,B){var D;do{D=C.css(B).toLowerCase();if(D!=""&&D!="transparent"){break}C=C.parent()}while(!jQuery.nodeName(C.get(0),"body"));if(D=="rgba(0, 0, 0, 0)"){D="transparent"}return jQuery.color.parse(D)};jQuery.color.parse=function(E){var D,B=jQuery.color.make;if(D=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10))}if(D=/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(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10),parseFloat(D[4]))}if(D=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55)}if(D=/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(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55,parseFloat(D[4]))}if(D=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(E)){return B(parseInt(D[1],16),parseInt(D[2],16),parseInt(D[3],16))}if(D=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(E)){return B(parseInt(D[1]+D[1],16),parseInt(D[2]+D[2],16),parseInt(D[3]+D[3],16))}var C=jQuery.trim(E).toLowerCase();if(C=="transparent"){return B(255,255,255,0)}else{D=A[C];return B(D[0],D[1],D[2])}};var A={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]}})();
--------------------------------------------------------------------------------
/js/flot/jquery.flot.crosshair.js:
--------------------------------------------------------------------------------
1 | /*
2 | Flot plugin for showing a crosshair, thin lines, when the mouse hovers
3 | over the plot.
4 |
5 | crosshair: {
6 | mode: null or "x" or "y" or "xy"
7 | color: color
8 | lineWidth: number
9 | }
10 |
11 | Set the mode to one of "x", "y" or "xy". The "x" mode enables a
12 | vertical crosshair that lets you trace the values on the x axis, "y"
13 | enables a horizontal crosshair and "xy" enables them both. "color" is
14 | the color of the crosshair (default is "rgba(170, 0, 0, 0.80)"),
15 | "lineWidth" is the width of the drawn lines (default is 1).
16 |
17 | The plugin also adds four public methods:
18 |
19 | - setCrosshair(pos)
20 |
21 | Set the position of the crosshair. Note that this is cleared if
22 | the user moves the mouse. "pos" should be on the form { x: xpos,
23 | y: ypos } (or x2 and y2 if you're using the secondary axes), which
24 | is coincidentally the same format as what you get from a "plothover"
25 | event. If "pos" is null, the crosshair is cleared.
26 |
27 | - clearCrosshair()
28 |
29 | Clear the crosshair.
30 |
31 | - lockCrosshair(pos)
32 |
33 | Cause the crosshair to lock to the current location, no longer
34 | updating if the user moves the mouse. Optionally supply a position
35 | (passed on to setCrosshair()) to move it to.
36 |
37 | Example usage:
38 | var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
39 | $("#graph").bind("plothover", function (evt, position, item) {
40 | if (item) {
41 | // Lock the crosshair to the data point being hovered
42 | myFlot.lockCrosshair({ x: item.datapoint[0], y: item.datapoint[1] });
43 | }
44 | else {
45 | // Return normal crosshair operation
46 | myFlot.unlockCrosshair();
47 | }
48 | });
49 |
50 | - unlockCrosshair()
51 |
52 | Free the crosshair to move again after locking it.
53 | */
54 |
55 | (function ($) {
56 | var options = {
57 | crosshair: {
58 | mode: null, // one of null, "x", "y" or "xy",
59 | color: "rgba(170, 0, 0, 0.80)",
60 | lineWidth: 1
61 | }
62 | };
63 |
64 | function init(plot) {
65 | // position of crosshair in pixels
66 | var crosshair = { x: -1, y: -1, locked: false };
67 |
68 | plot.setCrosshair = function setCrosshair(pos) {
69 | if (!pos)
70 | crosshair.x = -1;
71 | else {
72 | var axes = plot.getAxes();
73 |
74 | crosshair.x = Math.max(0, Math.min(pos.x != null ? axes.xaxis.p2c(pos.x) : axes.x2axis.p2c(pos.x2), plot.width()));
75 | crosshair.y = Math.max(0, Math.min(pos.y != null ? axes.yaxis.p2c(pos.y) : axes.y2axis.p2c(pos.y2), plot.height()));
76 | }
77 |
78 | plot.triggerRedrawOverlay();
79 | };
80 |
81 | plot.clearCrosshair = plot.setCrosshair; // passes null for pos
82 |
83 | plot.lockCrosshair = function lockCrosshair(pos) {
84 | if (pos)
85 | plot.setCrosshair(pos);
86 | crosshair.locked = true;
87 | }
88 |
89 | plot.unlockCrosshair = function unlockCrosshair() {
90 | crosshair.locked = false;
91 | }
92 |
93 | plot.hooks.bindEvents.push(function (plot, eventHolder) {
94 | if (!plot.getOptions().crosshair.mode)
95 | return;
96 |
97 | eventHolder.mouseout(function () {
98 | if (crosshair.x != -1) {
99 | crosshair.x = -1;
100 | plot.triggerRedrawOverlay();
101 | }
102 | });
103 |
104 | eventHolder.mousemove(function (e) {
105 | if (plot.getSelection && plot.getSelection()) {
106 | crosshair.x = -1; // hide the crosshair while selecting
107 | return;
108 | }
109 |
110 | if (crosshair.locked)
111 | return;
112 |
113 | var offset = plot.offset();
114 | crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
115 | crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
116 | plot.triggerRedrawOverlay();
117 | });
118 | });
119 |
120 | plot.hooks.drawOverlay.push(function (plot, ctx) {
121 | var c = plot.getOptions().crosshair;
122 | if (!c.mode)
123 | return;
124 |
125 | var plotOffset = plot.getPlotOffset();
126 |
127 | ctx.save();
128 | ctx.translate(plotOffset.left, plotOffset.top);
129 |
130 | if (crosshair.x != -1) {
131 | ctx.strokeStyle = c.color;
132 | ctx.lineWidth = c.lineWidth;
133 | ctx.lineJoin = "round";
134 |
135 | ctx.beginPath();
136 | if (c.mode.indexOf("x") != -1) {
137 | ctx.moveTo(crosshair.x, 0);
138 | ctx.lineTo(crosshair.x, plot.height());
139 | }
140 | if (c.mode.indexOf("y") != -1) {
141 | ctx.moveTo(0, crosshair.y);
142 | ctx.lineTo(plot.width(), crosshair.y);
143 | }
144 | ctx.stroke();
145 | }
146 | ctx.restore();
147 | });
148 | }
149 |
150 | $.plot.plugins.push({
151 | init: init,
152 | options: options,
153 | name: 'crosshair',
154 | version: '1.0'
155 | });
156 | })(jQuery);
157 |
--------------------------------------------------------------------------------
/js/flot/jquery.flot.crosshair.min.js:
--------------------------------------------------------------------------------
1 | (function(B){var A={crosshair:{mode:null,color:"rgba(170, 0, 0, 0.80)",lineWidth:1}};function C(G){var H={x:-1,y:-1,locked:false};G.setCrosshair=function D(J){if(!J){H.x=-1}else{var I=G.getAxes();H.x=Math.max(0,Math.min(J.x!=null?I.xaxis.p2c(J.x):I.x2axis.p2c(J.x2),G.width()));H.y=Math.max(0,Math.min(J.y!=null?I.yaxis.p2c(J.y):I.y2axis.p2c(J.y2),G.height()))}G.triggerRedrawOverlay()};G.clearCrosshair=G.setCrosshair;G.lockCrosshair=function E(I){if(I){G.setCrosshair(I)}H.locked=true};G.unlockCrosshair=function F(){H.locked=false};G.hooks.bindEvents.push(function(J,I){if(!J.getOptions().crosshair.mode){return }I.mouseout(function(){if(H.x!=-1){H.x=-1;J.triggerRedrawOverlay()}});I.mousemove(function(K){if(J.getSelection&&J.getSelection()){H.x=-1;return }if(H.locked){return }var L=J.offset();H.x=Math.max(0,Math.min(K.pageX-L.left,J.width()));H.y=Math.max(0,Math.min(K.pageY-L.top,J.height()));J.triggerRedrawOverlay()})});G.hooks.drawOverlay.push(function(K,I){var L=K.getOptions().crosshair;if(!L.mode){return }var J=K.getPlotOffset();I.save();I.translate(J.left,J.top);if(H.x!=-1){I.strokeStyle=L.color;I.lineWidth=L.lineWidth;I.lineJoin="round";I.beginPath();if(L.mode.indexOf("x")!=-1){I.moveTo(H.x,0);I.lineTo(H.x,K.height())}if(L.mode.indexOf("y")!=-1){I.moveTo(0,H.y);I.lineTo(K.width(),H.y)}I.stroke()}I.restore()})}B.plot.plugins.push({init:C,options:A,name:"crosshair",version:"1.0"})})(jQuery);
--------------------------------------------------------------------------------
/js/flot/jquery.flot.image.js:
--------------------------------------------------------------------------------
1 | /*
2 | Flot plugin for plotting images, e.g. useful for putting ticks on a
3 | prerendered complex visualization.
4 |
5 | The data syntax is [[image, x1, y1, x2, y2], ...] where (x1, y1) and
6 | (x2, y2) are where you intend the two opposite corners of the image to
7 | end up in the plot. Image must be a fully loaded Javascript image (you
8 | can make one with new Image()). If the image is not complete, it's
9 | skipped when plotting.
10 |
11 | There are two helpers included for retrieving images. The easiest work
12 | the way that you put in URLs instead of images in the data (like
13 | ["myimage.png", 0, 0, 10, 10]), then call $.plot.image.loadData(data,
14 | options, callback) where data and options are the same as you pass in
15 | to $.plot. This loads the images, replaces the URLs in the data with
16 | the corresponding images and calls "callback" when all images are
17 | loaded (or failed loading). In the callback, you can then call $.plot
18 | with the data set. See the included example.
19 |
20 | A more low-level helper, $.plot.image.load(urls, callback) is also
21 | included. Given a list of URLs, it calls callback with an object
22 | mapping from URL to Image object when all images are loaded or have
23 | failed loading.
24 |
25 | Options for the plugin are
26 |
27 | series: {
28 | images: {
29 | show: boolean
30 | anchor: "corner" or "center"
31 | alpha: [0,1]
32 | }
33 | }
34 |
35 | which can be specified for a specific series
36 |
37 | $.plot($("#placeholder"), [{ data: [ ... ], images: { ... } ])
38 |
39 | Note that because the data format is different from usual data points,
40 | you can't use images with anything else in a specific data series.
41 |
42 | Setting "anchor" to "center" causes the pixels in the image to be
43 | anchored at the corner pixel centers inside of at the pixel corners,
44 | effectively letting half a pixel stick out to each side in the plot.
45 |
46 |
47 | A possible future direction could be support for tiling for large
48 | images (like Google Maps).
49 |
50 | */
51 |
52 | (function ($) {
53 | var options = {
54 | series: {
55 | images: {
56 | show: false,
57 | alpha: 1,
58 | anchor: "corner" // or "center"
59 | }
60 | }
61 | };
62 |
63 | $.plot.image = {};
64 |
65 | $.plot.image.loadDataImages = function (series, options, callback) {
66 | var urls = [], points = [];
67 |
68 | var defaultShow = options.series.images.show;
69 |
70 | $.each(series, function (i, s) {
71 | if (!(defaultShow || s.images.show))
72 | return;
73 |
74 | if (s.data)
75 | s = s.data;
76 |
77 | $.each(s, function (i, p) {
78 | if (typeof p[0] == "string") {
79 | urls.push(p[0]);
80 | points.push(p);
81 | }
82 | });
83 | });
84 |
85 | $.plot.image.load(urls, function (loadedImages) {
86 | $.each(points, function (i, p) {
87 | var url = p[0];
88 | if (loadedImages[url])
89 | p[0] = loadedImages[url];
90 | });
91 |
92 | callback();
93 | });
94 | }
95 |
96 | $.plot.image.load = function (urls, callback) {
97 | var missing = urls.length, loaded = {};
98 | if (missing == 0)
99 | callback({});
100 |
101 | $.each(urls, function (i, url) {
102 | var handler = function () {
103 | --missing;
104 |
105 | loaded[url] = this;
106 |
107 | if (missing == 0)
108 | callback(loaded);
109 | };
110 |
111 | $('').load(handler).error(handler).attr('src', url);
112 | });
113 | }
114 |
115 | function draw(plot, ctx) {
116 | var plotOffset = plot.getPlotOffset();
117 |
118 | $.each(plot.getData(), function (i, series) {
119 | var points = series.datapoints.points,
120 | ps = series.datapoints.pointsize;
121 |
122 | for (var i = 0; i < points.length; i += ps) {
123 | var img = points[i],
124 | x1 = points[i + 1], y1 = points[i + 2],
125 | x2 = points[i + 3], y2 = points[i + 4],
126 | xaxis = series.xaxis, yaxis = series.yaxis,
127 | tmp;
128 |
129 | // actually we should check img.complete, but it
130 | // appears to be a somewhat unreliable indicator in
131 | // IE6 (false even after load event)
132 | if (!img || img.width <= 0 || img.height <= 0)
133 | continue;
134 |
135 | if (x1 > x2) {
136 | tmp = x2;
137 | x2 = x1;
138 | x1 = tmp;
139 | }
140 | if (y1 > y2) {
141 | tmp = y2;
142 | y2 = y1;
143 | y1 = tmp;
144 | }
145 |
146 | // if the anchor is at the center of the pixel, expand the
147 | // image by 1/2 pixel in each direction
148 | if (series.images.anchor == "center") {
149 | tmp = 0.5 * (x2-x1) / (img.width - 1);
150 | x1 -= tmp;
151 | x2 += tmp;
152 | tmp = 0.5 * (y2-y1) / (img.height - 1);
153 | y1 -= tmp;
154 | y2 += tmp;
155 | }
156 |
157 | // clip
158 | if (x1 == x2 || y1 == y2 ||
159 | x1 >= xaxis.max || x2 <= xaxis.min ||
160 | y1 >= yaxis.max || y2 <= yaxis.min)
161 | continue;
162 |
163 | var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height;
164 | if (x1 < xaxis.min) {
165 | sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1);
166 | x1 = xaxis.min;
167 | }
168 |
169 | if (x2 > xaxis.max) {
170 | sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1);
171 | x2 = xaxis.max;
172 | }
173 |
174 | if (y1 < yaxis.min) {
175 | sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1);
176 | y1 = yaxis.min;
177 | }
178 |
179 | if (y2 > yaxis.max) {
180 | sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1);
181 | y2 = yaxis.max;
182 | }
183 |
184 | x1 = xaxis.p2c(x1);
185 | x2 = xaxis.p2c(x2);
186 | y1 = yaxis.p2c(y1);
187 | y2 = yaxis.p2c(y2);
188 |
189 | // the transformation may have swapped us
190 | if (x1 > x2) {
191 | tmp = x2;
192 | x2 = x1;
193 | x1 = tmp;
194 | }
195 | if (y1 > y2) {
196 | tmp = y2;
197 | y2 = y1;
198 | y1 = tmp;
199 | }
200 |
201 | tmp = ctx.globalAlpha;
202 | ctx.globalAlpha *= series.images.alpha;
203 | ctx.drawImage(img,
204 | sx1, sy1, sx2 - sx1, sy2 - sy1,
205 | x1 + plotOffset.left, y1 + plotOffset.top,
206 | x2 - x1, y2 - y1);
207 | ctx.globalAlpha = tmp;
208 | }
209 | });
210 | }
211 |
212 | function processRawData(plot, series, data, datapoints) {
213 | if (!series.images.show)
214 | return;
215 |
216 | // format is Image, x1, y1, x2, y2 (opposite corners)
217 | datapoints.format = [
218 | { required: true },
219 | { x: true, number: true, required: true },
220 | { y: true, number: true, required: true },
221 | { x: true, number: true, required: true },
222 | { y: true, number: true, required: true }
223 | ];
224 | }
225 |
226 | function init(plot) {
227 | plot.hooks.processRawData.push(processRawData);
228 | plot.hooks.draw.push(draw);
229 | }
230 |
231 | $.plot.plugins.push({
232 | init: init,
233 | options: options,
234 | name: 'image',
235 | version: '1.1'
236 | });
237 | })(jQuery);
238 |
--------------------------------------------------------------------------------
/js/flot/jquery.flot.image.min.js:
--------------------------------------------------------------------------------
1 | (function(D){var B={series:{images:{show:false,alpha:1,anchor:"corner"}}};D.plot.image={};D.plot.image.loadDataImages=function(G,F,K){var J=[],H=[];var I=F.series.images.show;D.each(G,function(L,M){if(!(I||M.images.show)){return }if(M.data){M=M.data}D.each(M,function(N,O){if(typeof O[0]=="string"){J.push(O[0]);H.push(O)}})});D.plot.image.load(J,function(L){D.each(H,function(N,O){var M=O[0];if(L[M]){O[0]=L[M]}});K()})};D.plot.image.load=function(H,I){var G=H.length,F={};if(G==0){I({})}D.each(H,function(K,J){var L=function(){--G;F[J]=this;if(G==0){I(F)}};D("").load(L).error(L).attr("src",J)})};function A(H,F){var G=H.getPlotOffset();D.each(H.getData(),function(O,P){var X=P.datapoints.points,I=P.datapoints.pointsize;for(var O=0;OK){N=K;K=M;M=N}if(V>T){N=T;T=V;V=N}if(P.images.anchor=="center"){N=0.5*(K-M)/(Q.width-1);M-=N;K+=N;N=0.5*(T-V)/(Q.height-1);V-=N;T+=N}if(M==K||V==T||M>=W.max||K<=W.min||V>=S.max||T<=S.min){continue}var L=0,U=0,J=Q.width,R=Q.height;if(MW.max){J+=(J-L)*(W.max-K)/(K-M);K=W.max}if(VS.max){U+=(U-R)*(S.max-T)/(T-V);T=S.max}M=W.p2c(M);K=W.p2c(K);V=S.p2c(V);T=S.p2c(T);if(M>K){N=K;K=M;M=N}if(V>T){N=T;T=V;V=N}N=F.globalAlpha;F.globalAlpha*=P.images.alpha;F.drawImage(Q,L,U,J-L,R-U,M+G.left,V+G.top,K-M,T-V);F.globalAlpha=N}})}function C(I,F,G,H){if(!F.images.show){return }H.format=[{required:true},{x:true,number:true,required:true},{y:true,number:true,required:true},{x:true,number:true,required:true},{y:true,number:true,required:true}]}function E(F){F.hooks.processRawData.push(C);F.hooks.draw.push(A)}D.plot.plugins.push({init:E,options:B,name:"image",version:"1.1"})})(jQuery);
--------------------------------------------------------------------------------
/js/flot/jquery.flot.navigate.js:
--------------------------------------------------------------------------------
1 | /*
2 | Flot plugin for adding panning and zooming capabilities to a plot.
3 |
4 | The default behaviour is double click and scrollwheel up/down to zoom
5 | in, drag to pan. The plugin defines plot.zoom({ center }),
6 | plot.zoomOut() and plot.pan(offset) so you easily can add custom
7 | controls. It also fires a "plotpan" and "plotzoom" event when
8 | something happens, useful for synchronizing plots.
9 |
10 | Example usage:
11 |
12 | plot = $.plot(...);
13 |
14 | // zoom default amount in on the pixel (100, 200)
15 | plot.zoom({ center: { left: 10, top: 20 } });
16 |
17 | // zoom out again
18 | plot.zoomOut({ center: { left: 10, top: 20 } });
19 |
20 | // pan 100 pixels to the left and 20 down
21 | plot.pan({ left: -100, top: 20 })
22 |
23 |
24 | Options:
25 |
26 | zoom: {
27 | interactive: false
28 | trigger: "dblclick" // or "click" for single click
29 | amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out)
30 | }
31 |
32 | pan: {
33 | interactive: false
34 | }
35 |
36 | xaxis, yaxis, x2axis, y2axis: {
37 | zoomRange: null // or [number, number] (min range, max range)
38 | panRange: null // or [number, number] (min, max)
39 | }
40 |
41 | "interactive" enables the built-in drag/click behaviour. "amount" is
42 | the amount to zoom the viewport relative to the current range, so 1 is
43 | 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out).
44 |
45 | "zoomRange" is the interval in which zooming can happen, e.g. with
46 | zoomRange: [1, 100] the zoom will never scale the axis so that the
47 | difference between min and max is smaller than 1 or larger than 100.
48 | You can set either of them to null to ignore.
49 |
50 | "panRange" confines the panning to stay within a range, e.g. with
51 | panRange: [-10, 20] panning stops at -10 in one end and at 20 in the
52 | other. Either can be null.
53 | */
54 |
55 |
56 | // First two dependencies, jquery.event.drag.js and
57 | // jquery.mousewheel.js, we put them inline here to save people the
58 | // effort of downloading them.
59 |
60 | /*
61 | jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com)
62 | Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt
63 | */
64 | (function(E){E.fn.drag=function(L,K,J){if(K){this.bind("dragstart",L)}if(J){this.bind("dragend",J)}return !L?this.trigger("drag"):this.bind("drag",K?K:L)};var A=E.event,B=A.special,F=B.drag={not:":input",distance:0,which:1,dragging:false,setup:function(J){J=E.extend({distance:F.distance,which:F.which,not:F.not},J||{});J.distance=I(J.distance);A.add(this,"mousedown",H,J);if(this.attachEvent){this.attachEvent("ondragstart",D)}},teardown:function(){A.remove(this,"mousedown",H);if(this===F.dragging){F.dragging=F.proxy=false}G(this,true);if(this.detachEvent){this.detachEvent("ondragstart",D)}}};B.dragstart=B.dragend={setup:function(){},teardown:function(){}};function H(L){var K=this,J,M=L.data||{};if(M.elem){K=L.dragTarget=M.elem;L.dragProxy=F.proxy||K;L.cursorOffsetX=M.pageX-M.left;L.cursorOffsetY=M.pageY-M.top;L.offsetX=L.pageX-L.cursorOffsetX;L.offsetY=L.pageY-L.cursorOffsetY}else{if(F.dragging||(M.which>0&&L.which!=M.which)||E(L.target).is(M.not)){return }}switch(L.type){case"mousedown":E.extend(M,E(K).offset(),{elem:K,target:L.target,pageX:L.pageX,pageY:L.pageY});A.add(document,"mousemove mouseup",H,M);G(K,false);F.dragging=null;return false;case !F.dragging&&"mousemove":if(I(L.pageX-M.pageX)+I(L.pageY-M.pageY) zr[1])))
193 | return;
194 |
195 | axisOptions.min = min;
196 | axisOptions.max = max;
197 | }
198 |
199 | scaleAxis(x1, x2, 'xaxis');
200 | scaleAxis(x1, x2, 'x2axis');
201 | scaleAxis(y1, y2, 'yaxis');
202 | scaleAxis(y1, y2, 'y2axis');
203 |
204 | plot.setupGrid();
205 | plot.draw();
206 |
207 | if (!args.preventEvent)
208 | plot.getPlaceholder().trigger("plotzoom", [ plot ]);
209 | }
210 |
211 | plot.pan = function (args) {
212 | var l = +args.left, t = +args.top,
213 | axes = plot.getAxes(), options = plot.getOptions();
214 |
215 | if (isNaN(l))
216 | l = 0;
217 | if (isNaN(t))
218 | t = 0;
219 |
220 | function panAxis(delta, name) {
221 | var axis = axes[name],
222 | axisOptions = options[name],
223 | min, max;
224 |
225 | if (!axis.used)
226 | return;
227 |
228 | min = axis.c2p(axis.p2c(axis.min) + delta),
229 | max = axis.c2p(axis.p2c(axis.max) + delta);
230 |
231 | var pr = axisOptions.panRange;
232 | if (pr) {
233 | // check whether we hit the wall
234 | if (pr[0] != null && pr[0] > min) {
235 | delta = pr[0] - min;
236 | min += delta;
237 | max += delta;
238 | }
239 |
240 | if (pr[1] != null && pr[1] < max) {
241 | delta = pr[1] - max;
242 | min += delta;
243 | max += delta;
244 | }
245 | }
246 |
247 | axisOptions.min = min;
248 | axisOptions.max = max;
249 | }
250 |
251 | panAxis(l, 'xaxis');
252 | panAxis(l, 'x2axis');
253 | panAxis(t, 'yaxis');
254 | panAxis(t, 'y2axis');
255 |
256 | plot.setupGrid();
257 | plot.draw();
258 |
259 | if (!args.preventEvent)
260 | plot.getPlaceholder().trigger("plotpan", [ plot ]);
261 | }
262 |
263 | plot.hooks.bindEvents.push(bindEvents);
264 | }
265 |
266 | $.plot.plugins.push({
267 | init: init,
268 | options: options,
269 | name: 'navigate',
270 | version: '1.1'
271 | });
272 | })(jQuery);
273 |
--------------------------------------------------------------------------------
/js/flot/jquery.flot.navigate.min.js:
--------------------------------------------------------------------------------
1 | (function(R){R.fn.drag=function(A,B,C){if(B){this.bind("dragstart",A)}if(C){this.bind("dragend",C)}return !A?this.trigger("drag"):this.bind("drag",B?B:A)};var M=R.event,L=M.special,Q=L.drag={not:":input",distance:0,which:1,dragging:false,setup:function(A){A=R.extend({distance:Q.distance,which:Q.which,not:Q.not},A||{});A.distance=N(A.distance);M.add(this,"mousedown",O,A);if(this.attachEvent){this.attachEvent("ondragstart",J)}},teardown:function(){M.remove(this,"mousedown",O);if(this===Q.dragging){Q.dragging=Q.proxy=false}P(this,true);if(this.detachEvent){this.detachEvent("ondragstart",J)}}};L.dragstart=L.dragend={setup:function(){},teardown:function(){}};function O(A){var B=this,C,D=A.data||{};if(D.elem){B=A.dragTarget=D.elem;A.dragProxy=Q.proxy||B;A.cursorOffsetX=D.pageX-D.left;A.cursorOffsetY=D.pageY-D.top;A.offsetX=A.pageX-A.cursorOffsetX;A.offsetY=A.pageY-A.cursorOffsetY}else{if(Q.dragging||(D.which>0&&A.which!=D.which)||R(A.target).is(D.not)){return }}switch(A.type){case"mousedown":R.extend(D,R(B).offset(),{elem:B,target:A.target,pageX:A.pageX,pageY:A.pageY});M.add(document,"mousemove mouseup",O,D);P(B,false);Q.dragging=null;return false;case !Q.dragging&&"mousemove":if(N(A.pageX-D.pageX)+N(A.pageY-D.pageY)Z[1]))){return }a.min=X;a.max=T}K(G,F,"xaxis");K(G,F,"x2axis");K(P,O,"yaxis");K(P,O,"y2axis");D.setupGrid();D.draw();if(!M.preventEvent){D.getPlaceholder().trigger("plotzoom",[D])}};D.pan=function(I){var F=+I.left,J=+I.top,K=D.getAxes(),H=D.getOptions();if(isNaN(F)){F=0}if(isNaN(J)){J=0}function G(R,M){var O=K[M],Q=H[M],N,L;if(!O.used){return }N=O.c2p(O.p2c(O.min)+R),L=O.c2p(O.p2c(O.max)+R);var P=Q.panRange;if(P){if(P[0]!=null&&P[0]>N){R=P[0]-N;N+=R;L+=R}if(P[1]!=null&&P[1] max? max: value);
169 | }
170 |
171 | function setSelectionPos(pos, e) {
172 | var o = plot.getOptions();
173 | var offset = plot.getPlaceholder().offset();
174 | var plotOffset = plot.getPlotOffset();
175 | pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width());
176 | pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height());
177 |
178 | if (o.selection.mode == "y")
179 | pos.x = pos == selection.first? 0: plot.width();
180 |
181 | if (o.selection.mode == "x")
182 | pos.y = pos == selection.first? 0: plot.height();
183 | }
184 |
185 | function updateSelection(pos) {
186 | if (pos.pageX == null)
187 | return;
188 |
189 | setSelectionPos(selection.second, pos);
190 | if (selectionIsSane()) {
191 | selection.show = true;
192 | plot.triggerRedrawOverlay();
193 | }
194 | else
195 | clearSelection(true);
196 | }
197 |
198 | function clearSelection(preventEvent) {
199 | if (selection.show) {
200 | selection.show = false;
201 | plot.triggerRedrawOverlay();
202 | if (!preventEvent)
203 | plot.getPlaceholder().trigger("plotunselected", [ ]);
204 | }
205 | }
206 |
207 | function setSelection(ranges, preventEvent) {
208 | var axis, range, axes = plot.getAxes();
209 | var o = plot.getOptions();
210 |
211 | if (o.selection.mode == "y") {
212 | selection.first.x = 0;
213 | selection.second.x = plot.width();
214 | }
215 | else {
216 | axis = ranges["xaxis"]? axes["xaxis"]: (ranges["x2axis"]? axes["x2axis"]: axes["xaxis"]);
217 | range = ranges["xaxis"] || ranges["x2axis"] || { from:ranges["x1"], to:ranges["x2"] }
218 | selection.first.x = axis.p2c(Math.min(range.from, range.to));
219 | selection.second.x = axis.p2c(Math.max(range.from, range.to));
220 | }
221 |
222 | if (o.selection.mode == "x") {
223 | selection.first.y = 0;
224 | selection.second.y = plot.height();
225 | }
226 | else {
227 | axis = ranges["yaxis"]? axes["yaxis"]: (ranges["y2axis"]? axes["y2axis"]: axes["yaxis"]);
228 | range = ranges["yaxis"] || ranges["y2axis"] || { from:ranges["y1"], to:ranges["y2"] }
229 | selection.first.y = axis.p2c(Math.min(range.from, range.to));
230 | selection.second.y = axis.p2c(Math.max(range.from, range.to));
231 | }
232 |
233 | selection.show = true;
234 | plot.triggerRedrawOverlay();
235 | if (!preventEvent)
236 | triggerSelectedEvent();
237 | }
238 |
239 | function selectionIsSane() {
240 | var minSize = 5;
241 | return Math.abs(selection.second.x - selection.first.x) >= minSize &&
242 | Math.abs(selection.second.y - selection.first.y) >= minSize;
243 | }
244 |
245 | plot.clearSelection = clearSelection;
246 | plot.setSelection = setSelection;
247 | plot.getSelection = getSelection;
248 |
249 | plot.hooks.bindEvents.push(function(plot, eventHolder) {
250 | var o = plot.getOptions();
251 | if (o.selection.mode != null)
252 | eventHolder.mousemove(onMouseMove);
253 |
254 | if (o.selection.mode != null)
255 | eventHolder.mousedown(onMouseDown);
256 | });
257 |
258 |
259 | plot.hooks.drawOverlay.push(function (plot, ctx) {
260 | // draw selection
261 | if (selection.show && selectionIsSane()) {
262 | var plotOffset = plot.getPlotOffset();
263 | var o = plot.getOptions();
264 |
265 | ctx.save();
266 | ctx.translate(plotOffset.left, plotOffset.top);
267 |
268 | var c = $.color.parse(o.selection.color);
269 |
270 | ctx.strokeStyle = c.scale('a', 0.8).toString();
271 | ctx.lineWidth = 1;
272 | ctx.lineJoin = "round";
273 | ctx.fillStyle = c.scale('a', 0.4).toString();
274 |
275 | var x = Math.min(selection.first.x, selection.second.x),
276 | y = Math.min(selection.first.y, selection.second.y),
277 | w = Math.abs(selection.second.x - selection.first.x),
278 | h = Math.abs(selection.second.y - selection.first.y);
279 |
280 | ctx.fillRect(x, y, w, h);
281 | ctx.strokeRect(x, y, w, h);
282 |
283 | ctx.restore();
284 | }
285 | });
286 | }
287 |
288 | $.plot.plugins.push({
289 | init: init,
290 | options: {
291 | selection: {
292 | mode: null, // one of null, "x", "y" or "xy"
293 | color: "#e8cfac"
294 | }
295 | },
296 | name: 'selection',
297 | version: '1.0'
298 | });
299 | })(jQuery);
300 |
--------------------------------------------------------------------------------
/js/flot/jquery.flot.selection.min.js:
--------------------------------------------------------------------------------
1 | (function(A){function B(J){var O={first:{x:-1,y:-1},second:{x:-1,y:-1},show:false,active:false};var L={};function D(Q){if(O.active){J.getPlaceholder().trigger("plotselecting",[F()]);K(Q)}}function M(Q){if(Q.which!=1){return }document.body.focus();if(document.onselectstart!==undefined&&L.onselectstart==null){L.onselectstart=document.onselectstart;document.onselectstart=function(){return false}}if(document.ondrag!==undefined&&L.ondrag==null){L.ondrag=document.ondrag;document.ondrag=function(){return false}}C(O.first,Q);O.active=true;A(document).one("mouseup",I)}function I(Q){if(document.onselectstart!==undefined){document.onselectstart=L.onselectstart}if(document.ondrag!==undefined){document.ondrag=L.ondrag}O.active=false;K(Q);if(E()){H()}else{J.getPlaceholder().trigger("plotunselected",[]);J.getPlaceholder().trigger("plotselecting",[null])}return false}function F(){if(!E()){return null}var R=Math.min(O.first.x,O.second.x),Q=Math.max(O.first.x,O.second.x),T=Math.max(O.first.y,O.second.y),S=Math.min(O.first.y,O.second.y);var U={};var V=J.getAxes();if(V.xaxis.used){U.xaxis={from:V.xaxis.c2p(R),to:V.xaxis.c2p(Q)}}if(V.x2axis.used){U.x2axis={from:V.x2axis.c2p(R),to:V.x2axis.c2p(Q)}}if(V.yaxis.used){U.yaxis={from:V.yaxis.c2p(T),to:V.yaxis.c2p(S)}}if(V.y2axis.used){U.y2axis={from:V.y2axis.c2p(T),to:V.y2axis.c2p(S)}}return U}function H(){var Q=F();J.getPlaceholder().trigger("plotselected",[Q]);var R=J.getAxes();if(R.xaxis.used&&R.yaxis.used){J.getPlaceholder().trigger("selected",[{x1:Q.xaxis.from,y1:Q.yaxis.from,x2:Q.xaxis.to,y2:Q.yaxis.to}])}}function G(R,S,Q){return SQ?Q:S)}function C(U,R){var T=J.getOptions();var S=J.getPlaceholder().offset();var Q=J.getPlotOffset();U.x=G(0,R.pageX-S.left-Q.left,J.width());U.y=G(0,R.pageY-S.top-Q.top,J.height());if(T.selection.mode=="y"){U.x=U==O.first?0:J.width()}if(T.selection.mode=="x"){U.y=U==O.first?0:J.height()}}function K(Q){if(Q.pageX==null){return }C(O.second,Q);if(E()){O.show=true;J.triggerRedrawOverlay()}else{P(true)}}function P(Q){if(O.show){O.show=false;J.triggerRedrawOverlay();if(!Q){J.getPlaceholder().trigger("plotunselected",[])}}}function N(R,Q){var T,S,U=J.getAxes();var V=J.getOptions();if(V.selection.mode=="y"){O.first.x=0;O.second.x=J.width()}else{T=R.xaxis?U.xaxis:(R.x2axis?U.x2axis:U.xaxis);S=R.xaxis||R.x2axis||{from:R.x1,to:R.x2};O.first.x=T.p2c(Math.min(S.from,S.to));O.second.x=T.p2c(Math.max(S.from,S.to))}if(V.selection.mode=="x"){O.first.y=0;O.second.y=J.height()}else{T=R.yaxis?U.yaxis:(R.y2axis?U.y2axis:U.yaxis);S=R.yaxis||R.y2axis||{from:R.y1,to:R.y2};O.first.y=T.p2c(Math.min(S.from,S.to));O.second.y=T.p2c(Math.max(S.from,S.to))}O.show=true;J.triggerRedrawOverlay();if(!Q){H()}}function E(){var Q=5;return Math.abs(O.second.x-O.first.x)>=Q&&Math.abs(O.second.y-O.first.y)>=Q}J.clearSelection=P;J.setSelection=N;J.getSelection=F;J.hooks.bindEvents.push(function(R,Q){var S=R.getOptions();if(S.selection.mode!=null){Q.mousemove(D)}if(S.selection.mode!=null){Q.mousedown(M)}});J.hooks.drawOverlay.push(function(T,Y){if(O.show&&E()){var R=T.getPlotOffset();var Q=T.getOptions();Y.save();Y.translate(R.left,R.top);var U=A.color.parse(Q.selection.color);Y.strokeStyle=U.scale("a",0.8).toString();Y.lineWidth=1;Y.lineJoin="round";Y.fillStyle=U.scale("a",0.4).toString();var W=Math.min(O.first.x,O.second.x),V=Math.min(O.first.y,O.second.y),X=Math.abs(O.second.x-O.first.x),S=Math.abs(O.second.y-O.first.y);Y.fillRect(W,V,X,S);Y.strokeRect(W,V,X,S);Y.restore()}})}A.plot.plugins.push({init:B,options:{selection:{mode:null,color:"#e8cfac"}},name:"selection",version:"1.0"})})(jQuery);
--------------------------------------------------------------------------------
/js/flot/jquery.flot.stack.js:
--------------------------------------------------------------------------------
1 | /*
2 | Flot plugin for stacking data sets, i.e. putting them on top of each
3 | other, for accumulative graphs. Note that the plugin assumes the data
4 | is sorted on x. Also note that stacking a mix of positive and negative
5 | values in most instances doesn't make sense (so it looks weird).
6 |
7 | Two or more series are stacked when their "stack" attribute is set to
8 | the same key (which can be any number or string or just "true"). To
9 | specify the default stack, you can set
10 |
11 | series: {
12 | stack: null or true or key (number/string)
13 | }
14 |
15 | or specify it for a specific series
16 |
17 | $.plot($("#placeholder"), [{ data: [ ... ], stack: true ])
18 |
19 | The stacking order is determined by the order of the data series in
20 | the array (later series end up on top of the previous).
21 |
22 | Internally, the plugin modifies the datapoints in each series, adding
23 | an offset to the y value. For line series, extra data points are
24 | inserted through interpolation. For bar charts, the second y value is
25 | also adjusted.
26 | */
27 |
28 | (function ($) {
29 | var options = {
30 | series: { stack: null } // or number/string
31 | };
32 |
33 | function init(plot) {
34 | function findMatchingSeries(s, allseries) {
35 | var res = null
36 | for (var i = 0; i < allseries.length; ++i) {
37 | if (s == allseries[i])
38 | break;
39 |
40 | if (allseries[i].stack == s.stack)
41 | res = allseries[i];
42 | }
43 |
44 | return res;
45 | }
46 |
47 | function stackData(plot, s, datapoints) {
48 | if (s.stack == null)
49 | return;
50 |
51 | var other = findMatchingSeries(s, plot.getData());
52 | if (!other)
53 | return;
54 |
55 | var ps = datapoints.pointsize,
56 | points = datapoints.points,
57 | otherps = other.datapoints.pointsize,
58 | otherpoints = other.datapoints.points,
59 | newpoints = [],
60 | px, py, intery, qx, qy, bottom,
61 | withlines = s.lines.show, withbars = s.bars.show,
62 | withsteps = withlines && s.lines.steps,
63 | i = 0, j = 0, l;
64 |
65 | while (true) {
66 | if (i >= points.length)
67 | break;
68 |
69 | l = newpoints.length;
70 |
71 | if (j >= otherpoints.length
72 | || otherpoints[j] == null
73 | || points[i] == null) {
74 | // degenerate cases
75 | for (m = 0; m < ps; ++m)
76 | newpoints.push(points[i + m]);
77 | i += ps;
78 | }
79 | else {
80 | // cases where we actually got two points
81 | px = points[i];
82 | py = points[i + 1];
83 | qx = otherpoints[j];
84 | qy = otherpoints[j + 1];
85 | bottom = 0;
86 |
87 | if (px == qx) {
88 | for (m = 0; m < ps; ++m)
89 | newpoints.push(points[i + m]);
90 |
91 | newpoints[l + 1] += qy;
92 | bottom = qy;
93 |
94 | i += ps;
95 | j += otherps;
96 | }
97 | else if (px > qx) {
98 | // we got past point below, might need to
99 | // insert interpolated extra point
100 | if (withlines && i > 0 && points[i - ps] != null) {
101 | intery = py + (points[i - ps + 1] - py) * (qx - px) / (points[i - ps] - px);
102 | newpoints.push(qx);
103 | newpoints.push(intery + qy)
104 | for (m = 2; m < ps; ++m)
105 | newpoints.push(points[i + m]);
106 | bottom = qy;
107 | }
108 |
109 | j += otherps;
110 | }
111 | else {
112 | for (m = 0; m < ps; ++m)
113 | newpoints.push(points[i + m]);
114 |
115 | // we might be able to interpolate a point below,
116 | // this can give us a better y
117 | if (withlines && j > 0 && otherpoints[j - ps] != null)
118 | bottom = qy + (otherpoints[j - ps + 1] - qy) * (px - qx) / (otherpoints[j - ps] - qx);
119 |
120 | newpoints[l + 1] += bottom;
121 |
122 | i += ps;
123 | }
124 |
125 | if (l != newpoints.length && withbars)
126 | newpoints[l + 2] += bottom;
127 | }
128 |
129 | // maintain the line steps invariant
130 | if (withsteps && l != newpoints.length && l > 0
131 | && newpoints[l] != null
132 | && newpoints[l] != newpoints[l - ps]
133 | && newpoints[l + 1] != newpoints[l - ps + 1]) {
134 | for (m = 0; m < ps; ++m)
135 | newpoints[l + ps + m] = newpoints[l + m];
136 | newpoints[l + 1] = newpoints[l - ps + 1];
137 | }
138 | }
139 |
140 | datapoints.points = newpoints;
141 | }
142 |
143 | plot.hooks.processDatapoints.push(stackData);
144 | }
145 |
146 | $.plot.plugins.push({
147 | init: init,
148 | options: options,
149 | name: 'stack',
150 | version: '1.0'
151 | });
152 | })(jQuery);
153 |
--------------------------------------------------------------------------------
/js/flot/jquery.flot.stack.min.js:
--------------------------------------------------------------------------------
1 | (function(B){var A={series:{stack:null}};function C(F){function D(J,I){var H=null;for(var G=0;G=Y.length){break}U=N.length;if(V>=S.length||S[V]==null||Y[X]==null){for(m=0;ma){if(O&&X>0&&Y[X-T]!=null){I=Q+(Y[X-T+1]-Q)*(a-R)/(Y[X-T]-R);N.push(a);N.push(I+Z);for(m=2;m0&&S[V-T]!=null){M=Z+(S[V-T+1]-Z)*(R-a)/(S[V-T]-a)}N[U+1]+=M;X+=T}}if(U!=N.length&&K){N[U+2]+=M}}if(J&&U!=N.length&&U>0&&N[U]!=null&&N[U]!=N[U-T]&&N[U+1]!=N[U-T+1]){for(m=0;m 0 && origpoints[i - ps] != null) {
65 | var interx = (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]) * (below - y) + x;
66 | prevp.push(interx);
67 | prevp.push(below);
68 | for (m = 2; m < ps; ++m)
69 | prevp.push(origpoints[i + m]);
70 |
71 | p.push(null); // start new segment
72 | p.push(null);
73 | for (m = 2; m < ps; ++m)
74 | p.push(origpoints[i + m]);
75 | p.push(interx);
76 | p.push(below);
77 | for (m = 2; m < ps; ++m)
78 | p.push(origpoints[i + m]);
79 | }
80 |
81 | p.push(x);
82 | p.push(y);
83 | }
84 |
85 | datapoints.points = newpoints;
86 | thresholded.datapoints.points = threspoints;
87 |
88 | if (thresholded.datapoints.points.length > 0)
89 | plot.getData().push(thresholded);
90 |
91 | // FIXME: there are probably some edge cases left in bars
92 | }
93 |
94 | plot.hooks.processDatapoints.push(thresholdData);
95 | }
96 |
97 | $.plot.plugins.push({
98 | init: init,
99 | options: options,
100 | name: 'threshold',
101 | version: '1.0'
102 | });
103 | })(jQuery);
104 |
--------------------------------------------------------------------------------
/js/flot/jquery.flot.threshold.min.js:
--------------------------------------------------------------------------------
1 | (function(B){var A={series:{threshold:null}};function C(D){function E(L,S,M){if(!S.threshold){return }var F=M.pointsize,I,O,N,G,K,H=B.extend({},S);H.datapoints={points:[],pointsize:F};H.label=null;H.color=S.threshold.color;H.threshold=null;H.originSeries=S;H.data=[];var P=S.threshold.below,Q=M.points,R=S.lines.show;threspoints=[];newpoints=[];for(I=0;I0&&Q[I-F]!=null){var J=(O-Q[I-F])/(N-Q[I-F+1])*(P-N)+O;K.push(J);K.push(P);for(m=2;m0){L.getData().push(H)}}D.hooks.processDatapoints.push(E)}B.plot.plugins.push({init:C,options:A,name:"threshold",version:"1.0"})})(jQuery);
--------------------------------------------------------------------------------
/lib/common.php:
--------------------------------------------------------------------------------
1 | ";
36 |
37 | $r->incr(ab_key(array("metric", $metric, $timestamp)), $value);
38 |
39 | //loop through all related experiments and add
40 | if (isset($ab_metrics[$metric]['associated_tests']))
41 | {
42 | foreach ($ab_metrics[$metric]['associated_tests'] as $test)
43 | {
44 | ab_tests_track($test, $value);
45 | }
46 | }
47 | }
48 |
49 | //get all values for a metric in a range of dates
50 | function ab_values($metric, $from, $to)
51 | {
52 | global $r;
53 |
54 | //convert to unicode times
55 | $ut_from = strtotime($from);
56 | $ut_to = strtotime($to);
57 |
58 | //calculate difference
59 | $ut_diff = $ut_to - $ut_from;
60 |
61 | $ut_diff_in_days = $ut_diff / 24 / 60 / 60;
62 |
63 |
64 | //i am adding an extra day just to fix the bug.
65 | //this whole function needs to be rewritten with proper math
66 | $ut_diff_in_days += 1;
67 | //if ($ut_diff_in_days > 90) $ut_diff_in_days = 90;
68 |
69 | if ($ut_diff_in_days < 1) $ut_diff_in_days = 0;
70 |
71 | $keys = array();
72 | $dates = array();
73 | $i = 0;
74 |
75 | while ($i <= $ut_diff_in_days)
76 | {
77 |
78 | $date_key = date("m-d-y", $ut_from + ($i*24*60*60) );
79 |
80 | $dates[] = $ut_from + ($i*24*60*60);
81 | $keys[] = ab_key(array("metric", $metric, $date_key));
82 |
83 | //add one day. this algorithm doesn't handle daylight savings time, etc. properly
84 | $i ++;
85 | }
86 |
87 | //print_r($keys);
88 | $vals = $r->mget($keys);
89 | //print_r($vals);
90 | //print_r ($vals);
91 |
92 | $result = array();
93 |
94 | $i = 0;
95 |
96 | foreach ($vals as $val)
97 | {
98 | //echo $dates[$i] . " - ";
99 | //$d = strtotime($dates[$i]);
100 | //echo $d;
101 | $result[] = array($dates[$i], (int)$val);
102 | $i++;
103 | }
104 |
105 | //print_r($r);
106 |
107 | return $result;
108 | }
109 |
110 |
--------------------------------------------------------------------------------
/lib/report.php:
--------------------------------------------------------------------------------
1 | alts: "; print_r($alts);
43 |
44 | //gather a list of conversion rates for each test
45 | $conversion_rates = array();
46 |
47 | foreach($alts as $alt)
48 | {
49 | $conversion_rates[$alt] = ab_test_conversion_rate($test, $alt);
50 | }
51 |
52 | arsort($conversion_rates);
53 |
54 | $result['sorted_alts'] = $conversion_rates;
55 |
56 | // echo " sortedalts: "; print_r($conversion_rates);
57 |
58 | //base is the second best result
59 | $base = array_slice($conversion_rates, 1, 1);
60 |
61 | $result['base'] = $base;
62 |
63 | // echo " base: "; print_r($base);
64 |
65 | //calculate z-score for base:
66 | $base_name = array_slice( array_keys($conversion_rates) , 1, 1);
67 | $base_name = $base_name[0];
68 |
69 | // echo " base name: "; print_r($base_name);
70 |
71 | $result['base_name'] = $base_name;
72 |
73 | $pc = ab_test_conversion_rate($test, $base_name)/100;
74 | $nc = (int) ab_tests_total_participants_for_alternative($test,$base_name);
75 |
76 | // echo " pc: ". $pc . " nc:" . $nc;
77 |
78 | $z_scores = array();
79 | $percentiles = array();
80 |
81 | foreach($conversion_rates as $alt => $rate)
82 | {
83 | // echo " rate: "; print_r($alt); echo " "; print_r($rate);
84 |
85 | $p = ab_test_conversion_rate($test, $alt)/100;
86 | $n = (int) ab_tests_total_participants_for_alternative($test,$alt);
87 |
88 | //prevent division by zero
89 | $z_scores[$alt] = 0;
90 | $percentiles[$alt] = 0;
91 | if ($n == 0 || $nc == 0) continue;
92 |
93 | //z-score is the difference over the std deviation
94 | //(p - pc) / ((p * (1-p)/n) + (pc * (1-pc)/nc)).abs ** 0.5
95 | $std_deviation = pow(abs( ($p * (1-$p)/$n) + ($pc * (1-$pc)/$nc) ),0.5);
96 | if ($std_deviation == 0) continue;
97 | $z_scores[$alt] = ($p - $pc) / $std_deviation;
98 | $percentiles[$alt] = convert_z_score_to_percentile( abs($z_scores[$alt]) );
99 | }
100 |
101 | // echo " z-score: "; print_r($z_scores);
102 |
103 | $result['z-scores'] = $z_scores;
104 | $result['percentiles'] = $percentiles;
105 |
106 |
107 | //find the least converted one that converted more than 0 times
108 | $least = false;
109 | //fast forward to the end
110 | end($conversion_rates);
111 |
112 | if (current($conversion_rates) > 0)
113 | {
114 | $least['name'] = key($conversion_rates);
115 | $least['rate'] = current($conversion_rates);
116 | }
117 | else
118 | {
119 | //keep rewinding until you find one
120 | while(prev($conversion_rates) !== false && $least !== false)
121 | {
122 | if (current($conversion_rates) > 0)
123 | {
124 | $least['name'] = key($conversion_rates);
125 | $least['rate'] = current($conversion_rates);
126 | }
127 | }
128 | }
129 |
130 | // echo " least: "; print_r($least);
131 |
132 | $result['least'] = $least;
133 |
134 | $differences = array();
135 |
136 | foreach($conversion_rates as $alt => $rate)
137 | {
138 | $c1 = ab_test_conversion_rate($test, $alt);
139 | $c2 = $least['rate'];
140 |
141 | // echo " c1: " . $c1 . " c2: " . $c2;
142 |
143 | $differences[$alt] = $c1 - $c2;
144 | }
145 |
146 | // echo " differences: "; print_r($differences);
147 |
148 | $result['differences'] = $differences;
149 |
150 | $best = array_slice( array_keys($conversion_rates) , 0, 1);
151 | $best = $best[0];
152 |
153 | $result['best'] = $best;
154 |
155 | // echo " best: "; print_r($best);
156 |
157 | //print_r($result);
158 | return ($result);
159 | }
160 |
161 | //state a conclusion
162 | function ab_tests_conclusion($test)
163 | {
164 | $r = "";
165 |
166 | $score = ab_tests_score($test);
167 |
168 |
169 | //var_dump($score);
170 |
171 | $r .= "The best option was " . $score['best'] . ". ";
172 |
173 | $r .= "It converted at " . round($score['sorted_alts'][$score['best']],3) . "%. ";
174 |
175 | $best_percent_significance = $score['percentiles'][$score['best']];
176 |
177 | $r .= "With " . $best_percent_significance . "% probability this result is statistically significant. ";
178 |
179 | if ($best_percent_significance > 90)
180 | {
181 | $r .= "So we think this is pretty significant. ";
182 | }
183 | else
184 | {
185 | $r .= "So this isn't that significant, we suggest you continue this experiment. ";
186 | }
187 |
188 | return($r);
189 | }
190 |
191 |
192 | function erase_data_for_test($test)
193 | {
194 | global $r;
195 |
196 | //Find all keys that refer to this test using redis search functionality
197 | //@FIXME: this wont work right if there is a * in the test name but i am not checking for this
198 | $keys_to_delete = $r->keys("ab:test:$test:*");
199 |
200 | //print_r ($keys_to_delete);
201 |
202 | //try deleting these keys
203 | foreach ($keys_to_delete as $key)
204 | {
205 | if ($key != null)
206 | $r->delete($key);
207 | }
208 | }
209 |
210 | function erase_all_keys_except_forced_keys()
211 | {
212 | global $r;
213 |
214 | /////////////////////////////////////////
215 | //First get all the keys you want to save
216 | /////////////////////////////////////////
217 | $force_keys = array();
218 | $force_vals = array();
219 |
220 | //the redis "keys" function searches for keys that match a specified pattern
221 | $force_keys = $r->keys("ab:*:force");
222 |
223 | foreach($force_keys as $key)
224 | {
225 | $force_vals[] = $r->get($key);
226 | }
227 |
228 | //print_r($force_keys);
229 | //print_r($force_vals);
230 |
231 |
232 | /////////////////////////////////////////
233 | //Now erase everything else
234 | /////////////////////////////////////////
235 | $r->flushdb();
236 |
237 | /////////////////////////////////////////
238 | //Now restore the forced keys
239 | /////////////////////////////////////////
240 | for ($i = 0; $i < count($force_keys); $i++)
241 | {
242 | $r->set($force_keys[$i], $force_vals[$i]);
243 | }
244 | }
--------------------------------------------------------------------------------
/lib/tests.php:
--------------------------------------------------------------------------------
1 | sadd(ab_key( array('test',$test,'alt',$alt,'participants') ), $p);
56 | }
57 |
58 | //this is core and actual ab test function
59 | //-- it returns an alternative for the current visitor
60 | //don't call directly: use the wrapper in core.php
61 | function ab_tests_test($test)
62 | {
63 | global $ab_participant_id;
64 | global $redis_connected;
65 |
66 | $forced_alternative = null;
67 |
68 | if ($redis_connected)
69 | {
70 | $forced_alternative = ab_tests_get_forced_alternative($test);
71 | }
72 |
73 | if ($forced_alternative == null)
74 | $alt = ab_tests_alternative_for ($ab_participant_id, $test);
75 | else
76 | $alt = $forced_alternative;
77 |
78 | return $alt;
79 | }
80 |
81 | //record a conversion for a *test* and current paricipant
82 | function ab_tests_track($test, $value)
83 | {
84 | global $ab_participant_id;
85 | global $r;
86 |
87 | $p = $ab_participant_id;
88 |
89 | $alt = ab_tests_alternative_for($p, $test);
90 |
91 | //note that if a test was "forced" to a specific alternative,
92 | //then the following sismember function will return false
93 | //and nothing will be tracked for this test
94 | if ($r->sismember( ab_key(array('test',$test,'alt',$alt,'participants')) , $p))
95 | {
96 | //add this participant to the set of people who have converted on this metric
97 | $r->sadd( ab_key( array('test',$test,'alt',$alt,'converted')) , $p);
98 | //and increment the total number of conversions as well
99 | $r->incr( ab_key( array('test',$test,'alt',$alt,'conversions') ), $value );
100 | }
101 | }
102 |
103 | //pass me the participants ID and the name of the test and i will
104 | //return the correct alternative (by name as string) to use
105 | function ab_tests_alternative_for($p, $test)
106 | {
107 | global $ab_tests;
108 |
109 | //generate a unique id combining the name of the test
110 | //and the participants ID
111 | $u = $ab_tests[$test]['name'] . "/" . $p;
112 |
113 | //how many alternatives are there?
114 | $n = count($ab_tests[$test]['alternatives']);
115 |
116 |
117 | //i get an md5 of the test name + unique user id
118 | //then i substring it and convert to a decimal number
119 | //so i can do math on it later.
120 | $m = md5($u);
121 | $m = substr($m,0,7); //take substring as this number is too large to conver to an int
122 | //we choose the number 7 bc 0xFFFFFFF is less than 2^32
123 | $m = hexdec($m);
124 |
125 | $c = $m % $n;
126 | //echo "