├── .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 |
35 |

36 | Hello User #, 37 |
Please press the button if you like its color. 38 |

39 | 40 | 52 | 53 |
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.

22 | 23 |

24 | - 25 | data - 26 | 27 |

28 | 29 |

30 | - 31 | data - 32 | 33 |

34 | 35 |

36 | - 37 | data - 38 | 39 |

40 | 41 |

If you combine AJAX with setTimeout, you can poll the server 42 | for new data.

43 | 44 |

45 | 46 |

47 | 48 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /js/flot/examples/annotating.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Flot Examples

13 | 14 |
15 | 16 |

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.

21 | 22 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /js/flot/examples/arrow-down.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliasaria/PHP-redis-A-B-Testing/091515ab6304781bbb99dda215b9b53cf1a4532b/js/flot/examples/arrow-down.gif -------------------------------------------------------------------------------- /js/flot/examples/arrow-left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliasaria/PHP-redis-A-B-Testing/091515ab6304781bbb99dda215b9b53cf1a4532b/js/flot/examples/arrow-left.gif -------------------------------------------------------------------------------- /js/flot/examples/arrow-right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliasaria/PHP-redis-A-B-Testing/091515ab6304781bbb99dda215b9b53cf1a4532b/js/flot/examples/arrow-right.gif -------------------------------------------------------------------------------- /js/flot/examples/arrow-up.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliasaria/PHP-redis-A-B-Testing/091515ab6304781bbb99dda215b9b53cf1a4532b/js/flot/examples/arrow-up.gif -------------------------------------------------------------------------------- /js/flot/examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Flot Examples

13 | 14 |
15 | 16 |

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.

21 | 22 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /js/flot/examples/data-eu-gdp-growth-1.json: -------------------------------------------------------------------------------- 1 | { 2 | label: 'Europe (EU27)', 3 | data: [[1999, 3.0], [2000, 3.9]] 4 | } 5 | -------------------------------------------------------------------------------- /js/flot/examples/data-eu-gdp-growth-2.json: -------------------------------------------------------------------------------- 1 | { 2 | label: 'Europe (EU27)', 3 | data: [[1999, 3.0], [2000, 3.9], [2001, 2.0], [2002, 1.2]] 4 | } 5 | -------------------------------------------------------------------------------- /js/flot/examples/data-eu-gdp-growth-3.json: -------------------------------------------------------------------------------- 1 | { 2 | label: 'Europe (EU27)', 3 | data: [[1999, 3.0], [2000, 3.9], [2001, 2.0], [2002, 1.2], [2003, 1.3], [2004, 2.5]] 4 | } 5 | -------------------------------------------------------------------------------- /js/flot/examples/data-eu-gdp-growth-4.json: -------------------------------------------------------------------------------- 1 | { 2 | label: 'Europe (EU27)', 3 | data: [[1999, 3.0], [2000, 3.9], [2001, 2.0], [2002, 1.2], [2003, 1.3], [2004, 2.5], [2005, 2.0], [2006, 3.1]] 4 | } 5 | -------------------------------------------------------------------------------- /js/flot/examples/data-eu-gdp-growth-5.json: -------------------------------------------------------------------------------- 1 | { 2 | label: 'Europe (EU27)', 3 | data: [[1999, 3.0], [2000, 3.9], [2001, 2.0], [2002, 1.2], [2003, 1.3], [2004, 2.5], [2005, 2.0], [2006, 3.1], [2007, 2.9], [2008, 0.9]] 4 | } 5 | -------------------------------------------------------------------------------- /js/flot/examples/data-eu-gdp-growth.json: -------------------------------------------------------------------------------- 1 | { 2 | label: 'Europe (EU27)', 3 | data: [[1999, 3.0], [2000, 3.9], [2001, 2.0], [2002, 1.2], [2003, 1.3], [2004, 2.5], [2005, 2.0], [2006, 3.1], [2007, 2.9], [2008, 0.9]] 4 | } 5 | -------------------------------------------------------------------------------- /js/flot/examples/data-japan-gdp-growth.json: -------------------------------------------------------------------------------- 1 | { 2 | label: 'Japan', 3 | data: [[1999, -0.1], [2000, 2.9], [2001, 0.2], [2002, 0.3], [2003, 1.4], [2004, 2.7], [2005, 1.9], [2006, 2.0], [2007, 2.3], [2008, -0.7]] 4 | } 5 | -------------------------------------------------------------------------------- /js/flot/examples/data-usa-gdp-growth.json: -------------------------------------------------------------------------------- 1 | { 2 | label: 'USA', 3 | data: [[1999, 4.4], [2000, 3.7], [2001, 0.8], [2002, 1.6], [2003, 2.5], [2004, 3.6], [2005, 2.9], [2006, 2.8], [2007, 2.0], [2008, 1.1]] 4 | } 5 | -------------------------------------------------------------------------------- /js/flot/examples/graph-types.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Flot Examples

13 | 14 |
15 | 16 |

Flot supports lines, points, filled areas, bars and any 17 | combinations of these, in the same plot and even on the same data 18 | series.

19 | 20 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /js/flot/examples/hs-2004-27-a-large_web.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliasaria/PHP-redis-A-B-Testing/091515ab6304781bbb99dda215b9b53cf1a4532b/js/flot/examples/hs-2004-27-a-large_web.jpg -------------------------------------------------------------------------------- /js/flot/examples/image.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Flot Examples

14 | 15 |
16 | 17 |

The Cat's Eye Nebula (picture from Hubble).

18 | 19 |

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.

28 | 29 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /js/flot/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Flot Examples

13 | 14 |

Here are some examples for Flot, the Javascript charting library for jQuery:

15 | 16 | 22 | 23 |

Being interactive:

24 | 25 | 31 | 32 |

Some more esoteric features:

33 | 34 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /js/flot/examples/interacting.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Flot Examples

13 | 14 |
15 | 16 |

One of the goals of Flot is to support user interactions. Try 17 | pointing and clicking on the points.

18 | 19 |

Mouse hovers at 20 | (0, 0).

21 | 22 |

A tooltip is easy to build with a bit of jQuery code and the 23 | data returned from the plot.

24 | 25 |

Enable tooltip

26 | 27 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /js/flot/examples/layout.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | font-size: 16px; 4 | margin: 50px; 5 | max-width: 800px; 6 | } 7 | -------------------------------------------------------------------------------- /js/flot/examples/navigate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 27 | 28 | 29 |

Flot Examples

30 | 31 |
32 | 33 |

34 | 35 |

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.

42 | 43 | 44 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /js/flot/examples/selection.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Flot Examples

14 | 15 |
16 | 17 |

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.

37 | 38 |

Zoom to selection.

39 | 40 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /js/flot/examples/setting-options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Flot Examples

13 | 14 |
15 | 16 |

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.

25 | 26 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /js/flot/examples/stacking.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Flot Examples

14 | 15 |
16 | 17 |

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.

21 | 22 |

23 | 24 | 25 |

26 | 27 |

28 | 29 | 30 | 31 |

32 | 33 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /js/flot/examples/thresholding.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Flot Examples

14 | 15 |
16 | 17 |

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.

21 | 22 |

23 | 24 | 25 | 26 |

27 | 28 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /js/flot/examples/tracking.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Flot Examples

14 | 15 |
16 | 17 |

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).

23 | 24 |

25 | 26 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /js/flot/examples/turning-series.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Flot Examples

13 | 14 |
15 | 16 |

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.

22 | 23 |

Show:

24 | 25 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /js/flot/examples/visitors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Flot Examples

14 | 15 |
16 | 17 |

Visitors per day to the Flot homepage. Weekends are colored. Try zooming. 18 | The plot below shows an overview.

19 | 20 |
21 | 22 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /js/flot/examples/zooming.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flot Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Flot Examples

14 | 15 |
16 |
17 |
18 | 19 |
20 |
21 | 22 |

23 |
24 | 25 |

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 "
" . $u . " " . $m . " " . $n . " " . $c . "

"; 127 | 128 | $choice = $ab_tests[$test]['alternatives'][$c]; 129 | 130 | // echo "
for this $p dude, i choose: " . $choice; 131 | 132 | return ($choice); 133 | } 134 | 135 | //count the number of converstions for the given test and alternative 136 | function ab_tests_total_conversions_for_alternative($test,$alternative) 137 | { 138 | global $r; 139 | 140 | $a = $r->get( ab_key( array('test',$test,'alt',$alternative,'conversions') ) ); 141 | 142 | return ($a); 143 | } 144 | 145 | //count the number of participants for the given test and alternative 146 | function ab_tests_total_participants_for_alternative($test,$alternative) 147 | { 148 | global $r; 149 | 150 | $a = $r->scard( ab_key( array('test',$test,'alt',$alternative,'participants') ) ); 151 | 152 | return ($a); 153 | } 154 | 155 | //count the number of converted participants for the given test and alternative 156 | function ab_tests_total_converted_for_alternative($test,$alternative) 157 | { 158 | global $r; 159 | 160 | $a = $r->scard( ab_key( array('test',$test,'alt',$alternative,'converted') ) ); 161 | 162 | return ($a); 163 | } 164 | 165 | /** 166 | * Force a test to always land on specific result: 167 | * This function does no validation, and assumes test and alternative are valid 168 | * 169 | * @param $test -- name of test as string 170 | * @param $alternative -- name of alternative as string. Pass null to clear 171 | * @return n/a 172 | */ 173 | function ab_tests_force_alternative($test, $alternative) 174 | { 175 | global $r; 176 | 177 | $key = ab_key( array('test',$test,'force') ); 178 | 179 | if ($alternative != null) 180 | $r->set($key,$alternative); 181 | else 182 | $r->delete($key); 183 | 184 | } 185 | 186 | /** 187 | * For the given test, return which alternative is currently forced. 188 | * 189 | * Return null if 190 | * 191 | * @param $test -- name of test as string 192 | * @return String: name of alternative as string. null if no alternative 193 | * is currently forced 194 | */ 195 | function ab_tests_get_forced_alternative($test) 196 | { 197 | global $r; 198 | 199 | $key = ab_key( array('test',$test,'force') ); 200 | 201 | $return = $r->get($key); 202 | 203 | return($return); 204 | } 205 | 206 | function ab_test_clear_and_restart_test($test) 207 | { 208 | //not yet implemented 209 | } 210 | 211 | //get conversion rate (as percent) for the provided test and alternative 212 | function ab_test_conversion_rate($test, $alt) 213 | { 214 | $p = (int)ab_tests_total_participants_for_alternative($test,$alt); 215 | //$c1 = (int)ab_tests_total_conversions_for_alternative($test,$alt); 216 | $c2 = (int)ab_tests_total_converted_for_alternative($test,$alt); 217 | 218 | if ($p > 0) 219 | $conv_rate = $c2 / $p * 100; 220 | else 221 | $conv_rate = 0; 222 | 223 | 224 | return ($conv_rate); 225 | } 226 | 227 | -------------------------------------------------------------------------------- /lib/z_scores_table.php: -------------------------------------------------------------------------------- 1 | score: " . $z_score; 9 | 10 | 11 | $cur = -3; 12 | $last = 0; 13 | 14 | for ($i = 0; $i < count($table_z_scores); $i++) 15 | { 16 | $cur = $table_z_scores[$i]; 17 | 18 | if ($cur > $z_score) 19 | { 20 | break; 21 | } 22 | else 23 | { 24 | $last = $i; 25 | } 26 | } 27 | 28 | // echo "last: " . $last; 29 | 30 | return($table_percentiles[$last]); 31 | } 32 | 33 | $table_z_scores = array( 34 | -2.326 , 35 | -2.054 , 36 | -1.881 , 37 | -1.751 , 38 | -1.645 , 39 | -1.555 , 40 | -1.476 , 41 | -1.405 , 42 | -1.341 , 43 | -1.282 , 44 | -1.227 , 45 | -1.175 , 46 | -1.126 , 47 | -1.08 , 48 | -1.036 , 49 | -0.994 , 50 | -0.954 , 51 | -0.915 , 52 | -0.878 , 53 | -0.842 , 54 | -0.806 , 55 | -0.772 , 56 | -0.739 , 57 | -0.706 , 58 | -0.674 , 59 | -0.643 , 60 | -0.613 , 61 | -0.583 , 62 | -0.553 , 63 | -0.524 , 64 | -0.496 , 65 | -0.468 , 66 | -0.44 , 67 | -0.412 , 68 | -0.385 , 69 | -0.358 , 70 | -0.332 , 71 | -0.305 , 72 | -0.279 , 73 | -0.253 , 74 | -0.228 , 75 | -0.202 , 76 | -0.176 , 77 | -0.151 , 78 | -0.126 , 79 | -0.1 , 80 | -0.075 , 81 | -0.05 , 82 | -0.025 , 83 | 0 , 84 | 0.025 , 85 | 0.05 , 86 | 0.075 , 87 | 0.1 , 88 | 0.126 , 89 | 0.151 , 90 | 0.176 , 91 | 0.202 , 92 | 0.228 , 93 | 0.253 , 94 | 0.279 , 95 | 0.305 , 96 | 0.332 , 97 | 0.358 , 98 | 0.385 , 99 | 0.412 , 100 | 0.44 , 101 | 0.468 , 102 | 0.496 , 103 | 0.524 , 104 | 0.553 , 105 | 0.583 , 106 | 0.613 , 107 | 0.643 , 108 | 0.674 , 109 | 0.706 , 110 | 0.739 , 111 | 0.772 , 112 | 0.806 , 113 | 0.842 , 114 | 0.878 , 115 | 0.915 , 116 | 0.954 , 117 | 0.994 , 118 | 1.036 , 119 | 1.08 , 120 | 1.126 , 121 | 1.175 , 122 | 1.227 , 123 | 1.282 , 124 | 1.341 , 125 | 1.405 , 126 | 1.476 , 127 | 1.555 , 128 | 1.645 , 129 | 1.751 , 130 | 1.881 , 131 | 2.054 , 132 | 2.326 ); 133 | 134 | 135 | $table_percentiles = array( 136 | 1 , 137 | 2 , 138 | 3 , 139 | 4 , 140 | 5 , 141 | 6 , 142 | 7 , 143 | 8 , 144 | 9 , 145 | 10 , 146 | 11 , 147 | 12 , 148 | 13 , 149 | 14 , 150 | 15 , 151 | 16 , 152 | 17 , 153 | 18 , 154 | 19 , 155 | 20 , 156 | 21 , 157 | 22 , 158 | 23 , 159 | 24 , 160 | 25 , 161 | 26 , 162 | 27 , 163 | 28 , 164 | 29 , 165 | 30 , 166 | 31 , 167 | 32 , 168 | 33 , 169 | 34 , 170 | 35 , 171 | 36 , 172 | 37 , 173 | 38 , 174 | 39 , 175 | 40 , 176 | 41 , 177 | 42 , 178 | 43 , 179 | 44 , 180 | 45 , 181 | 46 , 182 | 47 , 183 | 48 , 184 | 49 , 185 | 50 , 186 | 51 , 187 | 52 , 188 | 53 , 189 | 54 , 190 | 55 , 191 | 56 , 192 | 57 , 193 | 58 , 194 | 59 , 195 | 60 , 196 | 61 , 197 | 62 , 198 | 63 , 199 | 64 , 200 | 65 , 201 | 66 , 202 | 67 , 203 | 68 , 204 | 69 , 205 | 70 , 206 | 71 , 207 | 72 , 208 | 73 , 209 | 74 , 210 | 75 , 211 | 76 , 212 | 77 , 213 | 78 , 214 | 79 , 215 | 80 , 216 | 81 , 217 | 82 , 218 | 83 , 219 | 84 , 220 | 85 , 221 | 86 , 222 | 87 , 223 | 88 , 224 | 89 , 225 | 90 , 226 | 91 , 227 | 92 , 228 | 93 , 229 | 94 , 230 | 95 , 231 | 96 , 232 | 97 , 233 | 98 , 234 | 99 ); -------------------------------------------------------------------------------- /redis/redis.php: -------------------------------------------------------------------------------- 1 | host = $host; 24 | $this->port = $port; 25 | } 26 | 27 | public function connect() { 28 | if ($this->_sock) return true; 29 | if ($sock = @fsockopen($this->host, $this->port, $errno, $errstr)) { 30 | $this->_sock = $sock; 31 | return true; 32 | } 33 | $msg = "Cannot open socket to {$this->host}:{$this->port}"; 34 | if ($errno || $errmsg) 35 | $msg .= "," . ($errno ? " error $errno" : "") . 36 | ($errmsg ? " $errmsg" : ""); 37 | trigger_error("$msg.", E_USER_WARNING); 38 | return false; // didn't connect 39 | } 40 | 41 | public function disconnect() { 42 | if ($this->_sock) @fclose($this->_sock); 43 | $this->_sock = null; 44 | } 45 | 46 | public function ping() { 47 | $this->connect(); 48 | $this->write("PING\r\n"); 49 | return $this->get_response(); 50 | } 51 | 52 | public function do_echo($s) { 53 | $this->connect(); 54 | $this->write("ECHO " . strlen($s) . "\r\n$s\r\n"); 55 | return $this->get_response(); 56 | } 57 | 58 | public function set($name, $value, $preserve=false) { 59 | $this->connect(); 60 | $this->write( 61 | ($preserve ? 'SETNX' : 'SET') . 62 | " $name " . strlen($value) . "\r\n$value\r\n" 63 | ); 64 | return $this->get_response(); 65 | } 66 | 67 | public function get($name) { 68 | $this->connect(); 69 | $this->write("GET $name\r\n"); 70 | return $this->get_response(); 71 | } 72 | 73 | public function mget($keys) { 74 | $this->connect(); 75 | $this->write("MGET ".implode(" ",$keys)."\r\n"); 76 | return $this->get_response(); 77 | } 78 | 79 | public function incr($name, $amount=1) { 80 | $this->connect(); 81 | if ($amount == 1) 82 | $this->write("INCR $name\r\n"); 83 | else 84 | $this->write("INCRBY $name $amount\r\n"); 85 | return $this->get_response(); 86 | } 87 | 88 | public function decr($name, $amount=1) { 89 | $this->connect(); 90 | if ($amount == 1) 91 | $this->write("DECR $name\r\n"); 92 | else 93 | $this->write("DECRBY $name $amount\r\n"); 94 | return $this->get_response(); 95 | } 96 | 97 | public function exists($name) { 98 | $this->connect(); 99 | $this->write("EXISTS $name\r\n"); 100 | return $this->get_response(); 101 | } 102 | 103 | public function delete($name) { 104 | $this->connect(); 105 | $this->write("DEL $name\r\n"); 106 | return $this->get_response(); 107 | } 108 | 109 | public function keys($pattern) { 110 | $this->connect(); 111 | $this->write("KEYS $pattern\r\n"); 112 | return explode(' ', $this->get_response()); 113 | } 114 | 115 | public function randomkey() { 116 | $this->connect(); 117 | $this->write("RANDOMKEY\r\n"); 118 | return $this->get_response(); 119 | } 120 | 121 | public function rename($src, $dst) { 122 | $this->connect(); 123 | $this->write("RENAME $src $dst\r\n"); 124 | return $this->get_response(); 125 | } 126 | 127 | public function renamenx($src, $dst) { 128 | $this->connect(); 129 | $this->write("RENAMENX $src $dst\r\n"); 130 | return $this->get_response(); 131 | } 132 | 133 | public function expire($name, $time) { 134 | $this->connect(); 135 | $this->write("EXPIRE $name $time\r\n"); 136 | return $this->get_response(); 137 | } 138 | 139 | public function push($name, $value, $tail=true) { 140 | // default is to append the element to the list 141 | $this->connect(); 142 | $this->write( 143 | ($tail ? 'RPUSH' : 'LPUSH') . 144 | " $name " . strlen($value) . "\r\n$value\r\n" 145 | ); 146 | return $this->get_response(); 147 | } 148 | 149 | public function lpush($name, $value) { 150 | return $this->push($name, $value, false); 151 | } 152 | 153 | public function rpush($name, $value) { 154 | return $this->push($name, $value, true); 155 | } 156 | 157 | public function ltrim($name, $start, $end) { 158 | $this->connect(); 159 | $this->write("LTRIM $name $start $end\r\n"); 160 | return $this->get_response(); 161 | } 162 | 163 | public function lindex($name, $index) { 164 | $this->connect(); 165 | $this->write("LINDEX $name $index\r\n"); 166 | return $this->get_response(); 167 | } 168 | 169 | public function pop($name, $tail=true) { 170 | $this->connect(); 171 | $this->write( 172 | ($tail ? 'RPOP' : 'LPOP') . 173 | " $name\r\n" 174 | ); 175 | return $this->get_response(); 176 | } 177 | 178 | public function lpop($name, $value) { 179 | return $this->pop($name, $value, false); 180 | } 181 | 182 | public function rpop($name, $value) { 183 | return $this->pop($name, $value, true); 184 | } 185 | 186 | public function llen($name) { 187 | $this->connect(); 188 | $this->write("LLEN $name\r\n"); 189 | return $this->get_response(); 190 | } 191 | 192 | public function lrange($name, $start, $end) { 193 | $this->connect(); 194 | $this->write("LRANGE $name $start $end\r\n"); 195 | return $this->get_response(); 196 | } 197 | 198 | public function sort($name, $query=false) { 199 | $this->connect(); 200 | $this->write($query == false ? "SORT $name\r\n" : "SORT $name $query\r\n"); 201 | return $this->get_response(); 202 | } 203 | 204 | public function lset($name, $value, $index) { 205 | $this->connect(); 206 | $this->write("LSET $name $index " . strlen($value) . "\r\n$value\r\n"); 207 | return $this->get_response(); 208 | } 209 | 210 | public function sadd($name, $value) { 211 | $this->connect(); 212 | $this->write("SADD $name " . strlen($value) . "\r\n$value\r\n"); 213 | return $this->get_response(); 214 | } 215 | 216 | public function srem($name, $value) { 217 | $this->connect(); 218 | $this->write("SREM $name " . strlen($value) . "\r\n$value\r\n"); 219 | return $this->get_response(); 220 | } 221 | 222 | public function sismember($name, $value) { 223 | $this->connect(); 224 | $this->write("SISMEMBER $name " . strlen($value) . "\r\n$value\r\n"); 225 | return $this->get_response(); 226 | } 227 | 228 | public function sinter($sets) { 229 | $this->connect(); 230 | $this->write('SINTER ' . implode(' ', $sets) . "\r\n"); 231 | return $this->get_response(); 232 | } 233 | 234 | public function smembers($name) { 235 | $this->connect(); 236 | $this->write("SMEMBERS $name\r\n"); 237 | return $this->get_response(); 238 | } 239 | 240 | public function scard($name) { 241 | $this->connect(); 242 | $this->write("SCARD $name\r\n"); 243 | return $this->get_response(); 244 | } 245 | 246 | public function select_db($name) { 247 | $this->connect(); 248 | $this->write("SELECT $name\r\n"); 249 | return $this->get_response(); 250 | } 251 | 252 | public function move($name, $db) { 253 | $this->connect(); 254 | $this->write("MOVE $name $db\r\n"); 255 | return $this->get_response(); 256 | } 257 | 258 | public function save($background=false) { 259 | $this->connect(); 260 | $this->write(($background ? "BGSAVE\r\n" : "SAVE\r\n")); 261 | return $this->get_response(); 262 | } 263 | 264 | public function bgsave($background=false) { 265 | return $this->save(true); 266 | } 267 | 268 | public function lastsave() { 269 | $this->connect(); 270 | $this->write("LASTSAVE\r\n"); 271 | return $this->get_response(); 272 | } 273 | 274 | public function flushdb($all=false) { 275 | $this->connect(); 276 | $this->write($all ? "FLUSHALL\r\n" : "FLUSHDB\r\n"); 277 | return $this->get_response(); 278 | } 279 | 280 | public function flushall() { 281 | return $this->flush(true); 282 | } 283 | 284 | public function dbsize() { 285 | $this->connect(); 286 | $this->write("DBSIZE\r\n"); 287 | return $this->get_response(); 288 | } 289 | 290 | public function info() { 291 | $this->connect(); 292 | $this->write("INFO\r\n"); 293 | $info = array(); 294 | $data =& $this->get_response(); 295 | foreach (explode("\r\n", $data) as $l) { 296 | if (!$l) 297 | continue; 298 | list($k, $v) = explode(':', $l, 2); 299 | $_v = strpos($v, '.') !== false ? (float)$v : (int)$v; 300 | $info[$k] = (string)$_v == $v ? $_v : $v; 301 | } 302 | return $info; 303 | } 304 | 305 | private function write($s) { 306 | while ($s) { 307 | $i = fwrite($this->_sock, $s); 308 | if ($i == 0) // || $i == strlen($s)) 309 | break; 310 | $s = substr($s, $i); 311 | } 312 | } 313 | 314 | private function read($len=1024) { 315 | if ($s = fgets($this->_sock)) 316 | return $s; 317 | $this->disconnect(); 318 | trigger_error("Cannot read from socket.", E_USER_WARNING); 319 | } 320 | 321 | private function get_response() { 322 | $data = trim($this->read()); 323 | $c = $data[0]; 324 | $data = substr($data, 1); 325 | switch ($c) { 326 | case '-': 327 | trigger_error($data, E_USER_WARNING); 328 | break; 329 | case '+': 330 | return $data; 331 | case ':': 332 | $i = strpos($data, '.') !== false ? (int)$data : (float)$data; 333 | if ((string)$i != $data) 334 | trigger_error("Cannot convert data '$c$data' to integer", E_USER_WARNING); 335 | return $i; 336 | case '$': 337 | return $this->get_bulk_reply($c . $data); 338 | case '*': 339 | $num = (int)$data; 340 | if ((string)$num != $data) 341 | trigger_error("Cannot convert multi-response header '$data' to integer", E_USER_WARNING); 342 | $result = array(); 343 | for ($i=0; $i<$num; $i++) 344 | $result[] =& $this->get_response(); 345 | return $result; 346 | default: 347 | trigger_error("Invalid reply type byte: '$c'"); 348 | } 349 | } 350 | 351 | private function get_bulk_reply($data=null) { 352 | if ($data === null) 353 | $data = trim($this->read()); 354 | if ($data == '$-1') 355 | return null; 356 | $c = $data[0]; 357 | $data = substr($data, 1); 358 | $bulklen = (int)$data; 359 | if ((string)$bulklen != $data) 360 | trigger_error("Cannot convert bulk read header '$c$data' to integer", E_USER_WARNING); 361 | if ($c != '$') 362 | trigger_error("Unkown response prefix for '$c$data'", E_USER_WARNING); 363 | $buffer = ''; 364 | while ($bulklen) { 365 | $data = fread($this->_sock,$bulklen); 366 | $bulklen -= strlen($data); 367 | $buffer .= $data; 368 | } 369 | $crlf = fread($this->_sock,2); 370 | return $buffer; 371 | } 372 | } 373 | 374 | /* 375 | $r = new Redis(); 376 | var_dump($r->set("foo","bar")); 377 | var_dump($r->get("foo")); 378 | var_dump($r->info()); 379 | */ -------------------------------------------------------------------------------- /report.php: -------------------------------------------------------------------------------- 1 | Text to send if user hits Cancel button

'; 23 | exit; 24 | } else if ( 25 | $_SERVER['PHP_AUTH_USER'] != $ab_config['ADMIN_USERNAME'] && 26 | $_SERVER['PHP_AUTH_PW'] != $ab_config['ADMIN_PASSWORD'] 27 | ) 28 | { 29 | header('WWW-Authenticate: Basic realm="My Realm"'); 30 | header('HTTP/1.0 401 Unauthorized'); 31 | echo "hi"; 32 | exit; 33 | } else { 34 | //echo "

Hello {$_SERVER['PHP_AUTH_USER']}.

"; 35 | //echo "

You entered {$_SERVER['PHP_AUTH_PW']} as your password.

"; 36 | } 37 | 38 | 39 | 40 | //connect to redis 41 | $r = new Redis($ab_config['redis_host']); 42 | $connected = $r->connect(); 43 | 44 | if ($connected) 45 | { 46 | $r->select_db($ab_config['redis_db_number']); 47 | //$r->flushdb(); 48 | } 49 | else 50 | { 51 | echo "

couldn't connect to redis

"; 52 | die(); 53 | } 54 | 55 | 56 | 57 | //do forced alternative logic if necessary 58 | if (isset($_GET['force'])) 59 | { 60 | $test = $_GET['test']; 61 | $alt = $_GET['alt']; 62 | 63 | if ($alt != "!!null!!") 64 | ab_tests_force_alternative(urldecode($test), urldecode($alt)); 65 | else 66 | ab_tests_force_alternative(urldecode($test), null); 67 | 68 | //redirect to remove get params from URL 69 | header('Location: ' . $_SERVER['PHP_SELF']); 70 | } 71 | 72 | 73 | 74 | //clear a specific test's data 75 | if (isset($_GET['clear-test'])) 76 | { 77 | $test = $_GET['test']; 78 | 79 | erase_data_for_test(urldecode($test)); 80 | 81 | //redirect to remove get params from URL 82 | header('Location: ' . $_SERVER['PHP_SELF']); 83 | } 84 | 85 | 86 | //clear all data if requested -- but maintain forced tests 87 | if (isset($_GET['clear-all-data'])) 88 | { 89 | erase_all_keys_except_forced_keys(); 90 | 91 | //redirect to remove get params from URL 92 | header('Location: ' . $_SERVER['PHP_SELF']); 93 | } 94 | ?> 95 | 96 | a/b testing report 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |

a/b testing report

106 |
107 |

" . $name . "

"; 120 | 121 | //echo "

There are " . $num_alternatives . " alternatives for this test"; 122 | echo "

" . $test['description']; 123 | 124 | 125 | if ($forced_alternative != null) 126 | { 127 | //echo "

" . "Currently forced to show option $forced_alternative for all participants"; 128 | /*echo ' 129 | Clear 130 | '; */ 131 | } 132 | 133 | 134 | $j = 0; 135 | $a = array(); 136 | $total_converted = 0; 137 | $total_conversions = 0; 138 | $total_particants = 0; 139 | $array_of_conversion_rates = array(); 140 | 141 | echo "

"; 142 | foreach ($alternatives as $alt) 143 | { 144 | $p = $a[$alt]['participants'] = (int)ab_tests_total_participants_for_alternative($name,$alt); 145 | $c1 = $a[$alt]['conversions'] = (int)ab_tests_total_conversions_for_alternative($name,$alt); 146 | $c2 = $a[$alt]['converted'] = (int)ab_tests_total_converted_for_alternative($name,$alt); 147 | 148 | if ($p > 0) 149 | $conv_rate = $c2 / $p * 100; 150 | else 151 | $conv_rate = 0; 152 | 153 | $conv_rate = round($conv_rate,3); 154 | 155 | $array_of_conversion_rates[] = array( $j+1,$conv_rate); 156 | echo "

Alternative " . $alt . ":"; 157 | 158 | if ($forced_alternative != $alt) 159 | { 160 | echo ' 161 | Show Always 162 | '; 163 | } 164 | else 165 | { 166 | echo ' Always Showing 167 | Clear 168 | '; 169 | } 170 | echo "

\n"; 171 | 172 | echo "

$p Participants, $c1 conversions, $c2 converted\n"; 173 | 174 | echo "

conversion rate: " . $conv_rate . "%"; 175 | 176 | $total_particants += $a[$alt]['participants']; 177 | $total_converted += $a[$alt]['converted']; 178 | 179 | $j++; 180 | } 181 | echo "

"; 182 | 183 | 184 | 185 | $total_conversion_rate = ($total_particants > 0)?($total_converted/$total_particants*100):0; 186 | echo "

Total:

"; 187 | echo "

" . $total_particants . " participants, " . $total_converted . " converted"; 188 | echo " = " . round($total_conversion_rate,2) . "% total conversion rate"; 189 | 190 | //echo 'Test is active:

Stop this test



'; 191 | 192 | $metrics = $test['metrics']; 193 | 194 | $metrics = implode(", ", $metrics); 195 | 196 | echo "

(For this test, 197 | a conversion is marked by any of these events: $metrics 198 | )

"; 199 | 200 | 201 | $chart_data = json_encode($array_of_conversion_rates); 202 | 203 | //echo "
";
204 |     //print_r ($chart_data);
205 |     //echo "
"; 206 | 207 | echo ' 208 |
209 | 210 | 211 | 255 | '; 256 | 257 | $conclusion = ab_tests_conclusion($test['name']); 258 | 259 | echo "

" . $conclusion . "

"; 260 | 261 | echo '
Clear This Test'; 262 | 263 | 264 | echo "
"; 265 | 266 | 267 | 268 | 269 | 270 | $i++; 271 | } 272 | ?> 273 | 274 | 275 |

metrics report

276 |
277 | 278 | ' . $metric['name'] . ''; 284 | echo "

" . $metric['description']; 285 | 286 | $today = date('Y-m-d'); 287 | $sixtydaysago = date('Y-m-d', strtotime("-60 days")); 288 | 289 | $d = json_encode(ab_values($metric['name'],$sixtydaysago,$today)); 290 | //print_r($d); 291 | 292 | 293 | echo ' 294 |

295 | 296 | 297 | 349 | '; 350 | 351 | 352 | $i ++; 353 | } 354 | 355 | echo ""; 356 | 357 | ?> 358 | 359 | 360 | 361 | 362 |
363 | Clear All Data 364 | info(); 367 | $mem = $info['used_memory']; 368 | 369 | $mem = round($mem / 1024, 1); 370 | 371 | $num_keys = $r->dbsize(); 372 | 373 | echo "

Memory used: $mem KBytes, Number of keys: $num_keys"; 374 | 375 | ?> 376 | 377 | --------------------------------------------------------------------------------