├── .gitignore ├── LICENSE ├── README.md ├── scripts ├── create_torque_log_table.sql └── dbdump_to_csv.sh └── web ├── auth_app.php ├── auth_functions.php ├── auth_user.php ├── creds-sample.php ├── data └── torque_keys.csv ├── del_session.php ├── export.php ├── get_columns.php ├── get_sessions.php ├── merge_sessions.php ├── parse_functions.php ├── plot.php ├── session.php ├── static ├── css │ ├── background.png │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── chosen-sprite.png │ ├── chosen-sprite@2x.png │ ├── chosen.css │ ├── chosen.min.css │ └── torque.css └── js │ ├── bootstrap.min.js │ ├── chosen.jquery.min.js │ ├── excanvas.js │ ├── excanvas.min.js │ ├── jquery.colorhelpers.js │ ├── jquery.flot.animator.js │ ├── jquery.flot.animator.min.js │ ├── jquery.flot.axislabels.js │ ├── jquery.flot.canvas.js │ ├── jquery.flot.categories.js │ ├── jquery.flot.crosshair.js │ ├── jquery.flot.downsample.js │ ├── jquery.flot.errorbars.js │ ├── jquery.flot.fillbetween.js │ ├── jquery.flot.grow.js │ ├── jquery.flot.hiddengraphs.js │ ├── jquery.flot.image.js │ ├── jquery.flot.js │ ├── jquery.flot.multihighlight-delta.js │ ├── jquery.flot.navigate.js │ ├── jquery.flot.pie.js │ ├── jquery.flot.resize.js │ ├── jquery.flot.resize.min.js │ ├── jquery.flot.selection.js │ ├── jquery.flot.smoother.js │ ├── jquery.flot.stack.js │ ├── jquery.flot.symbol.js │ ├── jquery.flot.threshold.js │ ├── jquery.flot.time.js │ ├── jquery.flot.tooltip.js │ ├── jquery.flot.tooltip.min.js │ ├── jquery.flot.updater.js │ ├── jquery.flot.valuelabels.js │ ├── jquery.min.js │ ├── jquery.peity.min.js │ └── torquehelpers.js ├── timezone.php ├── upload_data.php └── url.php /.gitignore: -------------------------------------------------------------------------------- 1 | creds.php 2 | torque_data 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) {{{2014}}} {{{Matt Nicklay}}} 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo contains everything needed to setup an interface for uploading ODB2 data logged from your car in real-time using the [Torque Pro](https://play.google.com/store/apps/details?id=org.prowl.torque) app for Android. 2 | 3 | The interface allows the user to: 4 | 5 | * View a Google Map showing your trips logged via Torque 6 | * Create time series plots of OBD2 data 7 | * Easily export data to CSV or JSON 8 | 9 | # Demo # 10 | 11 | [Check out the demo!](http://data.mattnicklay.com/torque/session.php?id=1404557450999) 12 | 13 | # Requirements # 14 | 15 | These instructions assume you already have a LAMP-like server (on a Linux/UNIX based host) or have access to one. Specifically, you'll need the following: 16 | 17 | * MySQL database 18 | * Apache webserver 19 | * PHP server-side scripting 20 | 21 | If in doubt, I'd recommend using Ubuntu 12.04 LTS. 22 | 23 | # Server Setup # 24 | 25 | First clone the repo: 26 | 27 | ```bash 28 | git clone https://github.com/econpy/torque 29 | cd torque 30 | ``` 31 | 32 | ### Configure MySQL ### 33 | 34 | To get started, create a database named `torque` and a user with permission to insert and read data from the database. In this tutorial, we'll create a user `steve` with password `zissou` that has access to all tables in the database `torque` from `localhost`: 35 | 36 | ```sql 37 | CREATE DATABASE torque; 38 | CREATE USER 'steve'@'localhost' IDENTIFIED BY 'zissou'; 39 | GRANT USAGE, FILE TO 'steve'@'localhost'; 40 | GRANT ALL PRIVILEGES ON torque.* TO 'steve'@'localhost'; 41 | FLUSH PRIVILEGES; 42 | ``` 43 | 44 | Then create a table in the database to store the logged data using the `create_torque_log_table.sql` file provided in the `scripts` folder of this repo: 45 | 46 | ```bash 47 | mysql -u yoursqlusername -p < scripts/create_torque_log_table.sql 48 | ``` 49 | 50 | 51 | ### Configure Webserver ### 52 | 53 | Move the contents of the `web` folder to your webserver and set the appropriate permissions. For example, using an Apache server located at `/var/www`: 54 | 55 | ```bash 56 | mv web /var/www/torque 57 | cd /var/www/torque 58 | find . -type d -exec chmod 755 {} + 59 | find . -type f -exec chmod 644 {} + 60 | ``` 61 | 62 | Rename the `creds-sample.php` file to `creds.php`: 63 | 64 | ```bash 65 | mv creds-sample.php creds.php 66 | ``` 67 | 68 | Then edit/enter your MySQL username and password in the empty **$db_user** and **$db_pass** fields: 69 | 70 | ```php 71 | ... 72 | $db_host = "localhost"; 73 | $db_user = "steve"; 74 | $db_pass = "zissou"; 75 | $db_name = "torque"; 76 | $db_table = "raw_logs"; 77 | ... 78 | ``` 79 | 80 | 81 | # Settings in Torque App # 82 | 83 | To use your database/server with Torque, open the app on your phone and navigate to: 84 | 85 | ``` 86 | Settings -> Data Logging & Upload -> Webserver URL 87 | ``` 88 | 89 | Enter the URL to your **upload_data.php** script and press `OK`. Test that it works by clicking `Test settings` and you should see a success message like the image on the right: 90 | 91 |
92 | 93 | The final thing you'll want to do before going for a drive is to check the appropriate boxes on the `Data Logging & Upload` page under the `REALTIME WEB UPLOAD` section. Personally, I have both **Upload to webserver** and **Only when ODB connected** checked. 94 | 95 | At this point, you should be all setup. The next time you connect to Torque in your car, data will begin syncing into your MySQL database in real-time! 96 | -------------------------------------------------------------------------------- /scripts/create_torque_log_table.sql: -------------------------------------------------------------------------------- 1 | USE `torque`; 2 | 3 | DROP TABLE IF EXISTS `raw_logs`; 4 | CREATE TABLE `raw_logs` ( 5 | `v` varchar(1) NOT NULL, 6 | `session` varchar(15) NOT NULL, 7 | `id` varchar(32) NOT NULL, 8 | `time` varchar(15) NOT NULL, 9 | `kff1005` float NOT NULL DEFAULT '0', 10 | `kff1006` float NOT NULL DEFAULT '0', 11 | `kff1001` float NOT NULL DEFAULT '0' COMMENT 'Speed (GPS)', 12 | `kff1007` float NOT NULL DEFAULT '0' COMMENT 'GPS Bearing', 13 | `k4` float NOT NULL DEFAULT '0' COMMENT 'Engine Load', 14 | `k2f` float NOT NULL DEFAULT '0' COMMENT 'Fuel Level', 15 | `k11` float NOT NULL DEFAULT '0' COMMENT 'Throttle Position', 16 | `k5` float NOT NULL DEFAULT '0' COMMENT 'Engine Coolant Temp', 17 | `kc` float NOT NULL DEFAULT '0' COMMENT 'Engine RPM', 18 | `kd` float NOT NULL DEFAULT '0' COMMENT 'Speed (OBD)', 19 | `kf` float NOT NULL DEFAULT '0' COMMENT 'Intake Air Temp', 20 | `kff1226` float NOT NULL DEFAULT '0' COMMENT 'Horsepower', 21 | `kff1220` float NOT NULL DEFAULT '0' COMMENT 'Accel (X)', 22 | `kff1221` float NOT NULL DEFAULT '0' COMMENT 'Accel (Y)', 23 | `k46` float NOT NULL DEFAULT '0' COMMENT 'Ambiant Air Temp', 24 | KEY `session` (`session`,`id`), 25 | KEY `id` (`id`) 26 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 27 | -------------------------------------------------------------------------------- /scripts/dbdump_to_csv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Default MySQL database and table names 4 | DBNAME=torque 5 | TABLE=raw_logs 6 | 7 | # Location of your MySQL options file 8 | OPTFILE=$HOME/.my.cnf 9 | 10 | # Define full path of current directory 11 | CURDIR=$(cd -P -- "$(dirname -- "$0")" && pwd -P) 12 | 13 | # Make a `torque_data` directory if it doesn't already exist 14 | mkdir -p $CURDIR/torque_data 15 | OUTDIR=$CURDIR/torque_data 16 | 17 | # Create a temporary directory where MySQL should be able to write 18 | TMPDIR=/tmp/torque_data 19 | mkdir -p $TMPDIR 20 | chmod a+rwx $TMPDIR 21 | TMPFILE=$TMPDIR/tempfile.csv 22 | 23 | # Define the name of the output CSV file 24 | FNAME=$OUTDIR/$(date +%Y.%m.%d)-$DBNAME.csv 25 | 26 | # Create an empty file and set up column names 27 | mysql --defaults-file=$OPTFILE $DBNAME -B -e "SELECT COLUMN_NAME FROM information_schema.COLUMNS C WHERE table_name = '$TABLE';" | awk '{print $1}' | grep -iv ^COLUMN_NAME$ | sed 's/^/"/g;s/$/"/g' | tr '\n' ',' > $FNAME 28 | 29 | # Append newline to mark beginning of data vs. column titles 30 | echo "" >> $FNAME 31 | 32 | # Dump data from DB into temp file 33 | mysql --defaults-file=$OPTFILE $DBNAME -B -e "SELECT * INTO OUTFILE '$TMPFILE' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' FROM $TABLE;" 34 | 35 | # Merge data file and file with column names 36 | cat $TMPFILE >> $FNAME 37 | 38 | # Delete temp file 39 | rm -rf $TMPFILE 40 | 41 | echo "File saved to:" $FNAME 42 | -------------------------------------------------------------------------------- /web/auth_app.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/auth_functions.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/auth_user.php: -------------------------------------------------------------------------------- 1 | 58 | 59 | 60 | 61 | 62 | 63 | Open Torque Viewer 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /web/creds-sample.php: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /web/data/torque_keys.csv: -------------------------------------------------------------------------------- 1 | id,Device ID 2 | k10,Mass Air Flow Rate 3 | k11,Throttle Position(Manifold) 4 | k12,Air Status 5 | k14,Fuel trim bank 1 sensor 1 6 | k15,Fuel trim bank 1 sensor 2 7 | k16,Fuel trim bank 1 sensor 3 8 | k17,Fuel trim bank 1 sensor 4 9 | k18,Fuel trim bank 2 sensor 1 10 | k19,Fuel trim bank 2 sensor 2 11 | k1a,Fuel trim bank 2 sensor 3 12 | k1b,Fuel trim bank 2 sensor 4 13 | k1f,Run time since engine start 14 | k21,Distance travelled with MIL/CEL lit 15 | k22,Fuel Rail Pressure (relative to manifold vacuum) 16 | k23,Fuel Rail Pressure 17 | k24,O2 Sensor1 Equivalence Ratio 18 | k25,O2 Sensor2 Equivalence Ratio 19 | k26,O2 Sensor3 Equivalence Ratio 20 | k27,O2 Sensor4 Equivalence Ratio 21 | k28,O2 Sensor5 Equivalence Ratio 22 | k29,O2 Sensor6 Equivalence Ratio 23 | k2a,O2 Sensor7 Equivalence Ratio 24 | k2b,O2 Sensor8 Equivalence Ratio 25 | k2c,EGR Commanded 26 | k2d,EGR Error 27 | k2f,Fuel Level (From Engine ECU) 28 | k3,Fuel Status 29 | k31,Distance travelled since codes cleared 30 | k32,Evap System Vapour Pressure 31 | k33,Barometric pressure (from vehicle) 32 | k34,O2 Sensor1 Equivalence Ratio(alternate) 33 | k3c,Catalyst Temperature (Bank 1 Sensor 1) 34 | k3d,Catalyst Temperature (Bank 2 Sensor 1) 35 | k3e,Catalyst Temperature (Bank 1 Sensor 2) 36 | k3f,Catalyst Temperature (Bank 2 Sensor 2) 37 | k4,Engine Load 38 | k42,Voltage (Control Module) 39 | k43,Engine Load(Absolute) 40 | k44,Commanded Equivalence Ratio(lambda) 41 | k45,Relative Throttle Position 42 | k46,Ambient air temp 43 | k47,Absolute Throttle Position B 44 | k49,Accelerator PedalPosition D 45 | k4a,Accelerator PedalPosition E 46 | k4b,Accelerator PedalPosition F 47 | k5,Engine Coolant Temperature 48 | k52,Ethanol Fuel % 49 | k5a,Relative Accelerator Pedal Position 50 | k5c,Engine Oil Temperature 51 | k6,Fuel Trim Bank 1 Short Term 52 | k7,Fuel Trim Bank 1 Long Term 53 | k78,Exhaust Gas Temperature 1 54 | k79,Exhaust Gas Temperature 2 55 | k8,Fuel Trim Bank 2 Short Term 56 | k9,Fuel Trim Bank 2 Long Term 57 | ka,Fuel pressure 58 | kb,Intake Manifold Pressure 59 | kb4,Transmission Temperature(Method 2) 60 | kc,Engine RPM 61 | kd,Speed (OBD) 62 | ke,Timing Advance 63 | kf,Intake Air Temperature 64 | kfe1805,Transmission Temperature(Method 1) 65 | kff1001,Speed (GPS) 66 | kff1005,GPS Longitude 67 | kff1006,GPS Latitude 68 | kff1010,GPS Altitude 69 | kff1201,Miles Per Gallon(Instant) 70 | kff1202,Turbo Boost & Vacuum Gauge 71 | kff1203,Kilometers Per Litre(Instant) 72 | kff1204,Trip Distance 73 | kff1205,Trip average MPG 74 | kff1206,Trip average KPL 75 | kff1207,Litres Per 100 Kilometer(Instant) 76 | kff1208,Trip average Litres/100 KM 77 | kff120c,Trip distance (stored in vehicle profile) 78 | kff1214,O2 Volts Bank 1 sensor 1 79 | kff1215,O2 Volts Bank 1 sensor 2 80 | kff1216,O2 Volts Bank 1 sensor 3 81 | kff1217,O2 Volts Bank 1 sensor 4 82 | kff1218,O2 Volts Bank 2 sensor 1 83 | kff1219,O2 Volts Bank 2 sensor 2 84 | kff121a,O2 Volts Bank 2 sensor 3 85 | kff121b,O2 Volts Bank 2 sensor 4 86 | kff1220,Acceleration Sensor(X axis) 87 | kff1221,Acceleration Sensor(Y axis) 88 | kff1222,Acceleration Sensor(Z axis) 89 | kff1223,Acceleration Sensor(Total) 90 | kff1225,Torque 91 | kff1226,Horsepower (At the wheels) 92 | kff122d,0-60mph Time 93 | kff122e,0-100kph Time 94 | kff122f,1/4 mile time 95 | kff1230,1/8 mile time 96 | kff1237,GPS vs OBD Speed difference 97 | kff1238,Voltage (OBD Adapter) 98 | kff1239,GPS Accuracy 99 | kff123a,GPS Satellites 100 | kff123b,GPS Bearing 101 | kff1240,O2 Sensor1 wide-range Voltage 102 | kff1241,O2 Sensor2 wide-range Voltage 103 | kff1242,O2 Sensor3 wide-range Voltage 104 | kff1243,O2 Sensor4 wide-range Voltage 105 | kff1244,O2 Sensor5 wide-range Voltage 106 | kff1245,O2 Sensor6 wide-range Voltage 107 | kff1246,O2 Sensor7 wide-range Voltage 108 | kff1247,O2 Sensor8 wide-range Voltage 109 | kff1249,Air Fuel Ratio(Measured) 110 | kff124a,Tilt(x) 111 | kff124b,Tilt(y) 112 | kff124c,Tilt(z) 113 | kff124d,Air Fuel Ratio(Commanded) 114 | kff124f,0-200kph Time 115 | kff1257,CO₂ in g/km (Instantaneous) 116 | kff1258,CO₂ in g/km (Average) 117 | kff125a,Fuel flow rate/minute 118 | kff125c,Fuel cost (trip) 119 | kff125d,Fuel flow rate/hour 120 | kff125e,60-120mph Time 121 | kff125f,60-80mph Time 122 | kff1260,40-60mph Time 123 | kff1261,80-100mph Time 124 | kff1263,Average trip speed(whilst moving only) 125 | kff1264,100-0kph Time 126 | kff1265,60-0mph Time 127 | kff1266,Trip Time(Since journey start) 128 | kff1267,Trip time(whilst stationary) 129 | kff1268,Trip Time(whilst moving) 130 | kff1269,Volumetric Efficiency (Calculated) 131 | kff126a,Distance to empty (Estimated) 132 | kff126b,Fuel Remaining (Calculated from vehicle profile) 133 | kff126d,Cost per mile/km (Instant) 134 | kff126e,Cost per mile/km (Trip) 135 | kff1270,Barometer (on Android device) 136 | kff1271,Fuel used (trip) 137 | kff1272,Average trip speed(whilst stopped or moving) 138 | kff1273,Engine kW (At the wheels) 139 | kff1275,80-120kph Time 140 | kff1276,60-130mph Time 141 | kff1277,0-30mph Time 142 | kff5201,Miles Per Gallon(Long Term Average) 143 | kff5202,Kilometers Per Litre(Long Term Average) 144 | kff5203,Litres Per 100 Kilometer(Long Term Average) 145 | session,Session ID 146 | time,Timestamp 147 | -------------------------------------------------------------------------------- /web/del_session.php: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /web/export.php: -------------------------------------------------------------------------------- 1 | 70 | -------------------------------------------------------------------------------- /web/get_columns.php: -------------------------------------------------------------------------------- 1 | $x[0], "colcomment"=>$x[1]); 17 | } 18 | } 19 | 20 | $numcols = strval(count($coldata)+1); 21 | 22 | mysql_free_result($colqry); 23 | 24 | 25 | //TODO: Do this once in a dedicated file 26 | if (isset($_POST["id"])) { 27 | $session_id = preg_replace('/\D/', '', $_POST['id']); 28 | } 29 | elseif (isset($_GET["id"])) { 30 | $session_id = preg_replace('/\D/', '', $_GET['id']); 31 | } 32 | 33 | 34 | // If we have a certain session, check which colums contain no information at all 35 | $coldataempty = array(); 36 | if (isset($session_id)) { 37 | mysql_select_db($db_name) or die(mysql_error()); 38 | 39 | //Count distinct values for each known column 40 | //TODO: Unroll loop into single query 41 | foreach ($coldata as $col) 42 | { 43 | $colname = $col["colname"]; 44 | 45 | // Count number of different values for this specific field 46 | $colqry = mysql_query("SELECT count(DISTINCT $colname)<2 as $colname 47 | FROM $db_table 48 | WHERE session=$session_id") or die(mysql_error()); 49 | $colresult = mysql_fetch_assoc($colqry); 50 | $coldataempty[$colname] = $colresult[$colname]; 51 | } 52 | 53 | //print_r($coldataempty); 54 | } 55 | 56 | mysql_close(); 57 | 58 | ?> 59 | -------------------------------------------------------------------------------- /web/get_sessions.php: -------------------------------------------------------------------------------- 1 | = 60) { 28 | $sid = $row["session"]; 29 | $sids[] = preg_replace('/\D/', '', $sid); 30 | $seshdates[$sid] = date("F d, Y h:ia", substr($sid, 0, -3)); 31 | $seshsizes[$sid] = " (Length $session_duration_str)"; 32 | } 33 | else {} 34 | } 35 | 36 | mysql_free_result($sessionqry); 37 | mysql_close($con); 38 | 39 | ?> 40 | -------------------------------------------------------------------------------- /web/merge_sessions.php: -------------------------------------------------------------------------------- 1 | 45 | -------------------------------------------------------------------------------- /web/parse_functions.php: -------------------------------------------------------------------------------- 1 | $intvalindex+1) 62 | $result = $floatval*($data[$intvalindex+1] - $data[$intvalindex]) + $data[$intvalindex]; 63 | else 64 | $result = $data[$intvalindex]; 65 | } 66 | return $result; 67 | } 68 | 69 | // Make comma separated string for sparkline data. 70 | function make_spark_data($sparkarry) { 71 | return implode(",", array_reverse($sparkarray)); 72 | } 73 | 74 | 75 | ?> 76 | -------------------------------------------------------------------------------- /web/plot.php: -------------------------------------------------------------------------------- 1 | val mappings 14 | $js = CSVtoJSON("./data/torque_keys.csv"); 15 | $jsarr = json_decode($js, TRUE); 16 | 17 | // The columns to plot -- if no PIDs are specified I default to intake temp and OBD speed 18 | if (isset($_GET["s1"])) { 19 | $v1 = mysql_real_escape_string($_GET['s1']); 20 | } 21 | else { 22 | $v1 = "kd"; // OBD Speed 23 | } 24 | if (isset($_GET["s2"])) { 25 | $v2 = mysql_real_escape_string($_GET['s2']); 26 | } 27 | else { 28 | $v2 = "kf"; // Intake Air Temp 29 | } 30 | 31 | // Grab the label for each PID to be used in the plot 32 | $v1_label = '"'.$jsarr[$v1].'"'; 33 | $v2_label = '"'.$jsarr[$v2].'"'; 34 | 35 | // Get data for session 36 | $sessionqry = mysql_query("SELECT time,$v1,$v2 37 | FROM $db_table 38 | WHERE session=$session_id 39 | ORDER BY time DESC;") or die(mysql_error()); 40 | 41 | //Speed conversion 42 | if (!$source_is_miles && $use_miles) 43 | { 44 | $speed_factor = 0.621371; 45 | $speed_measurand = ' [mph]'; 46 | } 47 | elseif ($source_is_miles && $use_miles) 48 | { 49 | $speed_factor = 1.0; 50 | $speed_measurand = ' [mph]'; 51 | } 52 | elseif ($source_is_miles && !$use_miles) 53 | { 54 | $speed_factor = 1.609344; 55 | $speed_measurand = ' [km/h]'; 56 | } 57 | else 58 | { 59 | $speed_factor = 1.0; 60 | $speed_measurand = ' [km/h]'; 61 | } 62 | 63 | //Temperature Conversion 64 | //From Celsius to Fahrenheit 65 | if (!$source_is_fahrenheit && $use_fahrenheit) 66 | { 67 | $temp_func = function ($temp) { return $temp*9.0/5.0+32.0; }; 68 | $temp_measurand = ' [°F]'; 69 | } 70 | //Just Fahrenheit 71 | elseif ($source_is_fahrenheit && $use_fahrenheit) 72 | { 73 | $temp_func = function ($temp) { return $temp; }; 74 | $temp_measurand = ' [°F]'; 75 | } 76 | //From Fahrenheit to Celsius 77 | elseif ($source_is_fahrenheit && !$use_fahrenheit) 78 | { 79 | $temp_func = function ($temp) { return ($temp-32.0)*5.0/9.0; }; 80 | $temp_measurand = ' [°C]'; 81 | } 82 | //Just Celsius 83 | else 84 | { 85 | $temp_func = function ($temp) { return $temp; }; 86 | $temp_measurand = ' [°C]'; 87 | } 88 | 89 | // Convert data units 90 | // TODO: Use the userDefault fields to do these conversions dynamically 91 | while($row = mysql_fetch_assoc($sessionqry)) { 92 | // data column #1 93 | if (substri_count($jsarr[$v1], "Speed") > 0) { 94 | $x = intval($row[$v1]) * $speed_factor; 95 | $v1_measurand = $speed_measurand; 96 | } 97 | elseif (substri_count($jsarr[$v1], "Temp") > 0) { 98 | $x = $temp_func ( floatval($row[$v1]) ); 99 | $v1_measurand = $temp_measurand; 100 | } 101 | else { 102 | $x = intval($row[$v1]); 103 | $v1_measurand = ''; 104 | } 105 | $d1[] = array($row['time'], $x); 106 | $spark1[] = $x; 107 | 108 | // data column #2 109 | if (substri_count($jsarr[$v2], "Speed") > 0) { 110 | $x = intval($row[$v2]) * $speed_factor; 111 | $v2_measurand = $speed_measurand; 112 | } 113 | elseif (substri_count($jsarr[$v2], "Temp") > 0) { 114 | $x = $temp_func ( floatval($row[$v2]) ); 115 | $v2_measurand = $temp_measurand; 116 | } 117 | else { 118 | $x = intval($row[$v2]); 119 | $v2_measurand = ''; 120 | } 121 | $d2[] = array($row['time'], $x); 122 | $spark2[] = $x; 123 | } 124 | 125 | $v1_label = '"'.$jsarr[$v1].$v1_measurand.'"'; 126 | $v2_label = '"'.$jsarr[$v2].$v2_measurand.'"'; 127 | 128 | $sparkdata1 = implode(",", array_reverse($spark1)); 129 | $sparkdata2 = implode(",", array_reverse($spark2)); 130 | $max1 = round(max($spark1), 1); 131 | $max2 = round(max($spark2), 1); 132 | $min1 = round(min($spark1), 1); 133 | $min2 = round(min($spark2), 1); 134 | $avg1 = round(average($spark1), 1); 135 | $avg2 = round(average($spark2), 1); 136 | $pcnt25data1 = round(calc_percentile($spark1, 25), 1); 137 | $pcnt25data2 = round(calc_percentile($spark2, 25), 1); 138 | $pcnt75data1 = round(calc_percentile($spark1, 75), 1); 139 | $pcnt75data2 = round(calc_percentile($spark2, 75), 1); 140 | 141 | } 142 | 143 | else { 144 | 145 | } 146 | 147 | ?> 148 | -------------------------------------------------------------------------------- /web/static/css/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/econpy/torque/f84131704ff0cb838be0c7689001c3948d9d49e2/web/static/css/background.png -------------------------------------------------------------------------------- /web/static/css/chosen-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/econpy/torque/f84131704ff0cb838be0c7689001c3948d9d49e2/web/static/css/chosen-sprite.png -------------------------------------------------------------------------------- /web/static/css/chosen-sprite@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/econpy/torque/f84131704ff0cb838be0c7689001c3948d9d49e2/web/static/css/chosen-sprite@2x.png -------------------------------------------------------------------------------- /web/static/css/chosen.min.css: -------------------------------------------------------------------------------- 1 | /* Chosen v1.1.0 | (c) 2011-2013 by Harvest | MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md */ 2 | 3 | .chosen-container{position:relative;display:inline-block;vertical-align:middle;font-size:13px;zoom:1;*display:inline;-webkit-user-select:none;-moz-user-select:none;user-select:none}.chosen-container .chosen-drop{position:absolute;top:100%;left:-9999px;z-index:1010;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;width:100%;border:1px solid #aaa;border-top:0;background:#fff;box-shadow:0 4px 5px rgba(0,0,0,.15)}.chosen-container.chosen-with-drop .chosen-drop{left:0}.chosen-container a{cursor:pointer}.chosen-container-single .chosen-single{position:relative;display:block;overflow:hidden;padding:0 0 0 8px;height:23px;border:1px solid #aaa;border-radius:5px;background-color:#fff;background:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#fff),color-stop(50%,#f6f6f6),color-stop(52%,#eee),color-stop(100%,#f4f4f4));background:-webkit-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background:-moz-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background:-o-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background:linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background-clip:padding-box;box-shadow:0 0 3px #fff inset,0 1px 1px rgba(0,0,0,.1);color:#444;text-decoration:none;white-space:nowrap;line-height:24px}.chosen-container-single .chosen-default{color:#999}.chosen-container-single .chosen-single span{display:block;overflow:hidden;margin-right:26px;text-overflow:ellipsis;white-space:nowrap}.chosen-container-single .chosen-single-with-deselect span{margin-right:38px}.chosen-container-single .chosen-single abbr{position:absolute;top:6px;right:26px;display:block;width:12px;height:12px;background:url(chosen-sprite.png) -42px 1px no-repeat;font-size:1px}.chosen-container-single .chosen-single abbr:hover{background-position:-42px -10px}.chosen-container-single.chosen-disabled .chosen-single abbr:hover{background-position:-42px -10px}.chosen-container-single .chosen-single div{position:absolute;top:0;right:0;display:block;width:18px;height:100%}.chosen-container-single .chosen-single div b{display:block;width:100%;height:100%;background:url(chosen-sprite.png) no-repeat 0 2px}.chosen-container-single .chosen-search{position:relative;z-index:1010;margin:0;padding:3px 4px;white-space:nowrap}.chosen-container-single .chosen-search input[type=text]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;margin:1px 0;padding:4px 20px 4px 5px;width:100%;height:auto;outline:0;border:1px solid #aaa;background:#fff url(chosen-sprite.png) no-repeat 100% -20px;background:url(chosen-sprite.png) no-repeat 100% -20px;font-size:1em;font-family:sans-serif;line-height:normal;border-radius:0}.chosen-container-single .chosen-drop{margin-top:-1px;border-radius:0 0 4px 4px;background-clip:padding-box}.chosen-container-single.chosen-container-single-nosearch .chosen-search{position:absolute;left:-9999px}.chosen-container .chosen-results{position:relative;overflow-x:hidden;overflow-y:auto;margin:0 4px 4px 0;padding:0 0 0 4px;max-height:240px;-webkit-overflow-scrolling:touch}.chosen-container .chosen-results li{display:none;margin:0;padding:5px 6px;list-style:none;line-height:15px;-webkit-touch-callout:none}.chosen-container .chosen-results li.active-result{display:list-item;cursor:pointer}.chosen-container .chosen-results li.disabled-result{display:list-item;color:#ccc;cursor:default}.chosen-container .chosen-results li.highlighted{background-color:#3875d7;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#3875d7),color-stop(90%,#2a62bc));background-image:-webkit-linear-gradient(#3875d7 20%,#2a62bc 90%);background-image:-moz-linear-gradient(#3875d7 20%,#2a62bc 90%);background-image:-o-linear-gradient(#3875d7 20%,#2a62bc 90%);background-image:linear-gradient(#3875d7 20%,#2a62bc 90%);color:#fff}.chosen-container .chosen-results li.no-results{display:list-item;background:#f4f4f4}.chosen-container .chosen-results li.group-result{display:list-item;font-weight:700;cursor:default}.chosen-container .chosen-results li.group-option{padding-left:15px}.chosen-container .chosen-results li em{font-style:normal;text-decoration:underline}.chosen-container-multi .chosen-choices{position:relative;overflow:hidden;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;width:100%;height:auto!important;height:1%;border:1px solid #aaa;background-color:#fff;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(1%,#eee),color-stop(15%,#fff));background-image:-webkit-linear-gradient(#eee 1%,#fff 15%);background-image:-moz-linear-gradient(#eee 1%,#fff 15%);background-image:-o-linear-gradient(#eee 1%,#fff 15%);background-image:linear-gradient(#eee 1%,#fff 15%);cursor:text}.chosen-container-multi .chosen-choices li{float:left;list-style:none}.chosen-container-multi .chosen-choices li.search-field{margin:0;padding:0;white-space:nowrap}.chosen-container-multi .chosen-choices li.search-field input[type=text]{margin:1px 0;padding:5px;height:15px;outline:0;border:0!important;background:transparent!important;box-shadow:none;color:#666;font-size:100%;font-family:sans-serif;line-height:normal;border-radius:0}.chosen-container-multi .chosen-choices li.search-field .default{color:#999}.chosen-container-multi .chosen-choices li.search-choice{position:relative;margin:3px 0 3px 5px;padding:3px 20px 3px 5px;border:1px solid #aaa;border-radius:3px;background-color:#e4e4e4;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#f4f4f4),color-stop(50%,#f0f0f0),color-stop(52%,#e8e8e8),color-stop(100%,#eee));background-image:-webkit-linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-moz-linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-o-linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-clip:padding-box;box-shadow:0 0 2px #fff inset,0 1px 0 rgba(0,0,0,.05);color:#333;line-height:13px;cursor:default}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close{position:absolute;top:4px;right:3px;display:block;width:12px;height:12px;background:url(chosen-sprite.png) -42px 1px no-repeat;font-size:1px}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover{background-position:-42px -10px}.chosen-container-multi .chosen-choices li.search-choice-disabled{padding-right:5px;border:1px solid #ccc;background-color:#e4e4e4;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#f4f4f4),color-stop(50%,#f0f0f0),color-stop(52%,#e8e8e8),color-stop(100%,#eee));background-image:-webkit-linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-moz-linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-o-linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);color:#666}.chosen-container-multi .chosen-choices li.search-choice-focus{background:#d4d4d4}.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close{background-position:-42px -10px}.chosen-container-multi .chosen-results{margin:0;padding:0}.chosen-container-multi .chosen-drop .result-selected{display:list-item;color:#ccc;cursor:default}.chosen-container-active .chosen-single{border:1px solid #5897fb;box-shadow:0 0 5px rgba(0,0,0,.3)}.chosen-container-active.chosen-with-drop .chosen-single{border:1px solid #aaa;-moz-border-radius-bottomright:0;border-bottom-right-radius:0;-moz-border-radius-bottomleft:0;border-bottom-left-radius:0;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#eee),color-stop(80%,#fff));background-image:-webkit-linear-gradient(#eee 20%,#fff 80%);background-image:-moz-linear-gradient(#eee 20%,#fff 80%);background-image:-o-linear-gradient(#eee 20%,#fff 80%);background-image:linear-gradient(#eee 20%,#fff 80%);box-shadow:0 1px 0 #fff inset}.chosen-container-active.chosen-with-drop .chosen-single div{border-left:0;background:transparent}.chosen-container-active.chosen-with-drop .chosen-single div b{background-position:-18px 2px}.chosen-container-active .chosen-choices{border:1px solid #5897fb;box-shadow:0 0 5px rgba(0,0,0,.3)}.chosen-container-active .chosen-choices li.search-field input[type=text]{color:#111!important}.chosen-disabled{opacity:.5!important;cursor:default}.chosen-disabled .chosen-single{cursor:default}.chosen-disabled .chosen-choices .search-choice .search-choice-close{cursor:default}.chosen-rtl{text-align:right}.chosen-rtl .chosen-single{overflow:visible;padding:0 8px 0 0}.chosen-rtl .chosen-single span{margin-right:0;margin-left:26px;direction:rtl}.chosen-rtl .chosen-single-with-deselect span{margin-left:38px}.chosen-rtl .chosen-single div{right:auto;left:3px}.chosen-rtl .chosen-single abbr{right:auto;left:26px}.chosen-rtl .chosen-choices li{float:right}.chosen-rtl .chosen-choices li.search-field input[type=text]{direction:rtl}.chosen-rtl .chosen-choices li.search-choice{margin:3px 5px 3px 0;padding:3px 5px 3px 19px}.chosen-rtl .chosen-choices li.search-choice .search-choice-close{right:auto;left:4px}.chosen-rtl.chosen-container-single-nosearch .chosen-search,.chosen-rtl .chosen-drop{left:9999px}.chosen-rtl.chosen-container-single .chosen-results{margin:0 0 4px 4px;padding:0 4px 0 0}.chosen-rtl .chosen-results li.group-option{padding-right:15px;padding-left:0}.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div{border-right:0}.chosen-rtl .chosen-search input[type=text]{padding:4px 5px 4px 20px;background:#fff url(chosen-sprite.png) no-repeat -30px -20px;background:url(chosen-sprite.png) no-repeat -30px -20px;direction:rtl}.chosen-rtl.chosen-container-single .chosen-single div b{background-position:6px 2px}.chosen-rtl.chosen-container-single.chosen-with-drop .chosen-single div b{background-position:-12px 2px}@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-resolution:144dppx){.chosen-rtl .chosen-search input[type=text],.chosen-container-single .chosen-single abbr,.chosen-container-single .chosen-single div b,.chosen-container-single .chosen-search input[type=text],.chosen-container-multi .chosen-choices .search-choice .search-choice-close,.chosen-container .chosen-results-scroll-down span,.chosen-container .chosen-results-scroll-up span{background-image:url(chosen-sprite@2x.png)!important;background-size:52px 37px!important;background-repeat:no-repeat!important}} 4 | -------------------------------------------------------------------------------- /web/static/css/torque.css: -------------------------------------------------------------------------------- 1 | *{ 2 | font-family: 'Lato', sans-serif; 3 | } 4 | html,body { 5 | height: 100%; 6 | } 7 | #map-container { 8 | height: 90%; 9 | padding-top: 65px; 10 | padding-left: 2%; 11 | } 12 | #map-canvas { 13 | width: 96%; 14 | height: 100%; 15 | position: absolute; 16 | } 17 | #right-container { 18 | height: 90%; 19 | padding-top: 62px; 20 | padding-right: 2%; 21 | } 22 | #right-cell { 23 | height: 100%; 24 | width: 96%; 25 | position: absolute; 26 | } 27 | .row.right-row { 28 | width: 100%; 29 | padding-top: 15px; 30 | padding-bottom: 15px; 31 | position: absolute; 32 | } 33 | .pull-left { 34 | float: left !important; 35 | } 36 | .pull-right { 37 | float: right !important; 38 | } 39 | .center-block { 40 | display: block; 41 | margin-left: auto; 42 | margin-right: auto; 43 | padding-right: 10px; 44 | } 45 | .table-responsive { 46 | overflow-x: auto; 47 | } 48 | .table-responsive .table { 49 | font-size: 0.875em; 50 | 51 | } 52 | 53 | #placeholder .tickLabel{ 54 | font-size: 80%; 55 | } 56 | 57 | #seshidtag_chosen { 58 | font-size: 15px; 59 | } 60 | 61 | .chosen-container-single .chosen-single { 62 | height: 28px; 63 | } 64 | 65 | .chosen-container-single .chosen-single span { 66 | margin-right: 0px; 67 | } 68 | 69 | li.search-field { 70 | height: 35px !important; 71 | } 72 | 73 | li.search-choice { 74 | margin: 7px 0 3px 5px !important; 75 | } 76 | 77 | ul.chosen-choices li input { 78 | height: 100% !important; 79 | font-size: 15px; 80 | padding-bottom: 10px !rimportant; 81 | } 82 | 83 | @media (min-width: 768px){ 84 | .navbar-nav{ 85 | margin: 0 auto; 86 | display: table; 87 | table-layout: fixed; 88 | float: none; 89 | } 90 | } 91 | @media (max-width: 767px) { 92 | #map-container { 93 | height: 42%; 94 | } 95 | } 96 | 97 | canvas *{ 98 | width:100%; 99 | } 100 | #chart-area { 101 | width: 100%; 102 | margin: 0px; 103 | padding-bottom: 15px; 104 | background-color: #428bca; 105 | /*padding-top: -10px;*/ 106 | } 107 | .clickable{ 108 | cursor: pointer; 109 | } 110 | -------------------------------------------------------------------------------- /web/static/js/jquery.colorhelpers.js: -------------------------------------------------------------------------------- 1 | /* Plugin for jQuery for working with colors. 2 | * 3 | * Version 1.1. 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() return the same modified object 17 | * instead of making a new one. 18 | * 19 | * V. 1.1: Fix error handling so e.g. parsing an empty string does 20 | * produce a color rather than just crashing. 21 | */ 22 | 23 | (function($) { 24 | $.color = {}; 25 | 26 | // construct color object with some convenient chainable helpers 27 | $.color.make = function (r, g, b, a) { 28 | var o = {}; 29 | o.r = r || 0; 30 | o.g = g || 0; 31 | o.b = b || 0; 32 | o.a = a != null ? a : 1; 33 | 34 | o.add = function (c, d) { 35 | for (var i = 0; i < c.length; ++i) 36 | o[c.charAt(i)] += d; 37 | return o.normalize(); 38 | }; 39 | 40 | o.scale = function (c, f) { 41 | for (var i = 0; i < c.length; ++i) 42 | o[c.charAt(i)] *= f; 43 | return o.normalize(); 44 | }; 45 | 46 | o.toString = function () { 47 | if (o.a >= 1.0) { 48 | return "rgb("+[o.r, o.g, o.b].join(",")+")"; 49 | } else { 50 | return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")"; 51 | } 52 | }; 53 | 54 | o.normalize = function () { 55 | function clamp(min, value, max) { 56 | return value < min ? min: (value > max ? max: value); 57 | } 58 | 59 | o.r = clamp(0, parseInt(o.r), 255); 60 | o.g = clamp(0, parseInt(o.g), 255); 61 | o.b = clamp(0, parseInt(o.b), 255); 62 | o.a = clamp(0, o.a, 1); 63 | return o; 64 | }; 65 | 66 | o.clone = function () { 67 | return $.color.make(o.r, o.b, o.g, o.a); 68 | }; 69 | 70 | return o.normalize(); 71 | } 72 | 73 | // extract CSS color property from element, going up in the DOM 74 | // if it's "transparent" 75 | $.color.extract = function (elem, css) { 76 | var c; 77 | 78 | do { 79 | c = elem.css(css).toLowerCase(); 80 | // keep going until we find an element that has color, or 81 | // we hit the body or root (have no parent) 82 | if (c != '' && c != 'transparent') 83 | break; 84 | elem = elem.parent(); 85 | } while (elem.length && !$.nodeName(elem.get(0), "body")); 86 | 87 | // catch Safari's way of signalling transparent 88 | if (c == "rgba(0, 0, 0, 0)") 89 | c = "transparent"; 90 | 91 | return $.color.parse(c); 92 | } 93 | 94 | // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"), 95 | // returns color object, if parsing failed, you get black (0, 0, 96 | // 0) out 97 | $.color.parse = function (str) { 98 | var res, m = $.color.make; 99 | 100 | // Look for rgb(num,num,num) 101 | if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)) 102 | return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10)); 103 | 104 | // Look for rgba(num,num,num,num) 105 | 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)) 106 | return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4])); 107 | 108 | // Look for rgb(num%,num%,num%) 109 | if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)) 110 | return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55); 111 | 112 | // Look for rgba(num%,num%,num%,num) 113 | 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)) 114 | return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4])); 115 | 116 | // Look for #a0b1c2 117 | if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)) 118 | return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)); 119 | 120 | // Look for #fff 121 | if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)) 122 | return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16)); 123 | 124 | // Otherwise, we're most likely dealing with a named color 125 | var name = $.trim(str).toLowerCase(); 126 | if (name == "transparent") 127 | return m(255, 255, 255, 0); 128 | else { 129 | // default to black 130 | res = lookupColors[name] || [0, 0, 0]; 131 | return m(res[0], res[1], res[2]); 132 | } 133 | } 134 | 135 | var lookupColors = { 136 | aqua:[0,255,255], 137 | azure:[240,255,255], 138 | beige:[245,245,220], 139 | black:[0,0,0], 140 | blue:[0,0,255], 141 | brown:[165,42,42], 142 | cyan:[0,255,255], 143 | darkblue:[0,0,139], 144 | darkcyan:[0,139,139], 145 | darkgrey:[169,169,169], 146 | darkgreen:[0,100,0], 147 | darkkhaki:[189,183,107], 148 | darkmagenta:[139,0,139], 149 | darkolivegreen:[85,107,47], 150 | darkorange:[255,140,0], 151 | darkorchid:[153,50,204], 152 | darkred:[139,0,0], 153 | darksalmon:[233,150,122], 154 | darkviolet:[148,0,211], 155 | fuchsia:[255,0,255], 156 | gold:[255,215,0], 157 | green:[0,128,0], 158 | indigo:[75,0,130], 159 | khaki:[240,230,140], 160 | lightblue:[173,216,230], 161 | lightcyan:[224,255,255], 162 | lightgreen:[144,238,144], 163 | lightgrey:[211,211,211], 164 | lightpink:[255,182,193], 165 | lightyellow:[255,255,224], 166 | lime:[0,255,0], 167 | magenta:[255,0,255], 168 | maroon:[128,0,0], 169 | navy:[0,0,128], 170 | olive:[128,128,0], 171 | orange:[255,165,0], 172 | pink:[255,192,203], 173 | purple:[128,0,128], 174 | violet:[128,0,128], 175 | red:[255,0,0], 176 | silver:[192,192,192], 177 | white:[255,255,255], 178 | yellow:[255,255,0] 179 | }; 180 | })(jQuery); 181 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.animator.js: -------------------------------------------------------------------------------- 1 | /* jQuery Flot Animator version 1.0. 2 | 3 | Flot Animator is a free jQuery Plugin that will add fluid animations to Flot charts. 4 | 5 | Copyright (c) 2012-2013 Chtiwi Malek 6 | http://www.codicode.com/art/jquery_flot_animator.aspx 7 | 8 | Licensed under Creative Commons Attribution 3.0 Unported License. 9 | */ 10 | 11 | $.extend({ 12 | plotAnimator: function (chart, data,g){ 13 | 14 | var serie = 0; 15 | for (var i = 0; i < data.length; i++) 16 | { 17 | if (data[i].animator) 18 | { 19 | serie = i; 20 | } 21 | } 22 | 23 | function pInit(arr){ 24 | var x = []; 25 | x.push([arr[0][0], Math.max.apply(Math, arr.map(function(i) { return i[1];}))]); 26 | x.push([arr[0][0], null]); 27 | x.push([arr[0][0], Math.min.apply(Math, arr.map(function(i) { return i[1];}))]); 28 | for(var i = 0; i < arr.length; i++) { 29 | x.push([arr[i][0], null]); 30 | } 31 | data[serie].data = x; 32 | return $.plot(chart, data, g); 33 | } 34 | 35 | var d0 = data[serie]; 36 | var oData = d0.data; 37 | 38 | var plot = pInit(oData); 39 | 40 | var isLines = (data[serie].lines)?true:false; 41 | var steps = (data[serie].animator && data[serie].animator.steps) || 135; 42 | var duration = (data[serie].animator && data[serie].animator.duration) || 1000; 43 | var start = (data[serie].animator && data[serie].animator.start) || 0; 44 | var dir = (data[serie].animator && data[serie].animator.direction) || "right"; 45 | function stepData() 46 | { 47 | var Si = oData[0][0]; 48 | var Fi = oData[oData.length-1][0]; 49 | var Pas = (Fi-Si)/steps; 50 | 51 | var d2 = []; 52 | d2.push(oData[0]); 53 | var nPointPos = 1; 54 | lPoint = oData[0]; 55 | nPoint = oData[nPointPos]; 56 | for (var i = Si+Pas; i < Fi+Pas; i += Pas) 57 | { 58 | if (i>Fi) {i=Fi;} 59 | $("#m2").html(i); 60 | while (i > nPoint[0]) 61 | { 62 | lPoint = nPoint; 63 | nPoint = oData[nPointPos++]; 64 | } 65 | if (i == nPoint[0]) 66 | { 67 | d2.push([i,nPoint[1]]); 68 | lPoint = nPoint; 69 | nPoint = oData[nPointPos++]; 70 | } 71 | else 72 | { 73 | var a = ((nPoint[1]-lPoint[1]) / ((nPoint[0]-lPoint[0]))); 74 | curV = (a * i) + (lPoint[1] - (a * lPoint[0])); 75 | d2.push([i,curV]); 76 | } 77 | } 78 | return d2; 79 | } 80 | 81 | var step=0; 82 | var sData = stepData(); 83 | function plotData() 84 | { 85 | var d3=[]; 86 | step++; 87 | 88 | switch(dir) 89 | { 90 | case 'right': 91 | d3 = sData.slice(0, step); 92 | break; 93 | case 'left': 94 | d3 = sData.slice(-1*step); 95 | break 96 | case 'center': 97 | d3 = sData.slice((sData.length/2)-(step/2),(sData.length/2)+(step/2)); 98 | break; 99 | } 100 | 101 | if (!isLines) 102 | { 103 | inV = d3[0][0]; 104 | laV = d3[d3.length-1][0]; 105 | d3=[]; 106 | for (var i = 0; i < oData.length; i++) 107 | { 108 | if (oData[i][0]>=inV && oData[i][0]<=laV) 109 | { 110 | d3.push(oData[i]); 111 | } 112 | } 113 | } 114 | 115 | data[serie].data = (stept){s=t}$("#18").19(s);1a(s>4[0]){7=4;4=o[i++]}9(s==4[0]){r.6([s,4[1]]);7=4;4=o[i++]}11{3 u=(4[1]-7[1])/(4[0]-7[0]);16=u*s+(7[1]-u*7[0]);r.6([s,16])}}j r}b v(){3 n=[];p++;1b(c){14"1c":n=d.w(-1*p);y;14"1h":n=d.w(d.8/2-p/2,d.8/2+p/2);y;1d:n=d.w(0,p);y}9(!u){13=n[0][0];12=n[n.8-1][0];n=[];q(3 i=0;i=13&&o[i][0]<=12){n.6(o[i])}}}t[r].x=p ") 193 | .css("position", "absolute") 194 | .addClass(typeof font === "string" ? font : null) 195 | .appendTo(this.getTextLayer(layer)); 196 | 197 | font = { 198 | lineHeight: element.height(), 199 | style: element.css("font-style"), 200 | variant: element.css("font-variant"), 201 | weight: element.css("font-weight"), 202 | family: element.css("font-family"), 203 | color: element.css("color") 204 | }; 205 | 206 | // Setting line-height to 1, without units, sets it equal 207 | // to the font-size, even if the font-size is abstract, 208 | // like 'smaller'. This enables us to read the real size 209 | // via the element's height, working around browsers that 210 | // return the literal 'smaller' value. 211 | 212 | font.size = element.css("line-height", 1).height(); 213 | 214 | element.remove(); 215 | } 216 | 217 | textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; 218 | 219 | // Create a new info object, initializing the dimensions to 220 | // zero so we can count them up line-by-line. 221 | 222 | info = styleCache[text] = { 223 | width: 0, 224 | height: 0, 225 | positions: [], 226 | lines: [], 227 | font: { 228 | definition: textStyle, 229 | color: font.color 230 | } 231 | }; 232 | 233 | context.save(); 234 | context.font = textStyle; 235 | 236 | // Canvas can't handle multi-line strings; break on various 237 | // newlines, including HTML brs, to build a list of lines. 238 | // Note that we could split directly on regexps, but IE < 9 is 239 | // broken; revisit when we drop IE 7/8 support. 240 | 241 | var lines = (text + "").replace(/
|\r\n|\r/g, "\n").split("\n"); 242 | 243 | for (var i = 0; i < lines.length; ++i) { 244 | 245 | var lineText = lines[i], 246 | measured = context.measureText(lineText); 247 | 248 | info.width = Math.max(measured.width, info.width); 249 | info.height += font.lineHeight; 250 | 251 | info.lines.push({ 252 | text: lineText, 253 | width: measured.width, 254 | height: font.lineHeight 255 | }); 256 | } 257 | 258 | context.restore(); 259 | } 260 | 261 | return info; 262 | }; 263 | 264 | // Adds a text string to the canvas text overlay. 265 | 266 | Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { 267 | 268 | if (!plot.getOptions().canvas) { 269 | return addText.call(this, layer, x, y, text, font, angle, width, halign, valign); 270 | } 271 | 272 | var info = this.getTextInfo(layer, text, font, angle, width), 273 | positions = info.positions, 274 | lines = info.lines; 275 | 276 | // Text is drawn with baseline 'middle', which we need to account 277 | // for by adding half a line's height to the y position. 278 | 279 | y += info.height / lines.length / 2; 280 | 281 | // Tweak the initial y-position to match vertical alignment 282 | 283 | if (valign == "middle") { 284 | y = Math.round(y - info.height / 2); 285 | } else if (valign == "bottom") { 286 | y = Math.round(y - info.height); 287 | } else { 288 | y = Math.round(y); 289 | } 290 | 291 | // FIXME: LEGACY BROWSER FIX 292 | // AFFECTS: Opera < 12.00 293 | 294 | // Offset the y coordinate, since Opera is off pretty 295 | // consistently compared to the other browsers. 296 | 297 | if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { 298 | y -= 2; 299 | } 300 | 301 | // Determine whether this text already exists at this position. 302 | // If so, mark it for inclusion in the next render pass. 303 | 304 | for (var i = 0, position; position = positions[i]; i++) { 305 | if (position.x == x && position.y == y) { 306 | position.active = true; 307 | return; 308 | } 309 | } 310 | 311 | // If the text doesn't exist at this position, create a new entry 312 | 313 | position = { 314 | active: true, 315 | lines: [], 316 | x: x, 317 | y: y 318 | }; 319 | 320 | positions.push(position); 321 | 322 | // Fill in the x & y positions of each line, adjusting them 323 | // individually for horizontal alignment. 324 | 325 | for (var i = 0, line; line = lines[i]; i++) { 326 | if (halign == "center") { 327 | position.lines.push([Math.round(x - line.width / 2), y]); 328 | } else if (halign == "right") { 329 | position.lines.push([Math.round(x - line.width), y]); 330 | } else { 331 | position.lines.push([Math.round(x), y]); 332 | } 333 | y += line.height; 334 | } 335 | }; 336 | } 337 | 338 | $.plot.plugins.push({ 339 | init: init, 340 | options: options, 341 | name: "canvas", 342 | version: "1.0" 343 | }); 344 | 345 | })(jQuery); 346 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.categories.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for plotting textual data or categories. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | Consider a dataset like [["February", 34], ["March", 20], ...]. This plugin 7 | allows you to plot such a dataset directly. 8 | 9 | To enable it, you must specify mode: "categories" on the axis with the textual 10 | labels, e.g. 11 | 12 | $.plot("#placeholder", data, { xaxis: { mode: "categories" } }); 13 | 14 | By default, the labels are ordered as they are met in the data series. If you 15 | need a different ordering, you can specify "categories" on the axis options 16 | and list the categories there: 17 | 18 | xaxis: { 19 | mode: "categories", 20 | categories: ["February", "March", "April"] 21 | } 22 | 23 | If you need to customize the distances between the categories, you can specify 24 | "categories" as an object mapping labels to values 25 | 26 | xaxis: { 27 | mode: "categories", 28 | categories: { "February": 1, "March": 3, "April": 4 } 29 | } 30 | 31 | If you don't specify all categories, the remaining categories will be numbered 32 | from the max value plus 1 (with a spacing of 1 between each). 33 | 34 | Internally, the plugin works by transforming the input data through an auto- 35 | generated mapping where the first category becomes 0, the second 1, etc. 36 | Hence, a point like ["February", 34] becomes [0, 34] internally in Flot (this 37 | is visible in hover and click events that return numbers rather than the 38 | category labels). The plugin also overrides the tick generator to spit out the 39 | categories as ticks instead of the values. 40 | 41 | If you need to map a value back to its label, the mapping is always accessible 42 | as "categories" on the axis object, e.g. plot.getAxes().xaxis.categories. 43 | 44 | */ 45 | 46 | (function ($) { 47 | var options = { 48 | xaxis: { 49 | categories: null 50 | }, 51 | yaxis: { 52 | categories: null 53 | } 54 | }; 55 | 56 | function processRawData(plot, series, data, datapoints) { 57 | // if categories are enabled, we need to disable 58 | // auto-transformation to numbers so the strings are intact 59 | // for later processing 60 | 61 | var xCategories = series.xaxis.options.mode == "categories", 62 | yCategories = series.yaxis.options.mode == "categories"; 63 | 64 | if (!(xCategories || yCategories)) 65 | return; 66 | 67 | var format = datapoints.format; 68 | 69 | if (!format) { 70 | // FIXME: auto-detection should really not be defined here 71 | var s = series; 72 | format = []; 73 | format.push({ x: true, number: true, required: true }); 74 | format.push({ y: true, number: true, required: true }); 75 | 76 | if (s.bars.show || (s.lines.show && s.lines.fill)) { 77 | var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); 78 | format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); 79 | if (s.bars.horizontal) { 80 | delete format[format.length - 1].y; 81 | format[format.length - 1].x = true; 82 | } 83 | } 84 | 85 | datapoints.format = format; 86 | } 87 | 88 | for (var m = 0; m < format.length; ++m) { 89 | if (format[m].x && xCategories) 90 | format[m].number = false; 91 | 92 | if (format[m].y && yCategories) 93 | format[m].number = false; 94 | } 95 | } 96 | 97 | function getNextIndex(categories) { 98 | var index = -1; 99 | 100 | for (var v in categories) 101 | if (categories[v] > index) 102 | index = categories[v]; 103 | 104 | return index + 1; 105 | } 106 | 107 | function categoriesTickGenerator(axis) { 108 | var res = []; 109 | for (var label in axis.categories) { 110 | var v = axis.categories[label]; 111 | if (v >= axis.min && v <= axis.max) 112 | res.push([v, label]); 113 | } 114 | 115 | res.sort(function (a, b) { return a[0] - b[0]; }); 116 | 117 | return res; 118 | } 119 | 120 | function setupCategoriesForAxis(series, axis, datapoints) { 121 | if (series[axis].options.mode != "categories") 122 | return; 123 | 124 | if (!series[axis].categories) { 125 | // parse options 126 | var c = {}, o = series[axis].options.categories || {}; 127 | if ($.isArray(o)) { 128 | for (var i = 0; i < o.length; ++i) 129 | c[o[i]] = i; 130 | } 131 | else { 132 | for (var v in o) 133 | c[v] = o[v]; 134 | } 135 | 136 | series[axis].categories = c; 137 | } 138 | 139 | // fix ticks 140 | if (!series[axis].options.ticks) 141 | series[axis].options.ticks = categoriesTickGenerator; 142 | 143 | transformPointsOnAxis(datapoints, axis, series[axis].categories); 144 | } 145 | 146 | function transformPointsOnAxis(datapoints, axis, categories) { 147 | // go through the points, transforming them 148 | var points = datapoints.points, 149 | ps = datapoints.pointsize, 150 | format = datapoints.format, 151 | formatColumn = axis.charAt(0), 152 | index = getNextIndex(categories); 153 | 154 | for (var i = 0; i < points.length; i += ps) { 155 | if (points[i] == null) 156 | continue; 157 | 158 | for (var m = 0; m < ps; ++m) { 159 | var val = points[i + m]; 160 | 161 | if (val == null || !format[m][formatColumn]) 162 | continue; 163 | 164 | if (!(val in categories)) { 165 | categories[val] = index; 166 | ++index; 167 | } 168 | 169 | points[i + m] = categories[val]; 170 | } 171 | } 172 | } 173 | 174 | function processDatapoints(plot, series, datapoints) { 175 | setupCategoriesForAxis(series, "xaxis", datapoints); 176 | setupCategoriesForAxis(series, "yaxis", datapoints); 177 | } 178 | 179 | function init(plot) { 180 | plot.hooks.processRawData.push(processRawData); 181 | plot.hooks.processDatapoints.push(processDatapoints); 182 | } 183 | 184 | $.plot.plugins.push({ 185 | init: init, 186 | options: options, 187 | name: 'categories', 188 | version: '1.0' 189 | }); 190 | })(jQuery); 191 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.crosshair.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for showing crosshairs when the mouse hovers over the plot. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | The plugin supports these options: 7 | 8 | crosshair: { 9 | mode: null or "x" or "y" or "xy" 10 | color: color 11 | lineWidth: number 12 | } 13 | 14 | Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical 15 | crosshair that lets you trace the values on the x axis, "y" enables a 16 | horizontal crosshair and "xy" enables them both. "color" is the color of the 17 | crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of 18 | the drawn lines (default is 1). 19 | 20 | The plugin also adds four public methods: 21 | 22 | - setCrosshair( pos ) 23 | 24 | Set the position of the crosshair. Note that this is cleared if the user 25 | moves the mouse. "pos" is in coordinates of the plot and should be on the 26 | form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple 27 | axes), which is coincidentally the same format as what you get from a 28 | "plothover" event. If "pos" is null, the crosshair is cleared. 29 | 30 | - clearCrosshair() 31 | 32 | Clear the crosshair. 33 | 34 | - lockCrosshair(pos) 35 | 36 | Cause the crosshair to lock to the current location, no longer updating if 37 | the user moves the mouse. Optionally supply a position (passed on to 38 | setCrosshair()) to move it to. 39 | 40 | Example usage: 41 | 42 | var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; 43 | $("#graph").bind( "plothover", function ( evt, position, item ) { 44 | if ( item ) { 45 | // Lock the crosshair to the data point being hovered 46 | myFlot.lockCrosshair({ 47 | x: item.datapoint[ 0 ], 48 | y: item.datapoint[ 1 ] 49 | }); 50 | } else { 51 | // Return normal crosshair operation 52 | myFlot.unlockCrosshair(); 53 | } 54 | }); 55 | 56 | - unlockCrosshair() 57 | 58 | Free the crosshair to move again after locking it. 59 | */ 60 | 61 | (function ($) { 62 | var options = { 63 | crosshair: { 64 | mode: null, // one of null, "x", "y" or "xy", 65 | color: "rgba(170, 0, 0, 0.80)", 66 | lineWidth: 1 67 | } 68 | }; 69 | 70 | function init(plot) { 71 | // position of crosshair in pixels 72 | var crosshair = { x: -1, y: -1, locked: false }; 73 | 74 | plot.setCrosshair = function setCrosshair(pos) { 75 | if (!pos) 76 | crosshair.x = -1; 77 | else { 78 | var o = plot.p2c(pos); 79 | crosshair.x = Math.max(0, Math.min(o.left, plot.width())); 80 | crosshair.y = Math.max(0, Math.min(o.top, plot.height())); 81 | } 82 | 83 | plot.triggerRedrawOverlay(); 84 | }; 85 | 86 | plot.clearCrosshair = plot.setCrosshair; // passes null for pos 87 | 88 | plot.lockCrosshair = function lockCrosshair(pos) { 89 | if (pos) 90 | plot.setCrosshair(pos); 91 | crosshair.locked = true; 92 | }; 93 | 94 | plot.unlockCrosshair = function unlockCrosshair() { 95 | crosshair.locked = false; 96 | }; 97 | 98 | function onMouseOut(e) { 99 | if (crosshair.locked) 100 | return; 101 | 102 | if (crosshair.x != -1) { 103 | crosshair.x = -1; 104 | plot.triggerRedrawOverlay(); 105 | } 106 | } 107 | 108 | function onMouseMove(e) { 109 | if (crosshair.locked) 110 | return; 111 | 112 | if (plot.getSelection && plot.getSelection()) { 113 | crosshair.x = -1; // hide the crosshair while selecting 114 | return; 115 | } 116 | 117 | var offset = plot.offset(); 118 | crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); 119 | crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); 120 | plot.triggerRedrawOverlay(); 121 | } 122 | 123 | plot.hooks.bindEvents.push(function (plot, eventHolder) { 124 | if (!plot.getOptions().crosshair.mode) 125 | return; 126 | 127 | eventHolder.mouseout(onMouseOut); 128 | eventHolder.mousemove(onMouseMove); 129 | }); 130 | 131 | plot.hooks.drawOverlay.push(function (plot, ctx) { 132 | var c = plot.getOptions().crosshair; 133 | if (!c.mode) 134 | return; 135 | 136 | var plotOffset = plot.getPlotOffset(); 137 | 138 | ctx.save(); 139 | ctx.translate(plotOffset.left, plotOffset.top); 140 | 141 | if (crosshair.x != -1) { 142 | var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0; 143 | 144 | ctx.strokeStyle = c.color; 145 | ctx.lineWidth = c.lineWidth; 146 | ctx.lineJoin = "round"; 147 | 148 | ctx.beginPath(); 149 | if (c.mode.indexOf("x") != -1) { 150 | var drawX = Math.floor(crosshair.x) + adj; 151 | ctx.moveTo(drawX, 0); 152 | ctx.lineTo(drawX, plot.height()); 153 | } 154 | if (c.mode.indexOf("y") != -1) { 155 | var drawY = Math.floor(crosshair.y) + adj; 156 | ctx.moveTo(0, drawY); 157 | ctx.lineTo(plot.width(), drawY); 158 | } 159 | ctx.stroke(); 160 | } 161 | ctx.restore(); 162 | }); 163 | 164 | plot.hooks.shutdown.push(function (plot, eventHolder) { 165 | eventHolder.unbind("mouseout", onMouseOut); 166 | eventHolder.unbind("mousemove", onMouseMove); 167 | }); 168 | } 169 | 170 | $.plot.plugins.push({ 171 | init: init, 172 | options: options, 173 | name: 'crosshair', 174 | version: '1.0' 175 | }); 176 | })(jQuery); 177 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.downsample.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | 4 | Copyright (c) 2013 by Sveinn Steinarsson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | (function ($) { 26 | "use strict"; 27 | 28 | var floor = Math.floor, 29 | abs = Math.abs; 30 | 31 | function largestTriangleThreeBuckets(data, threshold) { 32 | 33 | var data_length = data.length; 34 | if (threshold >= data_length || threshold === 0) { 35 | return data; // Nothing to do 36 | } 37 | 38 | var sampled = [], 39 | sampled_index = 0; 40 | 41 | // Bucket size. Leave room for start and end data points 42 | var every = (data_length - 2) / (threshold - 2); 43 | 44 | var a = 0, // Initially a is the first point in the triangle 45 | max_area_point, 46 | max_area, 47 | area, 48 | next_a; 49 | 50 | sampled[ sampled_index++ ] = data[ a ]; // Always add the first point 51 | 52 | for (var i = 0; i < threshold - 2; i++) { 53 | 54 | // Calculate point average for next bucket (containing c) 55 | var avg_x = 0, 56 | avg_y = 0, 57 | avg_range_start = floor( ( i + 1 ) * every ) + 1, 58 | avg_range_end = floor( ( i + 2 ) * every ) + 1; 59 | avg_range_end = avg_range_end < data_length ? avg_range_end : data_length; 60 | 61 | var avg_range_length = avg_range_end - avg_range_start; 62 | 63 | for ( ; avg_range_start max_area ) { 86 | max_area = area; 87 | max_area_point = data[ range_offs ]; 88 | next_a = range_offs; // Next a is this b 89 | } 90 | } 91 | 92 | sampled[ sampled_index++ ] = max_area_point; // Pick this point from the bucket 93 | a = next_a; // This a is the next a (chosen b) 94 | } 95 | 96 | sampled[ sampled_index++ ] = data[ data_length - 1 ]; // Always add last 97 | 98 | return sampled; 99 | } 100 | 101 | 102 | function processRawData ( plot, series ) { 103 | series.data = largestTriangleThreeBuckets( series.data, series.downsample.threshold ); 104 | } 105 | 106 | 107 | var options = { 108 | series: { 109 | downsample: { 110 | threshold: 1000 // 0 disables downsampling for this series. 111 | } 112 | } 113 | }; 114 | 115 | function init(plot) { 116 | plot.hooks.processRawData.push(processRawData); 117 | } 118 | 119 | $.plot.plugins.push({ 120 | init: init, 121 | options: options, 122 | name: "downsample", 123 | version: "1.0" 124 | }); 125 | 126 | })(jQuery); 127 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.errorbars.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for plotting error bars. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | Error bars are used to show standard deviation and other statistical 7 | properties in a plot. 8 | 9 | * Created by Rui Pereira - rui (dot) pereira (at) gmail (dot) com 10 | 11 | This plugin allows you to plot error-bars over points. Set "errorbars" inside 12 | the points series to the axis name over which there will be error values in 13 | your data array (*even* if you do not intend to plot them later, by setting 14 | "show: null" on xerr/yerr). 15 | 16 | The plugin supports these options: 17 | 18 | series: { 19 | points: { 20 | errorbars: "x" or "y" or "xy", 21 | xerr: { 22 | show: null/false or true, 23 | asymmetric: null/false or true, 24 | upperCap: null or "-" or function, 25 | lowerCap: null or "-" or function, 26 | color: null or color, 27 | radius: null or number 28 | }, 29 | yerr: { same options as xerr } 30 | } 31 | } 32 | 33 | Each data point array is expected to be of the type: 34 | 35 | "x" [ x, y, xerr ] 36 | "y" [ x, y, yerr ] 37 | "xy" [ x, y, xerr, yerr ] 38 | 39 | Where xerr becomes xerr_lower,xerr_upper for the asymmetric error case, and 40 | equivalently for yerr. Eg., a datapoint for the "xy" case with symmetric 41 | error-bars on X and asymmetric on Y would be: 42 | 43 | [ x, y, xerr, yerr_lower, yerr_upper ] 44 | 45 | By default no end caps are drawn. Setting upperCap and/or lowerCap to "-" will 46 | draw a small cap perpendicular to the error bar. They can also be set to a 47 | user-defined drawing function, with (ctx, x, y, radius) as parameters, as eg. 48 | 49 | function drawSemiCircle( ctx, x, y, radius ) { 50 | ctx.beginPath(); 51 | ctx.arc( x, y, radius, 0, Math.PI, false ); 52 | ctx.moveTo( x - radius, y ); 53 | ctx.lineTo( x + radius, y ); 54 | ctx.stroke(); 55 | } 56 | 57 | Color and radius both default to the same ones of the points series if not 58 | set. The independent radius parameter on xerr/yerr is useful for the case when 59 | we may want to add error-bars to a line, without showing the interconnecting 60 | points (with radius: 0), and still showing end caps on the error-bars. 61 | shadowSize and lineWidth are derived as well from the points series. 62 | 63 | */ 64 | 65 | (function ($) { 66 | var options = { 67 | series: { 68 | points: { 69 | errorbars: null, //should be 'x', 'y' or 'xy' 70 | xerr: { err: 'x', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null}, 71 | yerr: { err: 'y', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null} 72 | } 73 | } 74 | }; 75 | 76 | function processRawData(plot, series, data, datapoints){ 77 | if (!series.points.errorbars) 78 | return; 79 | 80 | // x,y values 81 | var format = [ 82 | { x: true, number: true, required: true }, 83 | { y: true, number: true, required: true } 84 | ]; 85 | 86 | var errors = series.points.errorbars; 87 | // error bars - first X then Y 88 | if (errors == 'x' || errors == 'xy') { 89 | // lower / upper error 90 | if (series.points.xerr.asymmetric) { 91 | format.push({ x: true, number: true, required: true }); 92 | format.push({ x: true, number: true, required: true }); 93 | } else 94 | format.push({ x: true, number: true, required: true }); 95 | } 96 | if (errors == 'y' || errors == 'xy') { 97 | // lower / upper error 98 | if (series.points.yerr.asymmetric) { 99 | format.push({ y: true, number: true, required: true }); 100 | format.push({ y: true, number: true, required: true }); 101 | } else 102 | format.push({ y: true, number: true, required: true }); 103 | } 104 | datapoints.format = format; 105 | } 106 | 107 | function parseErrors(series, i){ 108 | 109 | var points = series.datapoints.points; 110 | 111 | // read errors from points array 112 | var exl = null, 113 | exu = null, 114 | eyl = null, 115 | eyu = null; 116 | var xerr = series.points.xerr, 117 | yerr = series.points.yerr; 118 | 119 | var eb = series.points.errorbars; 120 | // error bars - first X 121 | if (eb == 'x' || eb == 'xy') { 122 | if (xerr.asymmetric) { 123 | exl = points[i + 2]; 124 | exu = points[i + 3]; 125 | if (eb == 'xy') 126 | if (yerr.asymmetric){ 127 | eyl = points[i + 4]; 128 | eyu = points[i + 5]; 129 | } else eyl = points[i + 4]; 130 | } else { 131 | exl = points[i + 2]; 132 | if (eb == 'xy') 133 | if (yerr.asymmetric) { 134 | eyl = points[i + 3]; 135 | eyu = points[i + 4]; 136 | } else eyl = points[i + 3]; 137 | } 138 | // only Y 139 | } else if (eb == 'y') 140 | if (yerr.asymmetric) { 141 | eyl = points[i + 2]; 142 | eyu = points[i + 3]; 143 | } else eyl = points[i + 2]; 144 | 145 | // symmetric errors? 146 | if (exu == null) exu = exl; 147 | if (eyu == null) eyu = eyl; 148 | 149 | var errRanges = [exl, exu, eyl, eyu]; 150 | // nullify if not showing 151 | if (!xerr.show){ 152 | errRanges[0] = null; 153 | errRanges[1] = null; 154 | } 155 | if (!yerr.show){ 156 | errRanges[2] = null; 157 | errRanges[3] = null; 158 | } 159 | return errRanges; 160 | } 161 | 162 | function drawSeriesErrors(plot, ctx, s){ 163 | 164 | var points = s.datapoints.points, 165 | ps = s.datapoints.pointsize, 166 | ax = [s.xaxis, s.yaxis], 167 | radius = s.points.radius, 168 | err = [s.points.xerr, s.points.yerr]; 169 | 170 | //sanity check, in case some inverted axis hack is applied to flot 171 | var invertX = false; 172 | if (ax[0].p2c(ax[0].max) < ax[0].p2c(ax[0].min)) { 173 | invertX = true; 174 | var tmp = err[0].lowerCap; 175 | err[0].lowerCap = err[0].upperCap; 176 | err[0].upperCap = tmp; 177 | } 178 | 179 | var invertY = false; 180 | if (ax[1].p2c(ax[1].min) < ax[1].p2c(ax[1].max)) { 181 | invertY = true; 182 | var tmp = err[1].lowerCap; 183 | err[1].lowerCap = err[1].upperCap; 184 | err[1].upperCap = tmp; 185 | } 186 | 187 | for (var i = 0; i < s.datapoints.points.length; i += ps) { 188 | 189 | //parse 190 | var errRanges = parseErrors(s, i); 191 | 192 | //cycle xerr & yerr 193 | for (var e = 0; e < err.length; e++){ 194 | 195 | var minmax = [ax[e].min, ax[e].max]; 196 | 197 | //draw this error? 198 | if (errRanges[e * err.length]){ 199 | 200 | //data coordinates 201 | var x = points[i], 202 | y = points[i + 1]; 203 | 204 | //errorbar ranges 205 | var upper = [x, y][e] + errRanges[e * err.length + 1], 206 | lower = [x, y][e] - errRanges[e * err.length]; 207 | 208 | //points outside of the canvas 209 | if (err[e].err == 'x') 210 | if (y > ax[1].max || y < ax[1].min || upper < ax[0].min || lower > ax[0].max) 211 | continue; 212 | if (err[e].err == 'y') 213 | if (x > ax[0].max || x < ax[0].min || upper < ax[1].min || lower > ax[1].max) 214 | continue; 215 | 216 | // prevent errorbars getting out of the canvas 217 | var drawUpper = true, 218 | drawLower = true; 219 | 220 | if (upper > minmax[1]) { 221 | drawUpper = false; 222 | upper = minmax[1]; 223 | } 224 | if (lower < minmax[0]) { 225 | drawLower = false; 226 | lower = minmax[0]; 227 | } 228 | 229 | //sanity check, in case some inverted axis hack is applied to flot 230 | if ((err[e].err == 'x' && invertX) || (err[e].err == 'y' && invertY)) { 231 | //swap coordinates 232 | var tmp = lower; 233 | lower = upper; 234 | upper = tmp; 235 | tmp = drawLower; 236 | drawLower = drawUpper; 237 | drawUpper = tmp; 238 | tmp = minmax[0]; 239 | minmax[0] = minmax[1]; 240 | minmax[1] = tmp; 241 | } 242 | 243 | // convert to pixels 244 | x = ax[0].p2c(x), 245 | y = ax[1].p2c(y), 246 | upper = ax[e].p2c(upper); 247 | lower = ax[e].p2c(lower); 248 | minmax[0] = ax[e].p2c(minmax[0]); 249 | minmax[1] = ax[e].p2c(minmax[1]); 250 | 251 | //same style as points by default 252 | var lw = err[e].lineWidth ? err[e].lineWidth : s.points.lineWidth, 253 | sw = s.points.shadowSize != null ? s.points.shadowSize : s.shadowSize; 254 | 255 | //shadow as for points 256 | if (lw > 0 && sw > 0) { 257 | var w = sw / 2; 258 | ctx.lineWidth = w; 259 | ctx.strokeStyle = "rgba(0,0,0,0.1)"; 260 | drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w + w/2, minmax); 261 | 262 | ctx.strokeStyle = "rgba(0,0,0,0.2)"; 263 | drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w/2, minmax); 264 | } 265 | 266 | ctx.strokeStyle = err[e].color? err[e].color: s.color; 267 | ctx.lineWidth = lw; 268 | //draw it 269 | drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, 0, minmax); 270 | } 271 | } 272 | } 273 | } 274 | 275 | function drawError(ctx,err,x,y,upper,lower,drawUpper,drawLower,radius,offset,minmax){ 276 | 277 | //shadow offset 278 | y += offset; 279 | upper += offset; 280 | lower += offset; 281 | 282 | // error bar - avoid plotting over circles 283 | if (err.err == 'x'){ 284 | if (upper > x + radius) drawPath(ctx, [[upper,y],[Math.max(x + radius,minmax[0]),y]]); 285 | else drawUpper = false; 286 | if (lower < x - radius) drawPath(ctx, [[Math.min(x - radius,minmax[1]),y],[lower,y]] ); 287 | else drawLower = false; 288 | } 289 | else { 290 | if (upper < y - radius) drawPath(ctx, [[x,upper],[x,Math.min(y - radius,minmax[0])]] ); 291 | else drawUpper = false; 292 | if (lower > y + radius) drawPath(ctx, [[x,Math.max(y + radius,minmax[1])],[x,lower]] ); 293 | else drawLower = false; 294 | } 295 | 296 | //internal radius value in errorbar, allows to plot radius 0 points and still keep proper sized caps 297 | //this is a way to get errorbars on lines without visible connecting dots 298 | radius = err.radius != null? err.radius: radius; 299 | 300 | // upper cap 301 | if (drawUpper) { 302 | if (err.upperCap == '-'){ 303 | if (err.err=='x') drawPath(ctx, [[upper,y - radius],[upper,y + radius]] ); 304 | else drawPath(ctx, [[x - radius,upper],[x + radius,upper]] ); 305 | } else if ($.isFunction(err.upperCap)){ 306 | if (err.err=='x') err.upperCap(ctx, upper, y, radius); 307 | else err.upperCap(ctx, x, upper, radius); 308 | } 309 | } 310 | // lower cap 311 | if (drawLower) { 312 | if (err.lowerCap == '-'){ 313 | if (err.err=='x') drawPath(ctx, [[lower,y - radius],[lower,y + radius]] ); 314 | else drawPath(ctx, [[x - radius,lower],[x + radius,lower]] ); 315 | } else if ($.isFunction(err.lowerCap)){ 316 | if (err.err=='x') err.lowerCap(ctx, lower, y, radius); 317 | else err.lowerCap(ctx, x, lower, radius); 318 | } 319 | } 320 | } 321 | 322 | function drawPath(ctx, pts){ 323 | ctx.beginPath(); 324 | ctx.moveTo(pts[0][0], pts[0][1]); 325 | for (var p=1; p < pts.length; p++) 326 | ctx.lineTo(pts[p][0], pts[p][1]); 327 | ctx.stroke(); 328 | } 329 | 330 | function draw(plot, ctx){ 331 | var plotOffset = plot.getPlotOffset(); 332 | 333 | ctx.save(); 334 | ctx.translate(plotOffset.left, plotOffset.top); 335 | $.each(plot.getData(), function (i, s) { 336 | if (s.points.errorbars && (s.points.xerr.show || s.points.yerr.show)) 337 | drawSeriesErrors(plot, ctx, s); 338 | }); 339 | ctx.restore(); 340 | } 341 | 342 | function init(plot) { 343 | plot.hooks.processRawData.push(processRawData); 344 | plot.hooks.draw.push(draw); 345 | } 346 | 347 | $.plot.plugins.push({ 348 | init: init, 349 | options: options, 350 | name: 'errorbars', 351 | version: '1.0' 352 | }); 353 | })(jQuery); 354 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.fillbetween.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for computing bottoms for filled line and bar charts. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | The case: you've got two series that you want to fill the area between. In Flot 7 | terms, you need to use one as the fill bottom of the other. You can specify the 8 | bottom of each data point as the third coordinate manually, or you can use this 9 | plugin to compute it for you. 10 | 11 | In order to name the other series, you need to give it an id, like this: 12 | 13 | var dataset = [ 14 | { data: [ ... ], id: "foo" } , // use default bottom 15 | { data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom 16 | ]; 17 | 18 | $.plot($("#placeholder"), dataset, { lines: { show: true, fill: true }}); 19 | 20 | As a convenience, if the id given is a number that doesn't appear as an id in 21 | the series, it is interpreted as the index in the array instead (so fillBetween: 22 | 0 can also mean the first series). 23 | 24 | Internally, the plugin modifies the datapoints in each series. For line series, 25 | extra data points might be inserted through interpolation. Note that at points 26 | where the bottom line is not defined (due to a null point or start/end of line), 27 | the current line will show a gap too. The algorithm comes from the 28 | jquery.flot.stack.js plugin, possibly some code could be shared. 29 | 30 | */ 31 | 32 | (function ( $ ) { 33 | 34 | var options = { 35 | series: { 36 | fillBetween: null // or number 37 | } 38 | }; 39 | 40 | function init( plot ) { 41 | 42 | function findBottomSeries( s, allseries ) { 43 | 44 | var i; 45 | 46 | for ( i = 0; i < allseries.length; ++i ) { 47 | if ( allseries[ i ].id === s.fillBetween ) { 48 | return allseries[ i ]; 49 | } 50 | } 51 | 52 | if ( typeof s.fillBetween === "number" ) { 53 | if ( s.fillBetween < 0 || s.fillBetween >= allseries.length ) { 54 | return null; 55 | } 56 | return allseries[ s.fillBetween ]; 57 | } 58 | 59 | return null; 60 | } 61 | 62 | function computeFillBottoms( plot, s, datapoints ) { 63 | 64 | if ( s.fillBetween == null ) { 65 | return; 66 | } 67 | 68 | var other = findBottomSeries( s, plot.getData() ); 69 | 70 | if ( !other ) { 71 | return; 72 | } 73 | 74 | var ps = datapoints.pointsize, 75 | points = datapoints.points, 76 | otherps = other.datapoints.pointsize, 77 | otherpoints = other.datapoints.points, 78 | newpoints = [], 79 | px, py, intery, qx, qy, bottom, 80 | withlines = s.lines.show, 81 | withbottom = ps > 2 && datapoints.format[2].y, 82 | withsteps = withlines && s.lines.steps, 83 | fromgap = true, 84 | i = 0, 85 | j = 0, 86 | l, m; 87 | 88 | while ( true ) { 89 | 90 | if ( i >= points.length ) { 91 | break; 92 | } 93 | 94 | l = newpoints.length; 95 | 96 | if ( points[ i ] == null ) { 97 | 98 | // copy gaps 99 | 100 | for ( m = 0; m < ps; ++m ) { 101 | newpoints.push( points[ i + m ] ); 102 | } 103 | 104 | i += ps; 105 | 106 | } else if ( j >= otherpoints.length ) { 107 | 108 | // for lines, we can't use the rest of the points 109 | 110 | if ( !withlines ) { 111 | for ( m = 0; m < ps; ++m ) { 112 | newpoints.push( points[ i + m ] ); 113 | } 114 | } 115 | 116 | i += ps; 117 | 118 | } else if ( otherpoints[ j ] == null ) { 119 | 120 | // oops, got a gap 121 | 122 | for ( m = 0; m < ps; ++m ) { 123 | newpoints.push( null ); 124 | } 125 | 126 | fromgap = true; 127 | j += otherps; 128 | 129 | } else { 130 | 131 | // cases where we actually got two points 132 | 133 | px = points[ i ]; 134 | py = points[ i + 1 ]; 135 | qx = otherpoints[ j ]; 136 | qy = otherpoints[ j + 1 ]; 137 | bottom = 0; 138 | 139 | if ( px === qx ) { 140 | 141 | for ( m = 0; m < ps; ++m ) { 142 | newpoints.push( points[ i + m ] ); 143 | } 144 | 145 | //newpoints[ l + 1 ] += qy; 146 | bottom = qy; 147 | 148 | i += ps; 149 | j += otherps; 150 | 151 | } else if ( px > qx ) { 152 | 153 | // we got past point below, might need to 154 | // insert interpolated extra point 155 | 156 | if ( withlines && i > 0 && points[ i - ps ] != null ) { 157 | intery = py + ( points[ i - ps + 1 ] - py ) * ( qx - px ) / ( points[ i - ps ] - px ); 158 | newpoints.push( qx ); 159 | newpoints.push( intery ); 160 | for ( m = 2; m < ps; ++m ) { 161 | newpoints.push( points[ i + m ] ); 162 | } 163 | bottom = qy; 164 | } 165 | 166 | j += otherps; 167 | 168 | } else { // px < qx 169 | 170 | // if we come from a gap, we just skip this point 171 | 172 | if ( fromgap && withlines ) { 173 | i += ps; 174 | continue; 175 | } 176 | 177 | for ( m = 0; m < ps; ++m ) { 178 | newpoints.push( points[ i + m ] ); 179 | } 180 | 181 | // we might be able to interpolate a point below, 182 | // this can give us a better y 183 | 184 | if ( withlines && j > 0 && otherpoints[ j - otherps ] != null ) { 185 | bottom = qy + ( otherpoints[ j - otherps + 1 ] - qy ) * ( px - qx ) / ( otherpoints[ j - otherps ] - qx ); 186 | } 187 | 188 | //newpoints[l + 1] += bottom; 189 | 190 | i += ps; 191 | } 192 | 193 | fromgap = false; 194 | 195 | if ( l !== newpoints.length && withbottom ) { 196 | newpoints[ l + 2 ] = bottom; 197 | } 198 | } 199 | 200 | // maintain the line steps invariant 201 | 202 | if ( withsteps && l !== newpoints.length && l > 0 && 203 | newpoints[ l ] !== null && 204 | newpoints[ l ] !== newpoints[ l - ps ] && 205 | newpoints[ l + 1 ] !== newpoints[ l - ps + 1 ] ) { 206 | for (m = 0; m < ps; ++m) { 207 | newpoints[ l + ps + m ] = newpoints[ l + m ]; 208 | } 209 | newpoints[ l + 1 ] = newpoints[ l - ps + 1 ]; 210 | } 211 | } 212 | 213 | datapoints.points = newpoints; 214 | } 215 | 216 | plot.hooks.processDatapoints.push( computeFillBottoms ); 217 | } 218 | 219 | $.plot.plugins.push({ 220 | init: init, 221 | options: options, 222 | name: "fillbetween", 223 | version: "1.0" 224 | }); 225 | 226 | })(jQuery); 227 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.grow.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | 4 | Copyright (c) 2010,2011,2012, 2013 by Juergen Marsch 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | /* 25 | * Added changerequest from Thodoris Greasidis 26 | * It makes growing more 27 | */ 28 | (function ($){ 29 | "use strict"; 30 | var pluginName = "grow", pluginVersion = "0.4"; 31 | var options ={ 32 | series: { 33 | grow: { 34 | active: false, 35 | stepDelay: 20, 36 | steps:100, 37 | growings:[ 38 | { 39 | valueIndex: 1, 40 | stepMode: "linear", 41 | stepDirection: "up" 42 | } 43 | ] 44 | } 45 | } 46 | }; 47 | function init(plot) { 48 | var done = false; 49 | var growfunc; 50 | var plt = plot; 51 | var data = null; 52 | var opt = null; 53 | var serie = null; 54 | var valueIndex; 55 | plot.hooks.bindEvents.push(processbindEvents); 56 | plot.hooks.drawSeries.push(processSeries); 57 | plot.hooks.shutdown.push(shutdown); 58 | function processSeries(plot, canvascontext, series){ 59 | opt = plot.getOptions(); 60 | valueIndex = opt.series.grow.valueIndex; 61 | if(opt.series.grow.active === true){ 62 | if (done === false){ 63 | data = plot.getData(); 64 | data.actualStep = 0; 65 | data.growingIndex = 0; 66 | for (var j = 0; j < data.length; j++){ 67 | data[j].dataOrg = clone(data[j].data); 68 | for (var i = 0; i < data[j].data.length; i++){ data[j].data[i][valueIndex] = 0;} 69 | } 70 | plot.setData(data); 71 | done = true; 72 | } 73 | } 74 | } 75 | function processbindEvents(plot,eventHolder){ 76 | opt = plot.getOptions(); 77 | if (opt.series.grow.active === true){ 78 | var d = plot.getData(); 79 | for (var j = 0; j < data.length; j++) { 80 | opt.series.grow.steps = Math.max(opt.series.grow.steps, d[j].grow.steps); 81 | } 82 | if(opt.series.grow.stepDelay === 0){ opt.series.grow.stepDelay++;} 83 | growingLoop(); 84 | if (isPluginRegistered("resize")) { 85 | plot.getPlaceholder().bind("resize", onResize); 86 | } 87 | } 88 | } 89 | function growingLoop(){ 90 | var growing, startTime = new Date(), timeDiff; 91 | if (data.actualStep < opt.series.grow.steps){ 92 | data.actualStep++; 93 | for(var j = 0; j < data.length; j++){ 94 | for(var g = 0; g < data[j].grow.growings.length; g++){ 95 | growing = data[j].grow.growings[g]; 96 | if (typeof growing.stepMode === "function"){ 97 | growing.stepMode(data[j],data.actualStep,growing); 98 | } 99 | else{ 100 | if (growing.stepMode === "linear"){ growLinear();} 101 | else if (growing.stepMode === "maximum"){ growMaximum();} 102 | else if (growing.stepMode === "delay"){ growDelay();} 103 | else{growNone();} 104 | } 105 | } 106 | } 107 | plt.setData(data); 108 | plt.draw(); 109 | timeDiff = new Date() - startTime; 110 | growfunc = window.setTimeout(growingLoop, Math.max(0,opt.series.grow.stepDelay - timeDiff)); 111 | } 112 | else{ window.clearTimeout(growfunc); growfunc = null; } 113 | function growNone(){ 114 | if (data.actualStep === 1){ 115 | for (var i = 0; i < data[j].data.length; i++){ 116 | data[j].data[i][valueIndex] = data[j].dataOrg[i][growing.valueIndex]; 117 | } 118 | } 119 | } 120 | function growLinear(){ 121 | if (data.actualStep <= data[j].grow.steps){ 122 | for (var i = 0; i < data[j].data.length; i++){ 123 | if (growing.stepDirection === "up"){ 124 | data[j].data[i][growing.valueIndex] = data[j].dataOrg[i][growing.valueIndex] / data[j].grow.steps * data.actualStep; 125 | } 126 | else if(growing.stepDirection === "down"){ 127 | data[j].data[i][growing.valueIndex] = data[j].dataOrg[i][growing.valueIndex] + (data[j].yaxis.max - data[j].dataOrg[i][growing.valueIndex]) / data[j].grow.steps * (data[j].grow.steps - data.actualStep); 128 | } 129 | } 130 | } 131 | } 132 | function growMaximum(){ 133 | if (data.actualStep <= data[j].grow.steps){ 134 | for (var i = 0; i < data[j].data.length; i++){ 135 | if (growing.stepDirection === "up"){ 136 | data[j].data[i][growing.valueIndex] = Math.min(data[j].dataOrg[i][growing.valueIndex], data[j].yaxis.max / data[j].grow.steps * data.actualStep); 137 | } 138 | else if (growing.stepDirection === "down"){ 139 | data[j].data[i][growing.valueIndex] = Math.max(data[j].dataOrg[i][growing.valueIndex], data[j].yaxis.max / data[j].grow.steps * (data[j].grow.steps - data.actualStep) ); 140 | } 141 | } 142 | } 143 | } 144 | function growDelay(){ 145 | if (data.actualStep == data[j].grow.steps){ 146 | for (var i = 0; i < data[j].data.length; i++){ 147 | data[j].data[i][growing.valueIndex] = data[j].dataOrg[i][growing.valueIndex]; 148 | } 149 | } 150 | } 151 | } 152 | function onResize() { 153 | if (growfunc) { window.clearTimeout(growfunc); growfunc = null; } 154 | } 155 | function shutdown(plot, eventHolder) { 156 | plot.getPlaceholder().unbind("resize", onResize); 157 | } 158 | function isPluginRegistered(pluginName) { 159 | var plugins = $.plot.plugins; 160 | for (var i = 0, len = plugins.length; i < len; i++) { 161 | var plug = plugins[i]; 162 | if (plug.name === pluginName) { 163 | return true; 164 | } 165 | } 166 | return false; 167 | } 168 | function clone(obj){ 169 | if(obj === null || typeof(obj) !== 'object'){ return obj;} 170 | var temp = new obj.constructor(); 171 | for(var key in obj){temp[key] = clone(obj[key]); } 172 | return temp; 173 | } 174 | } 175 | $.plot.plugins.push({ 176 | init: init, 177 | options: options, 178 | name: pluginName, 179 | version: pluginVersion 180 | }); 181 | })(jQuery); -------------------------------------------------------------------------------- /web/static/js/jquery.flot.hiddengraphs.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /* 6 | * Plugin to hide series in flot graphs. 7 | * 8 | * To activate, set legend.hideable to true in the flot options object. 9 | * To hide one or more series by default, set legend.hidden to an array of 10 | * label strings. 11 | * 12 | * At the moment, this only works with line and point graphs. 13 | * 14 | * Example: 15 | * 16 | * var plotdata = [ 17 | * { 18 | * data: [[1, 1], [2, 1], [3, 3], [4, 2], [5, 5]], 19 | * label: "graph 1" 20 | * }, 21 | * { 22 | * data: [[1, 0], [2, 1], [3, 0], [4, 4], [5, 3]], 23 | * label: "graph 2" 24 | * } 25 | * ]; 26 | * 27 | * plot = $.plot($("#placeholder"), plotdata, { 28 | * series: { 29 | * points: { show: true }, 30 | * lines: { show: true } 31 | * }, 32 | * legend: { 33 | * hideable: true, 34 | * hidden: ["graph 1", "graph 2"] 35 | * } 36 | * }); 37 | * 38 | */ 39 | (function ($) { 40 | var options = { }; 41 | var drawnOnce = false; 42 | 43 | function init(plot) { 44 | function findPlotSeries(label) { 45 | var plotdata = plot.getData(); 46 | for (var i = 0; i < plotdata.length; i++) { 47 | if (plotdata[i].label == label) { 48 | return plotdata[i]; 49 | } 50 | } 51 | return null; 52 | } 53 | 54 | function plotLabelClicked(label, mouseOut) { 55 | var series = findPlotSeries(label); 56 | if (!series) { 57 | return; 58 | } 59 | 60 | var switchedOff = false; 61 | if (typeof series.points.oldShow === "undefined") { 62 | series.points.oldShow = false; 63 | } 64 | if (typeof series.lines.oldShow === "undefined") { 65 | series.lines.oldShow = false; 66 | } 67 | if (series.points.show && !series.points.oldShow) { 68 | series.points.show = false; 69 | series.points.oldShow = true; 70 | switchedOff = true; 71 | } 72 | if (series.lines.show && !series.lines.oldShow) { 73 | series.lines.show = false; 74 | series.lines.oldShow = true; 75 | switchedOff = true; 76 | } 77 | if (switchedOff) { 78 | series.oldColor = series.color; 79 | series.color = "#fff"; 80 | } else { 81 | var switchedOn = false; 82 | if (!series.points.show && series.points.oldShow) { 83 | series.points.show = true; 84 | series.points.oldShow = false; 85 | switchedOn = true; 86 | } 87 | if (!series.lines.show && series.lines.oldShow) { 88 | series.lines.show = true; 89 | series.lines.oldShow = false; 90 | switchedOn = true; 91 | } 92 | if (switchedOn) { 93 | series.color = series.oldColor; 94 | } 95 | } 96 | 97 | // HACK: Reset the data, triggering recalculation of graph bounds 98 | plot.setData(plot.getData()); 99 | 100 | plot.setupGrid(); 101 | plot.draw(); 102 | } 103 | 104 | function plotLabelHandlers(plot, options) { 105 | $(".graphlabel").mouseenter(function() { $(this).css("cursor", "pointer"); }) 106 | .mouseleave(function() { $(this).css("cursor", "default"); }) 107 | .unbind("click").click(function() { plotLabelClicked($(this).parent().text()); }); 108 | if (!drawnOnce) { 109 | drawnOnce = true; 110 | if (options.legend.hidden) { 111 | for (var i = 0; i < options.legend.hidden.length; i++) { 112 | plotLabelClicked(options.legend.hidden[i], true); 113 | } 114 | } 115 | } 116 | } 117 | 118 | function checkOptions(plot, options) { 119 | if (!options.legend.hideable) { 120 | return; 121 | } 122 | 123 | options.legend.labelFormatter = function(label, series) { 124 | return '' + label + ''; 125 | }; 126 | 127 | // Really just needed for initial draw; the mouse-enter/leave 128 | // functions will call plotLabelHandlers() directly, since they 129 | // only call setupGrid(). 130 | plot.hooks.draw.push(function (plot, ctx) { 131 | plotLabelHandlers(plot, options); 132 | }); 133 | } 134 | 135 | plot.hooks.processOptions.push(checkOptions); 136 | 137 | function hideDatapointsIfNecessary(plot, s, datapoints) { 138 | if (!plot.getOptions().legend.hideable) { 139 | return; 140 | } 141 | 142 | if (!s.points.show && !s.lines.show) { 143 | s.datapoints.format = [ null, null ]; 144 | } 145 | } 146 | 147 | plot.hooks.processDatapoints.push(hideDatapointsIfNecessary); 148 | } 149 | 150 | $.plot.plugins.push({ 151 | init: init, 152 | options: options, 153 | name: 'hiddenGraphs', 154 | version: '1.0' 155 | }); 156 | 157 | })(jQuery); 158 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.image.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for plotting images. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | The data syntax is [ [ image, x1, y1, x2, y2 ], ... ] where (x1, y1) and 7 | (x2, y2) are where you intend the two opposite corners of the image to end up 8 | in the plot. Image must be a fully loaded Javascript image (you can make one 9 | with new Image()). If the image is not complete, it's skipped when plotting. 10 | 11 | There are two helpers included for retrieving images. The easiest work the way 12 | that you put in URLs instead of images in the data, like this: 13 | 14 | [ "myimage.png", 0, 0, 10, 10 ] 15 | 16 | Then call $.plot.image.loadData( data, options, callback ) where data and 17 | options are the same as you pass in to $.plot. This loads the images, replaces 18 | the URLs in the data with the corresponding images and calls "callback" when 19 | all images are loaded (or failed loading). In the callback, you can then call 20 | $.plot with the data set. See the included example. 21 | 22 | A more low-level helper, $.plot.image.load(urls, callback) is also included. 23 | Given a list of URLs, it calls callback with an object mapping from URL to 24 | Image object when all images are loaded or have failed loading. 25 | 26 | The plugin supports these options: 27 | 28 | series: { 29 | images: { 30 | show: boolean 31 | anchor: "corner" or "center" 32 | alpha: [ 0, 1 ] 33 | } 34 | } 35 | 36 | They can be specified for a specific series: 37 | 38 | $.plot( $("#placeholder"), [{ 39 | data: [ ... ], 40 | images: { ... } 41 | ]) 42 | 43 | Note that because the data format is different from usual data points, you 44 | can't use images with anything else in a specific data series. 45 | 46 | Setting "anchor" to "center" causes the pixels in the image to be anchored at 47 | the corner pixel centers inside of at the pixel corners, effectively letting 48 | half a pixel stick out to each side in the plot. 49 | 50 | A possible future direction could be support for tiling for large images (like 51 | Google Maps). 52 | 53 | */ 54 | 55 | (function ($) { 56 | var options = { 57 | series: { 58 | images: { 59 | show: false, 60 | alpha: 1, 61 | anchor: "corner" // or "center" 62 | } 63 | } 64 | }; 65 | 66 | $.plot.image = {}; 67 | 68 | $.plot.image.loadDataImages = function (series, options, callback) { 69 | var urls = [], points = []; 70 | 71 | var defaultShow = options.series.images.show; 72 | 73 | $.each(series, function (i, s) { 74 | if (!(defaultShow || s.images.show)) 75 | return; 76 | 77 | if (s.data) 78 | s = s.data; 79 | 80 | $.each(s, function (i, p) { 81 | if (typeof p[0] == "string") { 82 | urls.push(p[0]); 83 | points.push(p); 84 | } 85 | }); 86 | }); 87 | 88 | $.plot.image.load(urls, function (loadedImages) { 89 | $.each(points, function (i, p) { 90 | var url = p[0]; 91 | if (loadedImages[url]) 92 | p[0] = loadedImages[url]; 93 | }); 94 | 95 | callback(); 96 | }); 97 | } 98 | 99 | $.plot.image.load = function (urls, callback) { 100 | var missing = urls.length, loaded = {}; 101 | if (missing == 0) 102 | callback({}); 103 | 104 | $.each(urls, function (i, url) { 105 | var handler = function () { 106 | --missing; 107 | 108 | loaded[url] = this; 109 | 110 | if (missing == 0) 111 | callback(loaded); 112 | }; 113 | 114 | $('').load(handler).error(handler).attr('src', url); 115 | }); 116 | }; 117 | 118 | function drawSeries(plot, ctx, series) { 119 | var plotOffset = plot.getPlotOffset(); 120 | 121 | if (!series.images || !series.images.show) 122 | return; 123 | 124 | var points = series.datapoints.points, 125 | ps = series.datapoints.pointsize; 126 | 127 | for (var i = 0; i < points.length; i += ps) { 128 | var img = points[i], 129 | x1 = points[i + 1], y1 = points[i + 2], 130 | x2 = points[i + 3], y2 = points[i + 4], 131 | xaxis = series.xaxis, yaxis = series.yaxis, 132 | tmp; 133 | 134 | // actually we should check img.complete, but it 135 | // appears to be a somewhat unreliable indicator in 136 | // IE6 (false even after load event) 137 | if (!img || img.width <= 0 || img.height <= 0) 138 | continue; 139 | 140 | if (x1 > x2) { 141 | tmp = x2; 142 | x2 = x1; 143 | x1 = tmp; 144 | } 145 | if (y1 > y2) { 146 | tmp = y2; 147 | y2 = y1; 148 | y1 = tmp; 149 | } 150 | 151 | // if the anchor is at the center of the pixel, expand the 152 | // image by 1/2 pixel in each direction 153 | if (series.images.anchor == "center") { 154 | tmp = 0.5 * (x2-x1) / (img.width - 1); 155 | x1 -= tmp; 156 | x2 += tmp; 157 | tmp = 0.5 * (y2-y1) / (img.height - 1); 158 | y1 -= tmp; 159 | y2 += tmp; 160 | } 161 | 162 | // clip 163 | if (x1 == x2 || y1 == y2 || 164 | x1 >= xaxis.max || x2 <= xaxis.min || 165 | y1 >= yaxis.max || y2 <= yaxis.min) 166 | continue; 167 | 168 | var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height; 169 | if (x1 < xaxis.min) { 170 | sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1); 171 | x1 = xaxis.min; 172 | } 173 | 174 | if (x2 > xaxis.max) { 175 | sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1); 176 | x2 = xaxis.max; 177 | } 178 | 179 | if (y1 < yaxis.min) { 180 | sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1); 181 | y1 = yaxis.min; 182 | } 183 | 184 | if (y2 > yaxis.max) { 185 | sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1); 186 | y2 = yaxis.max; 187 | } 188 | 189 | x1 = xaxis.p2c(x1); 190 | x2 = xaxis.p2c(x2); 191 | y1 = yaxis.p2c(y1); 192 | y2 = yaxis.p2c(y2); 193 | 194 | // the transformation may have swapped us 195 | if (x1 > x2) { 196 | tmp = x2; 197 | x2 = x1; 198 | x1 = tmp; 199 | } 200 | if (y1 > y2) { 201 | tmp = y2; 202 | y2 = y1; 203 | y1 = tmp; 204 | } 205 | 206 | tmp = ctx.globalAlpha; 207 | ctx.globalAlpha *= series.images.alpha; 208 | ctx.drawImage(img, 209 | sx1, sy1, sx2 - sx1, sy2 - sy1, 210 | x1 + plotOffset.left, y1 + plotOffset.top, 211 | x2 - x1, y2 - y1); 212 | ctx.globalAlpha = tmp; 213 | } 214 | } 215 | 216 | function processRawData(plot, series, data, datapoints) { 217 | if (!series.images.show) 218 | return; 219 | 220 | // format is Image, x1, y1, x2, y2 (opposite corners) 221 | datapoints.format = [ 222 | { required: true }, 223 | { x: true, number: true, required: true }, 224 | { y: true, number: true, required: true }, 225 | { x: true, number: true, required: true }, 226 | { y: true, number: true, required: true } 227 | ]; 228 | } 229 | 230 | function init(plot) { 231 | plot.hooks.processRawData.push(processRawData); 232 | plot.hooks.drawSeries.push(drawSeries); 233 | } 234 | 235 | $.plot.plugins.push({ 236 | init: init, 237 | options: options, 238 | name: 'image', 239 | version: '1.1' 240 | }); 241 | })(jQuery); 242 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.multihighlight-delta.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * User: Patrick de Lanauze 4 | * Date: 2013-03-21 5 | * Time: 10:59 AM 6 | * 7 | */ 8 | 9 | (function (name, definition) { 10 | var theModule = definition(), 11 | // this is considered "safe": 12 | hasDefine = typeof define === 'function' && define.amd, 13 | // hasDefine = typeof define === 'function', 14 | hasExports = typeof module !== 'undefined' && module.exports; 15 | 16 | if (hasDefine) { // AMD Module 17 | define(theModule); 18 | } else if (hasExports) { // Node.js Module 19 | module.exports = theModule; 20 | } else { // Assign to common namespaces or simply the global object (window) 21 | (this.jQuery || this.ender || this.$ || this)[name] = theModule; 22 | } 23 | })('core', function () { 24 | 25 | var MultiHighlightDelta = { 26 | options: { 27 | multihighlightdelta: { 28 | mode: 'x', 29 | tooltipOffsetX: 20, 30 | tooltipOffsetY: 20, 31 | tooltipTemplate: '<%= body %>
ValueChange
', 32 | dataPointTemplate: '<%= series.label %><%= datapoint[1] %><%= (delta > 0 ? "+" : "") %><%= delta %>', 33 | transformDataPointData: false, 34 | tooltipStyles: { 35 | position: 'absolute', 36 | display: 'none', 37 | 'background': '#fff', 38 | 'z-index': '100', 39 | 'padding': '0.4em 0.6em', 40 | 'border-radius': '0.5em', 41 | 'font-size': '0.8em', 42 | 'border': '1px solid #111' 43 | }, 44 | delta: function (previousDataPoint, dataPoint) { 45 | if (!previousDataPoint) { 46 | return ''; 47 | } 48 | var chng = dataPoint[1] - previousDataPoint[1]; 49 | 50 | return (-1*chng).toFixed(2); 51 | } 52 | } 53 | } 54 | }; 55 | var MultiHighlightDeltaPlugin = function (plot) { 56 | this.plot = plot; 57 | }; 58 | 59 | /** 60 | * Thanks John Resig! [ http://ejohn.org/blog/javascript-micro-templating/ ] 61 | * Based heavily off john's implementation , but removed caching 62 | */ 63 | var compileTemplate = function (str) { 64 | 65 | // Generate a reusable function that will serve as a template 66 | // generator (and should be cached by its caller). 67 | /* jshint -W121 */ 68 | return new Function("obj", 69 | "var p=[],print=function(){p.push.apply(p,arguments);};" + 70 | 71 | // Introduce the data as local variables using with(){} 72 | "with(obj){p.push('" + 73 | 74 | // Convert the template into pure JavaScript 75 | str.replace(/[\r\t\n]/g, " ") 76 | .split("<%").join("\t") 77 | .replace(/((^|%>)[^\t]*)'/g, "$1\r") 78 | .replace(/\t=(.*?)%>/g, "',$1,'") 79 | .split("\t").join("');") 80 | .split("%>").join("p.push('") 81 | .split("\r").join("\\'") + "');}return p.join('');"); 82 | }; 83 | 84 | MultiHighlightDeltaPlugin.prototype = { 85 | initialize: function () { 86 | var ctx = this; 87 | 88 | var handlerProxies = { 89 | onPlotHover: $.proxy(ctx.onPlotHover, ctx), 90 | onMouseOut: $.proxy(ctx.onMouseOut, ctx) 91 | }; 92 | 93 | this.plot.hooks.bindEvents.push(function (plot) { 94 | if (!plot.getOptions().multihighlightdelta) { 95 | return; 96 | } 97 | 98 | var options = ctx.plot.getOptions().multihighlightdelta || {}; 99 | for (var key in MultiHighlightDelta.options.multihighlightdelta) { 100 | if (typeof options[key] === 'undefined') { 101 | options[key] = MultiHighlightDelta.options.multihighlightdelta[key]; 102 | } 103 | } 104 | 105 | plot.getPlaceholder().on('plothover', handlerProxies.onPlotHover); 106 | plot.getPlaceholder().on('mouseout', handlerProxies.onMouseOut); 107 | 108 | // Keep a cache of the template 109 | ctx.tooltipTemplate = compileTemplate(options.tooltipTemplate); 110 | ctx.dataPointTemplate = compileTemplate(options.dataPointTemplate); 111 | 112 | }); 113 | this.plot.hooks.shutdown.push(function (plot) { 114 | plot.getPlaceholder().off('plothover', handlerProxies.onPlotHover); 115 | plot.getPlaceholder().off('mouseout', handlerProxies.onMouseOut); 116 | }); 117 | 118 | return this; 119 | }, 120 | findOrCreateTooltip: function (tooltipStyles) { 121 | var $tip = null; 122 | if ($('#flotMultihighlightTip').length > 0) { 123 | $tip = $('#flotMultihighlightTip'); 124 | } 125 | else { 126 | $tip = $('
').attr('id', 'flotMultihighlightTip').addClass('flot-tooltip').css(tooltipStyles).appendTo('body'); 127 | } 128 | return $tip; 129 | }, 130 | onPlotHover: function (event, position, item) { 131 | var data = this.plot.getData(); 132 | var options = this.plot.getOptions().multihighlightdelta; 133 | var deltaFunction = options.delta; 134 | var mode = options.mode || 'x'; 135 | var index = 0; 136 | if (mode === 'x') { 137 | index = 0; 138 | } else if (mode === 'y') { 139 | index = 1; 140 | } else { 141 | throw new Error('Mode \'' + mode + '\', is not recognized, must be x or y'); 142 | } 143 | 144 | if (item) { 145 | 146 | this.plot.unhighlight(); 147 | var matchingDataPoints = []; 148 | 149 | for (var i = 0 , ii = data.length; i < ii; i++) { 150 | // Find the data point in the other series that matches the current datapoint 151 | var seriesData = data[i].data; 152 | for (var j = 0 , jj = seriesData.length; j < jj; j++) { 153 | if (seriesData[j][index] === item.datapoint[index]) { 154 | matchingDataPoints.push({ 155 | seriesData: data[i], 156 | dataPoint: seriesData[j], 157 | delta: deltaFunction(j > 0 ? seriesData[j - 1] : null, seriesData[j]) 158 | }); 159 | } 160 | } 161 | } 162 | 163 | var childrenTexts = []; 164 | for (var i = 0 , ii = matchingDataPoints.length; i < ii; i++) { 165 | var seriesData = matchingDataPoints[i].seriesData; 166 | var dataPoint = matchingDataPoints[i].dataPoint; 167 | var delta = matchingDataPoints[i].delta; 168 | this.plot.highlight(seriesData, dataPoint); 169 | 170 | var data = { 171 | series: seriesData, 172 | datapoint: dataPoint, 173 | delta: delta 174 | }; 175 | if (options.transformDataPointData){ 176 | data = options.transformDataPointData(data); 177 | } 178 | var text = this.dataPointTemplate(data); 179 | childrenTexts.push(text); 180 | } 181 | 182 | var tooltipText = this.tooltipTemplate({ 183 | body: childrenTexts.join('\n') 184 | }); 185 | 186 | var $tooltip = this.findOrCreateTooltip(options.tooltipStyles); 187 | 188 | // If we are going to overflow outside the screen's dimensions, display it to the left instead 189 | 190 | 191 | var xPositionProperty = 'left'; 192 | var yPositionProperty = 'top'; 193 | var xPosition = position.pageX + options.tooltipOffsetX; 194 | var yPosition = position.pageY + options.tooltipOffsetY; 195 | $tooltip.html(tooltipText); // So that we can use dimensions right away, we set the content immediately 196 | var tooltipWidth = $tooltip.width(); 197 | var tooltipHeight = $tooltip.height(); 198 | var css = { 199 | top: 'auto', 200 | left: 'auto', 201 | right: 'auto', 202 | bottom: 'auto' 203 | }; 204 | 205 | var pageWidth = window.innerWidth; 206 | var pageHeight = window.innerHeight; 207 | if (xPosition + tooltipWidth > pageWidth){ 208 | xPositionProperty = 'right'; 209 | xPosition = pageWidth - position.pageX + options.tooltipOffsetX; 210 | } 211 | if (yPosition + tooltipHeight > pageHeight){ 212 | yPositionProperty = 'bottom'; 213 | yPosition = pageHeight - position.pageY + options.tooltipOffsetY; 214 | } 215 | 216 | css[xPositionProperty] = xPosition; 217 | css[yPositionProperty] = yPosition; 218 | $tooltip.css(css).show(); 219 | } 220 | }, 221 | onMouseOut: function () { 222 | this.plot.unhighlight(); 223 | $('#flotMultihighlightTip').hide().css({ 224 | top: 'auto', 225 | left: 'auto', 226 | right: 'auto', 227 | bottom: 'auto' 228 | }); 229 | } 230 | }; 231 | 232 | MultiHighlightDelta.init = function (plot) { 233 | new MultiHighlightDeltaPlugin(plot).initialize(); 234 | }; 235 | 236 | // Wire up the plugin with flot 237 | this.jQuery.plot.plugins.push({ 238 | init: MultiHighlightDelta.init, 239 | options: MultiHighlightDelta.options, 240 | name: 'multihighlightdelta', 241 | version: '0.1' 242 | }); 243 | 244 | // Nothing to wire since we're injecting the plugin inside flot 245 | return {}; 246 | 247 | }); 248 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.resize.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for automatically redrawing plots as the placeholder resizes. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | It works by listening for changes on the placeholder div (through the jQuery 7 | resize event plugin) - if the size changes, it will redraw the plot. 8 | 9 | There are no options. If you need to disable the plugin for some plots, you 10 | can just fix the size of their placeholders. 11 | 12 | */ 13 | 14 | /* Inline dependency: 15 | * jQuery resize event - v1.1 - 3/14/2010 16 | * http://benalman.com/projects/jquery-resize-plugin/ 17 | * 18 | * Copyright (c) 2010 "Cowboy" Ben Alman 19 | * Dual licensed under the MIT and GPL licenses. 20 | * http://benalman.com/about/license/ 21 | */ 22 | 23 | (function($,t,n){function p(){for(var n=r.length-1;n>=0;n--){var o=$(r[n]);if(o[0]==t||o.is(":visible")){var h=o.width(),d=o.height(),v=o.data(a);!v||h===v.w&&d===v.h?i[f]=i[l]:(i[f]=i[c],o.trigger(u,[v.w=h,v.h=d]))}else v=o.data(a),v.w=0,v.h=0}s!==null&&(s=t.requestAnimationFrame(p))}var r=[],i=$.resize=$.extend($.resize,{}),s,o="setTimeout",u="resize",a=u+"-special-event",f="delay",l="pendingDelay",c="activeDelay",h="throttleWindow";i[l]=250,i[c]=20,i[f]=i[l],i[h]=!0,$.event.special[u]={setup:function(){if(!i[h]&&this[o])return!1;var t=$(this);r.push(this),t.data(a,{w:t.width(),h:t.height()}),r.length===1&&(s=n,p())},teardown:function(){if(!i[h]&&this[o])return!1;var t=$(this);for(var n=r.length-1;n>=0;n--)if(r[n]==this){r.splice(n,1);break}t.removeData(a),r.length||(cancelAnimationFrame(s),s=null)},add:function(t){function s(t,i,s){var o=$(this),u=o.data(a);u.w=i!==n?i:o.width(),u.h=s!==n?s:o.height(),r.apply(this,arguments)}if(!i[h]&&this[o])return!1;var r;if($.isFunction(t))return r=t,s;r=t.handler,t.handler=s}},t.requestAnimationFrame||(t.requestAnimationFrame=function(){return t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.oRequestAnimationFrame||t.msRequestAnimationFrame||function(e,n){return t.setTimeout(e,i[f])}}()),t.cancelAnimationFrame||(t.cancelAnimationFrame=function(){return t.webkitCancelRequestAnimationFrame||t.mozCancelRequestAnimationFrame||t.oCancelRequestAnimationFrame||t.msCancelRequestAnimationFrame||clearTimeout}())})(jQuery,this); 24 | 25 | (function ($) { 26 | var options = { }; // no options 27 | 28 | function init(plot) { 29 | function onResize() { 30 | var placeholder = plot.getPlaceholder(); 31 | 32 | // somebody might have hidden us and we can't plot 33 | // when we don't have the dimensions 34 | if (placeholder.width() == 0 || placeholder.height() == 0) 35 | return; 36 | 37 | plot.resize(); 38 | plot.setupGrid(); 39 | plot.draw(); 40 | } 41 | 42 | function bindEvents(plot, eventHolder) { 43 | plot.getPlaceholder().resize(onResize); 44 | } 45 | 46 | function shutdown(plot, eventHolder) { 47 | plot.getPlaceholder().unbind("resize", onResize); 48 | } 49 | 50 | plot.hooks.bindEvents.push(bindEvents); 51 | plot.hooks.shutdown.push(shutdown); 52 | } 53 | 54 | $.plot.plugins.push({ 55 | init: init, 56 | options: options, 57 | name: 'resize', 58 | version: '1.0' 59 | }); 60 | })(jQuery); 61 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.resize.min.js: -------------------------------------------------------------------------------- 1 | /* Javascript plotting library for jQuery, version 0.8.3. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | */ 7 | (function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this);(function($){var options={};function init(plot){function onResize(){var placeholder=plot.getPlaceholder();if(placeholder.width()==0||placeholder.height()==0)return;plot.resize();plot.setupGrid();plot.draw()}function bindEvents(plot,eventHolder){plot.getPlaceholder().resize(onResize)}function shutdown(plot,eventHolder){plot.getPlaceholder().unbind("resize",onResize)}plot.hooks.bindEvents.push(bindEvents);plot.hooks.shutdown.push(shutdown)}$.plot.plugins.push({init:init,options:options,name:"resize",version:"1.0"})})(jQuery); -------------------------------------------------------------------------------- /web/static/js/jquery.flot.smoother.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Written by Madison Williams 4 | * madisonwilliams.net 5 | */ 6 | 7 | (function($) { 8 | var bar, 9 | handle, 10 | handle_distance, 11 | h_offset, 12 | l_boundary, 13 | r_boundary, 14 | handle_width; 15 | function init(plot) { 16 | var placeholder = plot.getPlaceholder(), 17 | phwidth = $(placeholder).width(); 18 | $("#smoother_"+placeholder[0].id).remove() 19 | $(placeholder).after("
"); 20 | bar = $(".smoother"); 21 | handle = $(".smoother_handle"); 22 | handle_distance = $(bar).offset().left; 23 | handle_width = Math.floor(phwidth / 20); 24 | 25 | l_boundary = 0; 26 | r_boundary = (phwidth - handle_width); 27 | 28 | $(bar).css({ 29 | width : phwidth+"px", 30 | height: "30px", 31 | border : "2px #999 solid", 32 | backgroundColor : "#fefefe" 33 | }) 34 | 35 | $(handle).css({ 36 | width : handle_width+"px", 37 | height: "28px", 38 | top : "0px", 39 | position : "relative", 40 | "-moz-border-radius" : "5px", 41 | border : "1px #999 solid", 42 | left : "1px", 43 | backgroundColor : "#CCC" 44 | }) 45 | function bind() { 46 | var min=handle_distance + (handle_width/2), 47 | max=handle_distance + (phwidth), 48 | scale = max-min, 49 | firstrun = true, 50 | percentage, 51 | divval, 52 | orig_points, 53 | max_smooth, 54 | oldObj=[] 55 | smoothObj=[], 56 | newData=[]; 57 | 58 | function smoothit(intensity) { 59 | var oLeft, 60 | oRight, 61 | oArr, 62 | oData, 63 | diff; 64 | divval = scale / intensity; 65 | percentage = (Math.floor(100 / divval)) / 100; 66 | $(oldObj).each(function(ind) { 67 | 68 | newData[ind] = { 69 | data : [], 70 | label : oldObj[ind].label, 71 | yaxis : oldObj[ind].yaxis 72 | }; 73 | 74 | oData = oldObj[ind].data; 75 | $(oData).each(function(index) { 76 | newData[ind].data[index] = [ 77 | oldObj[ind].data[index][0] + (smoothObj[ind].data[index][0] - oldObj[ind].data[index][0]) * percentage, 78 | oldObj[ind].data[index][1] + (smoothObj[ind].data[index][1] - oldObj[ind].data[index][1]) * percentage 79 | ] 80 | }) 81 | }) 82 | plot.setData(newData); 83 | plot.draw() 84 | 85 | } 86 | function draggable() { 87 | var oLeft, 88 | boxPos; 89 | $(document).mousemove(function(e) { 90 | boxPos = (e.pageX - handle_distance) - handle_width / 2; 91 | if (boxPos > l_boundary && boxPos < r_boundary) { 92 | $(handle).css({ 93 | left : boxPos 94 | }) 95 | smoothit(e.pageX) 96 | } 97 | 98 | 99 | }) 100 | } 101 | 102 | function draggableoff() { 103 | $(document).unbind("mousemove"); 104 | } 105 | 106 | $(bar).mousedown(function(e) { 107 | e.preventDefault() 108 | if (firstrun) { 109 | //create the "perfectly straight" input object 110 | $(plot.getData()).each(function(index) { 111 | oldObj.push({data : this.data, label : this.label, yaxis : this.yaxis.n}) //old input object 112 | var that = this; 113 | max_smooth = (function() { 114 | var arr=[], 115 | points = that.data, 116 | oLeft = points[0][0], 117 | oRight= points[0][1], 118 | p_len = points.length, 119 | f_left = points[0][0], 120 | f_right = points[0][1], 121 | l_left = points[p_len-1][0], 122 | l_right = points[p_len-1][1], 123 | left_inc = (l_left - f_left) / p_len, 124 | right_inc = (l_right - f_right) / p_len; 125 | $(points).each(function(index) { 126 | arr[index] = [oLeft,oRight] 127 | oLeft+=left_inc; 128 | oRight+=right_inc; 129 | }) 130 | return arr; 131 | }()); 132 | smoothObj[index] = {data : max_smooth, label : this.label, yaxis : this.yaxis.n} 133 | }) 134 | firstrun=false; 135 | } 136 | draggable(); 137 | }) 138 | 139 | $(document).mouseup(function(e) { 140 | draggableoff(); 141 | }) 142 | } 143 | bind() 144 | } 145 | $.plot.plugins.push({ 146 | init : init, 147 | options: {}, 148 | name : "smoother", 149 | version : "0.1" 150 | }) 151 | }(jQuery)) 152 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.stack.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for stacking data sets rather than overlyaing them. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | The plugin assumes the data is sorted on x (or y if stacking horizontally). 7 | For line charts, it is assumed that if a line has an undefined gap (from a 8 | null point), then the line above it should have the same gap - insert zeros 9 | instead of "null" if you want another behaviour. This also holds for the start 10 | and end of the chart. Note that stacking a mix of positive and negative values 11 | in most instances doesn't make sense (so it looks weird). 12 | 13 | Two or more series are stacked when their "stack" attribute is set to the same 14 | key (which can be any number or string or just "true"). To specify the default 15 | stack, you can set the stack option like this: 16 | 17 | series: { 18 | stack: null/false, true, or a key (number/string) 19 | } 20 | 21 | You can also specify it for a single series, like this: 22 | 23 | $.plot( $("#placeholder"), [{ 24 | data: [ ... ], 25 | stack: true 26 | }]) 27 | 28 | The stacking order is determined by the order of the data series in the array 29 | (later series end up on top of the previous). 30 | 31 | Internally, the plugin modifies the datapoints in each series, adding an 32 | offset to the y value. For line series, extra data points are inserted through 33 | interpolation. If there's a second y value, it's also adjusted (e.g for bar 34 | charts or filled areas). 35 | 36 | */ 37 | 38 | (function ($) { 39 | var options = { 40 | series: { stack: null } // or number/string 41 | }; 42 | 43 | function init(plot) { 44 | function findMatchingSeries(s, allseries) { 45 | var res = null; 46 | for (var i = 0; i < allseries.length; ++i) { 47 | if (s == allseries[i]) 48 | break; 49 | 50 | if (allseries[i].stack == s.stack) 51 | res = allseries[i]; 52 | } 53 | 54 | return res; 55 | } 56 | 57 | function stackData(plot, s, datapoints) { 58 | if (s.stack == null || s.stack === false) 59 | return; 60 | 61 | var other = findMatchingSeries(s, plot.getData()); 62 | if (!other) 63 | return; 64 | 65 | var ps = datapoints.pointsize, 66 | points = datapoints.points, 67 | otherps = other.datapoints.pointsize, 68 | otherpoints = other.datapoints.points, 69 | newpoints = [], 70 | px, py, intery, qx, qy, bottom, 71 | withlines = s.lines.show, 72 | horizontal = s.bars.horizontal, 73 | withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), 74 | withsteps = withlines && s.lines.steps, 75 | fromgap = true, 76 | keyOffset = horizontal ? 1 : 0, 77 | accumulateOffset = horizontal ? 0 : 1, 78 | i = 0, j = 0, l, m; 79 | 80 | while (true) { 81 | if (i >= points.length) 82 | break; 83 | 84 | l = newpoints.length; 85 | 86 | if (points[i] == null) { 87 | // copy gaps 88 | for (m = 0; m < ps; ++m) 89 | newpoints.push(points[i + m]); 90 | i += ps; 91 | } 92 | else if (j >= otherpoints.length) { 93 | // for lines, we can't use the rest of the points 94 | if (!withlines) { 95 | for (m = 0; m < ps; ++m) 96 | newpoints.push(points[i + m]); 97 | } 98 | i += ps; 99 | } 100 | else if (otherpoints[j] == null) { 101 | // oops, got a gap 102 | for (m = 0; m < ps; ++m) 103 | newpoints.push(null); 104 | fromgap = true; 105 | j += otherps; 106 | } 107 | else { 108 | // cases where we actually got two points 109 | px = points[i + keyOffset]; 110 | py = points[i + accumulateOffset]; 111 | qx = otherpoints[j + keyOffset]; 112 | qy = otherpoints[j + accumulateOffset]; 113 | bottom = 0; 114 | 115 | if (px == qx) { 116 | for (m = 0; m < ps; ++m) 117 | newpoints.push(points[i + m]); 118 | 119 | newpoints[l + accumulateOffset] += qy; 120 | bottom = qy; 121 | 122 | i += ps; 123 | j += otherps; 124 | } 125 | else if (px > qx) { 126 | // we got past point below, might need to 127 | // insert interpolated extra point 128 | if (withlines && i > 0 && points[i - ps] != null) { 129 | intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); 130 | newpoints.push(qx); 131 | newpoints.push(intery + qy); 132 | for (m = 2; m < ps; ++m) 133 | newpoints.push(points[i + m]); 134 | bottom = qy; 135 | } 136 | 137 | j += otherps; 138 | } 139 | else { // px < qx 140 | if (fromgap && withlines) { 141 | // if we come from a gap, we just skip this point 142 | i += ps; 143 | continue; 144 | } 145 | 146 | for (m = 0; m < ps; ++m) 147 | newpoints.push(points[i + m]); 148 | 149 | // we might be able to interpolate a point below, 150 | // this can give us a better y 151 | if (withlines && j > 0 && otherpoints[j - otherps] != null) 152 | bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); 153 | 154 | newpoints[l + accumulateOffset] += bottom; 155 | 156 | i += ps; 157 | } 158 | 159 | fromgap = false; 160 | 161 | if (l != newpoints.length && withbottom) 162 | newpoints[l + 2] += bottom; 163 | } 164 | 165 | // maintain the line steps invariant 166 | if (withsteps && l != newpoints.length && l > 0 167 | && newpoints[l] != null 168 | && newpoints[l] != newpoints[l - ps] 169 | && newpoints[l + 1] != newpoints[l - ps + 1]) { 170 | for (m = 0; m < ps; ++m) 171 | newpoints[l + ps + m] = newpoints[l + m]; 172 | newpoints[l + 1] = newpoints[l - ps + 1]; 173 | } 174 | } 175 | 176 | datapoints.points = newpoints; 177 | } 178 | 179 | plot.hooks.processDatapoints.push(stackData); 180 | } 181 | 182 | $.plot.plugins.push({ 183 | init: init, 184 | options: options, 185 | name: 'stack', 186 | version: '1.2' 187 | }); 188 | })(jQuery); 189 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.symbol.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin that adds some extra symbols for plotting points. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | The symbols are accessed as strings through the standard symbol options: 7 | 8 | series: { 9 | points: { 10 | symbol: "square" // or "diamond", "triangle", "cross" 11 | } 12 | } 13 | 14 | */ 15 | 16 | (function ($) { 17 | function processRawData(plot, series, datapoints) { 18 | // we normalize the area of each symbol so it is approximately the 19 | // same as a circle of the given radius 20 | 21 | var handlers = { 22 | square: function (ctx, x, y, radius, shadow) { 23 | // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 24 | var size = radius * Math.sqrt(Math.PI) / 2; 25 | ctx.rect(x - size, y - size, size + size, size + size); 26 | }, 27 | diamond: function (ctx, x, y, radius, shadow) { 28 | // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) 29 | var size = radius * Math.sqrt(Math.PI / 2); 30 | ctx.moveTo(x - size, y); 31 | ctx.lineTo(x, y - size); 32 | ctx.lineTo(x + size, y); 33 | ctx.lineTo(x, y + size); 34 | ctx.lineTo(x - size, y); 35 | }, 36 | triangle: function (ctx, x, y, radius, shadow) { 37 | // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) 38 | var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); 39 | var height = size * Math.sin(Math.PI / 3); 40 | ctx.moveTo(x - size/2, y + height/2); 41 | ctx.lineTo(x + size/2, y + height/2); 42 | if (!shadow) { 43 | ctx.lineTo(x, y - height/2); 44 | ctx.lineTo(x - size/2, y + height/2); 45 | } 46 | }, 47 | cross: function (ctx, x, y, radius, shadow) { 48 | // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 49 | var size = radius * Math.sqrt(Math.PI) / 2; 50 | ctx.moveTo(x - size, y - size); 51 | ctx.lineTo(x + size, y + size); 52 | ctx.moveTo(x - size, y + size); 53 | ctx.lineTo(x + size, y - size); 54 | } 55 | }; 56 | 57 | var s = series.points.symbol; 58 | if (handlers[s]) 59 | series.points.symbol = handlers[s]; 60 | } 61 | 62 | function init(plot) { 63 | plot.hooks.processDatapoints.push(processRawData); 64 | } 65 | 66 | $.plot.plugins.push({ 67 | init: init, 68 | name: 'symbols', 69 | version: '1.0' 70 | }); 71 | })(jQuery); 72 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.threshold.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for thresholding data. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | The plugin supports these options: 7 | 8 | series: { 9 | threshold: { 10 | below: number 11 | color: colorspec 12 | } 13 | } 14 | 15 | It can also be applied to a single series, like this: 16 | 17 | $.plot( $("#placeholder"), [{ 18 | data: [ ... ], 19 | threshold: { ... } 20 | }]) 21 | 22 | An array can be passed for multiple thresholding, like this: 23 | 24 | threshold: [{ 25 | below: number1 26 | color: color1 27 | },{ 28 | below: number2 29 | color: color2 30 | }] 31 | 32 | These multiple threshold objects can be passed in any order since they are 33 | sorted by the processing function. 34 | 35 | The data points below "below" are drawn with the specified color. This makes 36 | it easy to mark points below 0, e.g. for budget data. 37 | 38 | Internally, the plugin works by splitting the data into two series, above and 39 | below the threshold. The extra series below the threshold will have its label 40 | cleared and the special "originSeries" attribute set to the original series. 41 | You may need to check for this in hover events. 42 | 43 | */ 44 | 45 | (function ($) { 46 | var options = { 47 | series: { threshold: null } // or { below: number, color: color spec} 48 | }; 49 | 50 | function init(plot) { 51 | function thresholdData(plot, s, datapoints, below, color) { 52 | var ps = datapoints.pointsize, i, x, y, p, prevp, 53 | thresholded = $.extend({}, s); // note: shallow copy 54 | 55 | thresholded.datapoints = { points: [], pointsize: ps, format: datapoints.format }; 56 | thresholded.label = null; 57 | thresholded.color = color; 58 | thresholded.threshold = null; 59 | thresholded.originSeries = s; 60 | thresholded.data = []; 61 | 62 | var origpoints = datapoints.points, 63 | addCrossingPoints = s.lines.show; 64 | 65 | var threspoints = []; 66 | var newpoints = []; 67 | var m; 68 | 69 | for (i = 0; i < origpoints.length; i += ps) { 70 | x = origpoints[i]; 71 | y = origpoints[i + 1]; 72 | 73 | prevp = p; 74 | if (y < below) 75 | p = threspoints; 76 | else 77 | p = newpoints; 78 | 79 | if (addCrossingPoints && prevp != p && x != null 80 | && i > 0 && origpoints[i - ps] != null) { 81 | var interx = x + (below - y) * (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]); 82 | prevp.push(interx); 83 | prevp.push(below); 84 | for (m = 2; m < ps; ++m) 85 | prevp.push(origpoints[i + m]); 86 | 87 | p.push(null); // start new segment 88 | p.push(null); 89 | for (m = 2; m < ps; ++m) 90 | p.push(origpoints[i + m]); 91 | p.push(interx); 92 | p.push(below); 93 | for (m = 2; m < ps; ++m) 94 | p.push(origpoints[i + m]); 95 | } 96 | 97 | p.push(x); 98 | p.push(y); 99 | for (m = 2; m < ps; ++m) 100 | p.push(origpoints[i + m]); 101 | } 102 | 103 | datapoints.points = newpoints; 104 | thresholded.datapoints.points = threspoints; 105 | 106 | if (thresholded.datapoints.points.length > 0) { 107 | var origIndex = $.inArray(s, plot.getData()); 108 | // Insert newly-generated series right after original one (to prevent it from becoming top-most) 109 | plot.getData().splice(origIndex + 1, 0, thresholded); 110 | } 111 | 112 | // FIXME: there are probably some edge cases left in bars 113 | } 114 | 115 | function processThresholds(plot, s, datapoints) { 116 | if (!s.threshold) 117 | return; 118 | 119 | if (s.threshold instanceof Array) { 120 | s.threshold.sort(function(a, b) { 121 | return a.below - b.below; 122 | }); 123 | 124 | $(s.threshold).each(function(i, th) { 125 | thresholdData(plot, s, datapoints, th.below, th.color); 126 | }); 127 | } 128 | else { 129 | thresholdData(plot, s, datapoints, s.threshold.below, s.threshold.color); 130 | } 131 | } 132 | 133 | plot.hooks.processDatapoints.push(processThresholds); 134 | } 135 | 136 | $.plot.plugins.push({ 137 | init: init, 138 | options: options, 139 | name: 'threshold', 140 | version: '1.2' 141 | }); 142 | })(jQuery); 143 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.time.js: -------------------------------------------------------------------------------- 1 | /* Pretty handling of time axes. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | Set axis.mode to "time" to enable. See the section "Time series data" in 7 | API.txt for details. 8 | 9 | */ 10 | 11 | (function($) { 12 | 13 | var options = { 14 | xaxis: { 15 | timezone: null, // "browser" for local to the client or timezone for timezone-js 16 | timeformat: null, // format string to use 17 | twelveHourClock: false, // 12 or 24 time in time mode 18 | monthNames: null // list of names of months 19 | } 20 | }; 21 | 22 | // round to nearby lower multiple of base 23 | 24 | function floorInBase(n, base) { 25 | return base * Math.floor(n / base); 26 | } 27 | 28 | // Returns a string with the date d formatted according to fmt. 29 | // A subset of the Open Group's strftime format is supported. 30 | 31 | function formatDate(d, fmt, monthNames, dayNames) { 32 | 33 | if (typeof d.strftime == "function") { 34 | return d.strftime(fmt); 35 | } 36 | 37 | var leftPad = function(n, pad) { 38 | n = "" + n; 39 | pad = "" + (pad == null ? "0" : pad); 40 | return n.length == 1 ? pad + n : n; 41 | }; 42 | 43 | var r = []; 44 | var escape = false; 45 | var hours = d.getHours(); 46 | var isAM = hours < 12; 47 | 48 | if (monthNames == null) { 49 | monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 50 | } 51 | 52 | if (dayNames == null) { 53 | dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 54 | } 55 | 56 | var hours12; 57 | 58 | if (hours > 12) { 59 | hours12 = hours - 12; 60 | } else if (hours == 0) { 61 | hours12 = 12; 62 | } else { 63 | hours12 = hours; 64 | } 65 | 66 | for (var i = 0; i < fmt.length; ++i) { 67 | 68 | var c = fmt.charAt(i); 69 | 70 | if (escape) { 71 | switch (c) { 72 | case 'a': c = "" + dayNames[d.getDay()]; break; 73 | case 'b': c = "" + monthNames[d.getMonth()]; break; 74 | case 'd': c = leftPad(d.getDate()); break; 75 | case 'e': c = leftPad(d.getDate(), " "); break; 76 | case 'h': // For back-compat with 0.7; remove in 1.0 77 | case 'H': c = leftPad(hours); break; 78 | case 'I': c = leftPad(hours12); break; 79 | case 'l': c = leftPad(hours12, " "); break; 80 | case 'm': c = leftPad(d.getMonth() + 1); break; 81 | case 'M': c = leftPad(d.getMinutes()); break; 82 | // quarters not in Open Group's strftime specification 83 | case 'q': 84 | c = "" + (Math.floor(d.getMonth() / 3) + 1); break; 85 | case 'S': c = leftPad(d.getSeconds()); break; 86 | case 'y': c = leftPad(d.getFullYear() % 100); break; 87 | case 'Y': c = "" + d.getFullYear(); break; 88 | case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; 89 | case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; 90 | case 'w': c = "" + d.getDay(); break; 91 | } 92 | r.push(c); 93 | escape = false; 94 | } else { 95 | if (c == "%") { 96 | escape = true; 97 | } else { 98 | r.push(c); 99 | } 100 | } 101 | } 102 | 103 | return r.join(""); 104 | } 105 | 106 | // To have a consistent view of time-based data independent of which time 107 | // zone the client happens to be in we need a date-like object independent 108 | // of time zones. This is done through a wrapper that only calls the UTC 109 | // versions of the accessor methods. 110 | 111 | function makeUtcWrapper(d) { 112 | 113 | function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { 114 | sourceObj[sourceMethod] = function() { 115 | return targetObj[targetMethod].apply(targetObj, arguments); 116 | }; 117 | }; 118 | 119 | var utc = { 120 | date: d 121 | }; 122 | 123 | // support strftime, if found 124 | 125 | if (d.strftime != undefined) { 126 | addProxyMethod(utc, "strftime", d, "strftime"); 127 | } 128 | 129 | addProxyMethod(utc, "getTime", d, "getTime"); 130 | addProxyMethod(utc, "setTime", d, "setTime"); 131 | 132 | var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; 133 | 134 | for (var p = 0; p < props.length; p++) { 135 | addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); 136 | addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); 137 | } 138 | 139 | return utc; 140 | }; 141 | 142 | // select time zone strategy. This returns a date-like object tied to the 143 | // desired timezone 144 | 145 | function dateGenerator(ts, opts) { 146 | if (opts.timezone == "browser") { 147 | return new Date(ts); 148 | } else if (!opts.timezone || opts.timezone == "utc") { 149 | return makeUtcWrapper(new Date(ts)); 150 | } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { 151 | var d = new timezoneJS.Date(); 152 | // timezone-js is fickle, so be sure to set the time zone before 153 | // setting the time. 154 | d.setTimezone(opts.timezone); 155 | d.setTime(ts); 156 | return d; 157 | } else { 158 | return makeUtcWrapper(new Date(ts)); 159 | } 160 | } 161 | 162 | // map of app. size of time units in milliseconds 163 | 164 | var timeUnitSize = { 165 | "second": 1000, 166 | "minute": 60 * 1000, 167 | "hour": 60 * 60 * 1000, 168 | "day": 24 * 60 * 60 * 1000, 169 | "month": 30 * 24 * 60 * 60 * 1000, 170 | "quarter": 3 * 30 * 24 * 60 * 60 * 1000, 171 | "year": 365.2425 * 24 * 60 * 60 * 1000 172 | }; 173 | 174 | // the allowed tick sizes, after 1 year we use 175 | // an integer algorithm 176 | 177 | var baseSpec = [ 178 | [1, "second"], [2, "second"], [5, "second"], [10, "second"], 179 | [30, "second"], 180 | [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], 181 | [30, "minute"], 182 | [1, "hour"], [2, "hour"], [4, "hour"], 183 | [8, "hour"], [12, "hour"], 184 | [1, "day"], [2, "day"], [3, "day"], 185 | [0.25, "month"], [0.5, "month"], [1, "month"], 186 | [2, "month"] 187 | ]; 188 | 189 | // we don't know which variant(s) we'll need yet, but generating both is 190 | // cheap 191 | 192 | var specMonths = baseSpec.concat([[3, "month"], [6, "month"], 193 | [1, "year"]]); 194 | var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], 195 | [1, "year"]]); 196 | 197 | function init(plot) { 198 | plot.hooks.processOptions.push(function (plot, options) { 199 | $.each(plot.getAxes(), function(axisName, axis) { 200 | 201 | var opts = axis.options; 202 | 203 | if (opts.mode == "time") { 204 | axis.tickGenerator = function(axis) { 205 | 206 | var ticks = []; 207 | var d = dateGenerator(axis.min, opts); 208 | var minSize = 0; 209 | 210 | // make quarter use a possibility if quarters are 211 | // mentioned in either of these options 212 | 213 | var spec = (opts.tickSize && opts.tickSize[1] === 214 | "quarter") || 215 | (opts.minTickSize && opts.minTickSize[1] === 216 | "quarter") ? specQuarters : specMonths; 217 | 218 | if (opts.minTickSize != null) { 219 | if (typeof opts.tickSize == "number") { 220 | minSize = opts.tickSize; 221 | } else { 222 | minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; 223 | } 224 | } 225 | 226 | for (var i = 0; i < spec.length - 1; ++i) { 227 | if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] 228 | + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 229 | && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { 230 | break; 231 | } 232 | } 233 | 234 | var size = spec[i][0]; 235 | var unit = spec[i][1]; 236 | 237 | // special-case the possibility of several years 238 | 239 | if (unit == "year") { 240 | 241 | // if given a minTickSize in years, just use it, 242 | // ensuring that it's an integer 243 | 244 | if (opts.minTickSize != null && opts.minTickSize[1] == "year") { 245 | size = Math.floor(opts.minTickSize[0]); 246 | } else { 247 | 248 | var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); 249 | var norm = (axis.delta / timeUnitSize.year) / magn; 250 | 251 | if (norm < 1.5) { 252 | size = 1; 253 | } else if (norm < 3) { 254 | size = 2; 255 | } else if (norm < 7.5) { 256 | size = 5; 257 | } else { 258 | size = 10; 259 | } 260 | 261 | size *= magn; 262 | } 263 | 264 | // minimum size for years is 1 265 | 266 | if (size < 1) { 267 | size = 1; 268 | } 269 | } 270 | 271 | axis.tickSize = opts.tickSize || [size, unit]; 272 | var tickSize = axis.tickSize[0]; 273 | unit = axis.tickSize[1]; 274 | 275 | var step = tickSize * timeUnitSize[unit]; 276 | 277 | if (unit == "second") { 278 | d.setSeconds(floorInBase(d.getSeconds(), tickSize)); 279 | } else if (unit == "minute") { 280 | d.setMinutes(floorInBase(d.getMinutes(), tickSize)); 281 | } else if (unit == "hour") { 282 | d.setHours(floorInBase(d.getHours(), tickSize)); 283 | } else if (unit == "month") { 284 | d.setMonth(floorInBase(d.getMonth(), tickSize)); 285 | } else if (unit == "quarter") { 286 | d.setMonth(3 * floorInBase(d.getMonth() / 3, 287 | tickSize)); 288 | } else if (unit == "year") { 289 | d.setFullYear(floorInBase(d.getFullYear(), tickSize)); 290 | } 291 | 292 | // reset smaller components 293 | 294 | d.setMilliseconds(0); 295 | 296 | if (step >= timeUnitSize.minute) { 297 | d.setSeconds(0); 298 | } 299 | if (step >= timeUnitSize.hour) { 300 | d.setMinutes(0); 301 | } 302 | if (step >= timeUnitSize.day) { 303 | d.setHours(0); 304 | } 305 | if (step >= timeUnitSize.day * 4) { 306 | d.setDate(1); 307 | } 308 | if (step >= timeUnitSize.month * 2) { 309 | d.setMonth(floorInBase(d.getMonth(), 3)); 310 | } 311 | if (step >= timeUnitSize.quarter * 2) { 312 | d.setMonth(floorInBase(d.getMonth(), 6)); 313 | } 314 | if (step >= timeUnitSize.year) { 315 | d.setMonth(0); 316 | } 317 | 318 | var carry = 0; 319 | var v = Number.NaN; 320 | var prev; 321 | 322 | do { 323 | 324 | prev = v; 325 | v = d.getTime(); 326 | ticks.push(v); 327 | 328 | if (unit == "month" || unit == "quarter") { 329 | if (tickSize < 1) { 330 | 331 | // a bit complicated - we'll divide the 332 | // month/quarter up but we need to take 333 | // care of fractions so we don't end up in 334 | // the middle of a day 335 | 336 | d.setDate(1); 337 | var start = d.getTime(); 338 | d.setMonth(d.getMonth() + 339 | (unit == "quarter" ? 3 : 1)); 340 | var end = d.getTime(); 341 | d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); 342 | carry = d.getHours(); 343 | d.setHours(0); 344 | } else { 345 | d.setMonth(d.getMonth() + 346 | tickSize * (unit == "quarter" ? 3 : 1)); 347 | } 348 | } else if (unit == "year") { 349 | d.setFullYear(d.getFullYear() + tickSize); 350 | } else { 351 | d.setTime(v + step); 352 | } 353 | } while (v < axis.max && v != prev); 354 | 355 | return ticks; 356 | }; 357 | 358 | axis.tickFormatter = function (v, axis) { 359 | 360 | var d = dateGenerator(v, axis.options); 361 | 362 | // first check global format 363 | 364 | if (opts.timeformat != null) { 365 | return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); 366 | } 367 | 368 | // possibly use quarters if quarters are mentioned in 369 | // any of these places 370 | 371 | var useQuarters = (axis.options.tickSize && 372 | axis.options.tickSize[1] == "quarter") || 373 | (axis.options.minTickSize && 374 | axis.options.minTickSize[1] == "quarter"); 375 | 376 | var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; 377 | var span = axis.max - axis.min; 378 | var suffix = (opts.twelveHourClock) ? " %p" : ""; 379 | var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; 380 | var fmt; 381 | 382 | if (t < timeUnitSize.minute) { 383 | fmt = hourCode + ":%M:%S" + suffix; 384 | } else if (t < timeUnitSize.day) { 385 | if (span < 2 * timeUnitSize.day) { 386 | fmt = hourCode + ":%M" + suffix; 387 | } else { 388 | fmt = "%b %d " + hourCode + ":%M" + suffix; 389 | } 390 | } else if (t < timeUnitSize.month) { 391 | fmt = "%b %d"; 392 | } else if ((useQuarters && t < timeUnitSize.quarter) || 393 | (!useQuarters && t < timeUnitSize.year)) { 394 | if (span < timeUnitSize.year) { 395 | fmt = "%b"; 396 | } else { 397 | fmt = "%b %Y"; 398 | } 399 | } else if (useQuarters && t < timeUnitSize.year) { 400 | if (span < timeUnitSize.year) { 401 | fmt = "Q%q"; 402 | } else { 403 | fmt = "Q%q %Y"; 404 | } 405 | } else { 406 | fmt = "%Y"; 407 | } 408 | 409 | var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); 410 | 411 | return rt; 412 | }; 413 | } 414 | }); 415 | }); 416 | } 417 | 418 | $.plot.plugins.push({ 419 | init: init, 420 | options: options, 421 | name: 'time', 422 | version: '1.0' 423 | }); 424 | 425 | // Time-axis support used to be in Flot core, which exposed the 426 | // formatDate function on the plot object. Various plugins depend 427 | // on the function, so we need to re-expose it here. 428 | 429 | $.plot.formatDate = formatDate; 430 | $.plot.dateGenerator = dateGenerator; 431 | 432 | })(jQuery); 433 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.tooltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery.flot.tooltip 3 | * 4 | * description: easy-to-use tooltips for Flot charts 5 | * version: 0.6.6 6 | * author: Krzysztof Urbas @krzysu [myviews.pl] 7 | * website: https://github.com/krzysu/flot.tooltip 8 | * 9 | * build on 2014-02-10 10 | * released under MIT License, 2012 11 | */ 12 | (function ($) { 13 | 14 | // plugin options, default values 15 | var defaultOptions = { 16 | tooltip: false, 17 | tooltipOpts: { 18 | content: "%s | X: %x | Y: %y", 19 | // allowed templates are: 20 | // %s -> series label, 21 | // %x -> X value, 22 | // %y -> Y value, 23 | // %x.2 -> precision of X value, 24 | // %p -> percent 25 | xDateFormat: null, 26 | yDateFormat: null, 27 | monthNames: null, 28 | dayNames: null, 29 | shifts: { 30 | x: 10, 31 | y: 20 32 | }, 33 | defaultTheme: true, 34 | 35 | // callbacks 36 | onHover: function(flotItem, $tooltipEl) {} 37 | } 38 | }; 39 | 40 | // object 41 | var FlotTooltip = function(plot) { 42 | 43 | // variables 44 | this.tipPosition = {x: 0, y: 0}; 45 | 46 | this.init(plot); 47 | }; 48 | 49 | // main plugin function 50 | FlotTooltip.prototype.init = function(plot) { 51 | 52 | var that = this; 53 | 54 | plot.hooks.bindEvents.push(function (plot, eventHolder) { 55 | 56 | // get plot options 57 | that.plotOptions = plot.getOptions(); 58 | 59 | // if not enabled return 60 | if (that.plotOptions.tooltip === false || typeof that.plotOptions.tooltip === 'undefined') return; 61 | 62 | // shortcut to access tooltip options 63 | that.tooltipOptions = that.plotOptions.tooltipOpts; 64 | 65 | // create tooltip DOM element 66 | var $tip = that.getDomElement(); 67 | 68 | // bind event 69 | $( plot.getPlaceholder() ).bind("plothover", plothover); 70 | 71 | $(eventHolder).bind('mousemove', mouseMove); 72 | }); 73 | 74 | plot.hooks.shutdown.push(function (plot, eventHolder){ 75 | $(plot.getPlaceholder()).unbind("plothover", plothover); 76 | $(eventHolder).unbind("mousemove", mouseMove); 77 | }); 78 | 79 | function mouseMove(e){ 80 | var pos = {}; 81 | pos.x = e.pageX; 82 | pos.y = e.pageY; 83 | that.updateTooltipPosition(pos); 84 | } 85 | 86 | function plothover(event, pos, item) { 87 | var $tip = that.getDomElement(); 88 | if (item) { 89 | var tipText; 90 | 91 | // convert tooltip content template to real tipText 92 | tipText = that.stringFormat(that.tooltipOptions.content, item); 93 | 94 | $tip.html( tipText ); 95 | that.updateTooltipPosition({ x: pos.pageX, y: pos.pageY }); 96 | $tip.css({ 97 | left: that.tipPosition.x + that.tooltipOptions.shifts.x, 98 | top: that.tipPosition.y + that.tooltipOptions.shifts.y 99 | }) 100 | .show(); 101 | 102 | // run callback 103 | if(typeof that.tooltipOptions.onHover === 'function') { 104 | that.tooltipOptions.onHover(item, $tip); 105 | } 106 | } 107 | else { 108 | $tip.hide().html(''); 109 | } 110 | } 111 | }; 112 | 113 | /** 114 | * get or create tooltip DOM element 115 | * @return jQuery object 116 | */ 117 | FlotTooltip.prototype.getDomElement = function() { 118 | var $tip; 119 | 120 | if( $('#flotTip').length > 0 ){ 121 | $tip = $('#flotTip'); 122 | } 123 | else { 124 | $tip = $('
').attr('id', 'flotTip'); 125 | $tip.appendTo('body').hide().css({position: 'absolute'}); 126 | 127 | if(this.tooltipOptions.defaultTheme) { 128 | $tip.css({ 129 | 'background': '#fff', 130 | 'z-index': '1040', 131 | 'padding': '0.4em 0.6em', 132 | 'border-radius': '0.5em', 133 | 'font-size': '1.2em', 134 | 'border': '1px solid #111', 135 | 'display': 'none', 136 | 'white-space': 'nowrap' 137 | }); 138 | } 139 | } 140 | 141 | return $tip; 142 | }; 143 | 144 | // as the name says 145 | FlotTooltip.prototype.updateTooltipPosition = function(pos) { 146 | var totalTipWidth = $("#flotTip").outerWidth() + this.tooltipOptions.shifts.x; 147 | var totalTipHeight = $("#flotTip").outerHeight() + this.tooltipOptions.shifts.y; 148 | if ((pos.x - $(window).scrollLeft()) > ($(window).innerWidth() - totalTipWidth)) { 149 | pos.x -= totalTipWidth; 150 | } 151 | if ((pos.y - $(window).scrollTop()) > ($(window).innerHeight() - totalTipHeight)) { 152 | pos.y -= totalTipHeight; 153 | } 154 | this.tipPosition.x = pos.x; 155 | this.tipPosition.y = pos.y; 156 | }; 157 | 158 | /** 159 | * core function, create tooltip content 160 | * @param {string} content - template with tooltip content 161 | * @param {object} item - Flot item 162 | * @return {string} real tooltip content for current item 163 | */ 164 | FlotTooltip.prototype.stringFormat = function(content, item) { 165 | 166 | var percentPattern = /%p\.{0,1}(\d{0,})/; 167 | var seriesPattern = /%s/; 168 | var xPattern = /%x\.{0,1}(\d{0,})/; 169 | var yPattern = /%y\.{0,1}(\d{0,})/; 170 | var xPatternWithoutPrecision = "%x"; 171 | var yPatternWithoutPrecision = "%y"; 172 | 173 | var x, y; 174 | 175 | // for threshold plugin we need to read data from different place 176 | if (typeof item.series.threshold !== "undefined") { 177 | x = item.datapoint[0]; 178 | y = item.datapoint[1]; 179 | } else { 180 | x = item.series.data[item.dataIndex][0]; 181 | y = item.series.data[item.dataIndex][1]; 182 | } 183 | 184 | // I think this is only in case of threshold plugin 185 | if (item.series.label === null && item.series.originSeries) { 186 | item.series.label = item.series.originSeries.label; 187 | } 188 | 189 | // if it is a function callback get the content string 190 | if( typeof(content) === 'function' ) { 191 | content = content(item.series.label, x, y, item); 192 | } 193 | 194 | // percent match for pie charts 195 | if( typeof (item.series.percent) !== 'undefined' ) { 196 | content = this.adjustValPrecision(percentPattern, content, item.series.percent); 197 | } 198 | 199 | // series match 200 | if( typeof(item.series.label) !== 'undefined' ) { 201 | content = content.replace(seriesPattern, item.series.label); 202 | } 203 | else { 204 | //remove %s if label is undefined 205 | content = content.replace(seriesPattern, ""); 206 | } 207 | 208 | // time mode axes with custom dateFormat 209 | if(this.isTimeMode('xaxis', item) && this.isXDateFormat(item)) { 210 | content = content.replace(xPattern, this.timestampToDate(x, this.tooltipOptions.xDateFormat)); 211 | } 212 | 213 | if(this.isTimeMode('yaxis', item) && this.isYDateFormat(item)) { 214 | content = content.replace(yPattern, this.timestampToDate(y, this.tooltipOptions.yDateFormat)); 215 | } 216 | 217 | // set precision if defined 218 | if(typeof x === 'number') { 219 | content = this.adjustValPrecision(xPattern, content, x); 220 | } 221 | if(typeof y === 'number') { 222 | content = this.adjustValPrecision(yPattern, content, y); 223 | } 224 | 225 | // change x from number to given label, if given 226 | if(typeof item.series.xaxis.ticks !== 'undefined') { 227 | if(item.series.xaxis.ticks.length > item.dataIndex && !this.isTimeMode('xaxis', item)) 228 | content = content.replace(xPattern, item.series.xaxis.ticks[item.dataIndex].label); 229 | } 230 | 231 | // change y from number to given label, if given 232 | if(typeof item.series.yaxis.ticks !== 'undefined') { 233 | for (var index in item.series.yaxis.ticks) { 234 | if (item.series.yaxis.ticks.hasOwnProperty(index)) { 235 | var value = (this.isCategoriesMode('yaxis', item)) ? item.series.yaxis.ticks[index].label : item.series.yaxis.ticks[index].v; 236 | if (value === y) { 237 | content = content.replace(yPattern, item.series.yaxis.ticks[index].label); 238 | } 239 | } 240 | } 241 | } 242 | 243 | // if no value customization, use tickFormatter by default 244 | if(typeof item.series.xaxis.tickFormatter !== 'undefined') { 245 | //escape dollar 246 | content = content.replace(xPatternWithoutPrecision, item.series.xaxis.tickFormatter(x, item.series.xaxis).replace(/\$/g, '$$')); 247 | } 248 | if(typeof item.series.yaxis.tickFormatter !== 'undefined') { 249 | //escape dollar 250 | content = content.replace(yPatternWithoutPrecision, item.series.yaxis.tickFormatter(y, item.series.yaxis).replace(/\$/g, '$$')); 251 | } 252 | 253 | return content; 254 | }; 255 | 256 | // helpers just for readability 257 | FlotTooltip.prototype.isTimeMode = function(axisName, item) { 258 | return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'time'); 259 | }; 260 | 261 | FlotTooltip.prototype.isXDateFormat = function(item) { 262 | return (typeof this.tooltipOptions.xDateFormat !== 'undefined' && this.tooltipOptions.xDateFormat !== null); 263 | }; 264 | 265 | FlotTooltip.prototype.isYDateFormat = function(item) { 266 | return (typeof this.tooltipOptions.yDateFormat !== 'undefined' && this.tooltipOptions.yDateFormat !== null); 267 | }; 268 | 269 | FlotTooltip.prototype.isCategoriesMode = function(axisName, item) { 270 | return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'categories'); 271 | }; 272 | 273 | // 274 | FlotTooltip.prototype.timestampToDate = function(tmst, dateFormat) { 275 | var theDate = new Date(tmst*1); 276 | return $.plot.formatDate(theDate, dateFormat, this.tooltipOptions.monthNames, this.tooltipOptions.dayNames); 277 | }; 278 | 279 | // 280 | FlotTooltip.prototype.adjustValPrecision = function(pattern, content, value) { 281 | 282 | var precision; 283 | var matchResult = content.match(pattern); 284 | if( matchResult !== null ) { 285 | if(RegExp.$1 !== '') { 286 | precision = RegExp.$1; 287 | value = value.toFixed(precision); 288 | 289 | // only replace content if precision exists, in other case use thickformater 290 | content = content.replace(pattern, value); 291 | } 292 | } 293 | return content; 294 | }; 295 | 296 | // 297 | var init = function(plot) { 298 | new FlotTooltip(plot); 299 | }; 300 | 301 | // define Flot plugin 302 | $.plot.plugins.push({ 303 | init: init, 304 | options: defaultOptions, 305 | name: 'tooltip', 306 | version: '0.6.6' 307 | }); 308 | 309 | })(jQuery); 310 | -------------------------------------------------------------------------------- /web/static/js/jquery.flot.tooltip.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery.flot.tooltip 3 | * 4 | * description: easy-to-use tooltips for Flot charts 5 | * version: 0.6.6 6 | * author: Krzysztof Urbas @krzysu [myviews.pl] 7 | * website: https://github.com/krzysu/flot.tooltip 8 | * 9 | * build on 2014-02-10 10 | * released under MIT License, 2012 11 | */ 12 | !function(a){var b={tooltip:!1,tooltipOpts:{content:"%s | X: %x | Y: %y",xDateFormat:null,yDateFormat:null,monthNames:null,dayNames:null,shifts:{x:10,y:20},defaultTheme:!0,onHover:function(){}}},c=function(a){this.tipPosition={x:0,y:0},this.init(a)};c.prototype.init=function(b){function c(a){var b={};b.x=a.pageX,b.y=a.pageY,e.updateTooltipPosition(b)}function d(a,b,c){var d=e.getDomElement();if(c){var f;f=e.stringFormat(e.tooltipOptions.content,c),d.html(f),e.updateTooltipPosition({x:b.pageX,y:b.pageY}),d.css({left:e.tipPosition.x+e.tooltipOptions.shifts.x,top:e.tipPosition.y+e.tooltipOptions.shifts.y}).show(),"function"==typeof e.tooltipOptions.onHover&&e.tooltipOptions.onHover(c,d)}else d.hide().html("")}var e=this;b.hooks.bindEvents.push(function(b,f){if(e.plotOptions=b.getOptions(),e.plotOptions.tooltip!==!1&&"undefined"!=typeof e.plotOptions.tooltip){e.tooltipOptions=e.plotOptions.tooltipOpts;{e.getDomElement()}a(b.getPlaceholder()).bind("plothover",d),a(f).bind("mousemove",c)}}),b.hooks.shutdown.push(function(b,e){a(b.getPlaceholder()).unbind("plothover",d),a(e).unbind("mousemove",c)})},c.prototype.getDomElement=function(){var b;return a("#flotTip").length>0?b=a("#flotTip"):(b=a("
").attr("id","flotTip"),b.appendTo("body").hide().css({position:"absolute"}),this.tooltipOptions.defaultTheme&&b.css({background:"#fff","z-index":"1040",padding:"0.4em 0.6em","border-radius":"0.5em","font-size":"0.8em",border:"1px solid #111",display:"none","white-space":"nowrap"})),b},c.prototype.updateTooltipPosition=function(b){var c=a("#flotTip").outerWidth()+this.tooltipOptions.shifts.x,d=a("#flotTip").outerHeight()+this.tooltipOptions.shifts.y;b.x-a(window).scrollLeft()>a(window).innerWidth()-c&&(b.x-=c),b.y-a(window).scrollTop()>a(window).innerHeight()-d&&(b.y-=d),this.tipPosition.x=b.x,this.tipPosition.y=b.y},c.prototype.stringFormat=function(a,b){var c,d,e=/%p\.{0,1}(\d{0,})/,f=/%s/,g=/%x\.{0,1}(\d{0,})/,h=/%y\.{0,1}(\d{0,})/,i="%x",j="%y";if("undefined"!=typeof b.series.threshold?(c=b.datapoint[0],d=b.datapoint[1]):(c=b.series.data[b.dataIndex][0],d=b.series.data[b.dataIndex][1]),null===b.series.label&&b.series.originSeries&&(b.series.label=b.series.originSeries.label),"function"==typeof a&&(a=a(b.series.label,c,d,b)),"undefined"!=typeof b.series.percent&&(a=this.adjustValPrecision(e,a,b.series.percent)),a="undefined"!=typeof b.series.label?a.replace(f,b.series.label):a.replace(f,""),this.isTimeMode("xaxis",b)&&this.isXDateFormat(b)&&(a=a.replace(g,this.timestampToDate(c,this.tooltipOptions.xDateFormat))),this.isTimeMode("yaxis",b)&&this.isYDateFormat(b)&&(a=a.replace(h,this.timestampToDate(d,this.tooltipOptions.yDateFormat))),"number"==typeof c&&(a=this.adjustValPrecision(g,a,c)),"number"==typeof d&&(a=this.adjustValPrecision(h,a,d)),"undefined"!=typeof b.series.xaxis.ticks&&b.series.xaxis.ticks.length>b.dataIndex&&!this.isTimeMode("xaxis",b)&&(a=a.replace(g,b.series.xaxis.ticks[b.dataIndex].label)),"undefined"!=typeof b.series.yaxis.ticks)for(var k in b.series.yaxis.ticks)if(b.series.yaxis.ticks.hasOwnProperty(k)){var l=this.isCategoriesMode("yaxis",b)?b.series.yaxis.ticks[k].label:b.series.yaxis.ticks[k].v;l===d&&(a=a.replace(h,b.series.yaxis.ticks[k].label))}return"undefined"!=typeof b.series.xaxis.tickFormatter&&(a=a.replace(i,b.series.xaxis.tickFormatter(c,b.series.xaxis).replace(/\$/g,"$$"))),"undefined"!=typeof b.series.yaxis.tickFormatter&&(a=a.replace(j,b.series.yaxis.tickFormatter(d,b.series.yaxis).replace(/\$/g,"$$"))),a},c.prototype.isTimeMode=function(a,b){return"undefined"!=typeof b.series[a].options.mode&&"time"===b.series[a].options.mode},c.prototype.isXDateFormat=function(){return"undefined"!=typeof this.tooltipOptions.xDateFormat&&null!==this.tooltipOptions.xDateFormat},c.prototype.isYDateFormat=function(){return"undefined"!=typeof this.tooltipOptions.yDateFormat&&null!==this.tooltipOptions.yDateFormat},c.prototype.isCategoriesMode=function(a,b){return"undefined"!=typeof b.series[a].options.mode&&"categories"===b.series[a].options.mode},c.prototype.timestampToDate=function(b,c){var d=new Date(1*b);return a.plot.formatDate(d,c,this.tooltipOptions.monthNames,this.tooltipOptions.dayNames)},c.prototype.adjustValPrecision=function(a,b,c){var d,e=b.match(a);return null!==e&&""!==RegExp.$1&&(d=RegExp.$1,c=c.toFixed(d),b=b.replace(a,c)),b};var d=function(a){new c(a)};a.plot.plugins.push({init:d,options:b,name:"tooltip",version:"0.6.6"})}(jQuery); -------------------------------------------------------------------------------- /web/static/js/jquery.flot.updater.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | function init(plot) { 3 | var datas = {}; 4 | var plot = plot; 5 | 6 | var updateInterval; 7 | var totalPoints; 8 | var pointsRemaining; 9 | 10 | var params = {} 11 | var ajax_args; 12 | 13 | var processingRequest = false; 14 | 15 | function noData(jqXHR, textStatus, errorThrown) { 16 | processingRequest = false; 17 | } 18 | 19 | function updateData(resp) { 20 | processingRequest = false; 21 | if (plot == null) 22 | plot = $.plot(doc, [ ], options); 23 | d = resp; 24 | 25 | for(key in d) { 26 | if (datas.hasOwnProperty(key)) { 27 | datas[key] = datas[key].concat(d[key]); 28 | } else { 29 | datas[key] = d[key]; 30 | } 31 | 32 | if (datas[key].length > totalPoints) { 33 | datas[key] = datas[key].slice(datas[key].length - totalPoints ); 34 | } 35 | } 36 | data = []; 37 | for (key in datas) { 38 | data.push({'label' : key, 39 | 'data' : datas[key]}); 40 | } 41 | 42 | plot.setData(data); 43 | plot.setupGrid(); 44 | plot.draw(); 45 | } 46 | 47 | function update(first) { 48 | setTimeout(update, updateInterval); 49 | 50 | pointsRemaining++; 51 | params.totalPoints = pointsRemaining; 52 | 53 | if (!processingRequest) { 54 | try { 55 | pointsRemaining = 0 56 | processingRequest = true; 57 | ajax_args.data = params; 58 | 59 | $.ajax(ajax_args); 60 | 61 | } catch (err) { 62 | processingRequest = false; 63 | } 64 | } 65 | } 66 | 67 | 68 | function processOptions(plot, options) { 69 | if (options.updater) { 70 | updater = options.updater; 71 | updateInterval = updater.updateInterval; 72 | totalPoints = updater.totalPoints; 73 | 74 | ajax_args = updater.ajax; 75 | ajax_args.success = updateData; 76 | ajax_args.error = noData; 77 | ajax_args.dataType = 'json'; 78 | if (ajax_args.data) { 79 | params = ajax_args.data; 80 | } 81 | 82 | 83 | pointsRemaining = totalPoints; 84 | params.updateInterval = updateInterval; 85 | 86 | update( 1); 87 | } 88 | } 89 | plot.hooks.processOptions.push(processOptions); 90 | } 91 | 92 | var options = { updater: 0 }; 93 | 94 | $.plot.plugins.push({ 95 | init: init, 96 | options: options, 97 | name: "updater", 98 | version: "0.1" 99 | }); 100 | })(jQuery); -------------------------------------------------------------------------------- /web/static/js/jquery.flot.valuelabels.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Value Labels Plugin for flot. 3 | * http://leonardoeloy.github.com/flot-valuelabels 4 | * http://wiki.github.com/leonardoeloy/flot-valuelabels/ 5 | * 6 | * Using canvas.fillText instead of divs, which is better for printing - by Leonardo Eloy, March 2010. 7 | * Tested with Flot 0.6 and JQuery 1.3.2. 8 | * 9 | * Original homepage: http://sites.google.com/site/petrsstuff/projects/flotvallab 10 | * Released under the MIT license by Petr Blahos, December 2009. 11 | */ 12 | (function ($) { 13 | var options = { 14 | series: { 15 | valueLabels: { 16 | show: false, 17 | showAsHtml: false, // Set to true if you wanna switch back to DIV usage (you need plot.css for this) 18 | showLastValue: false, // Use this to show the label only for the last value in the series 19 | labelFormatter: function(v) { return v; }, // Format the label value to what you want 20 | align: 'start', // can also be 'center', 'left' or 'right' 21 | plotAxis: 'y', // Set to the axis values you wish to plot 22 | hideZero: false 23 | } 24 | } 25 | }; 26 | 27 | function init(plot) { 28 | plot.hooks.draw.push( function (plot, ctx) { 29 | 30 | 31 | $.each(plot.getData(), function(ii, series) { 32 | if (!series.valueLabels.show) return; 33 | 34 | var showLastValue = series.valueLabels.showLastValue; 35 | var showAsHtml = series.valueLabels.showAsHtml; 36 | var plotAxis = series.valueLabels.plotAxis; 37 | var labelFormatter = series.valueLabels.labelFormatter; 38 | var fontcolor = series.valueLabels.fontcolor; 39 | var xoffset = series.valueLabels.xoffset; 40 | var yoffset = series.valueLabels.yoffset; 41 | var align = series.valueLabels.align; 42 | var font = series.valueLabels.font; 43 | var hideZero = series.valueLabels.hideZero; 44 | // Workaround, since Flot doesn't set this value anymore 45 | series.seriesIndex = ii; 46 | 47 | if (showAsHtml) { 48 | plot.getPlaceholder().find("#valueLabels"+ii).remove(); 49 | } 50 | 51 | var html = '
'; 52 | var last_val = null; 53 | var last_x = -1000; 54 | var last_y = -1000; 55 | var categories = series.xaxis.options.mode == 'categories'; 56 | 57 | for (var i = 0; i < series.data.length; ++i) { 58 | 59 | if (series.data[i] === null || (showLastValue && i != series.data.length-1)) continue; 60 | 61 | var x = series.data[i][0], y = series.data[i][1]; 62 | 63 | if (categories) { 64 | x = series.xaxis.categories[x]; 65 | } 66 | 67 | if (x < series.xaxis.min || x > series.xaxis.max || y < series.yaxis.min || y > series.yaxis.max) continue; 68 | 69 | var val = ( plotAxis === 'x' ) ? x : y; 70 | 71 | if(val == null){val=''} 72 | 73 | if ( val === 0 && hideZero ) continue; 74 | 75 | if (series.valueLabels.valueLabelFunc) { 76 | val = series.valueLabels.valueLabelFunc({ series: series, seriesIndex: ii, index: i }); 77 | } 78 | 79 | val = "" + val; 80 | val = labelFormatter(val); 81 | 82 | if (val != last_val || i == series.data.length - 1) { 83 | var xx = series.xaxis.p2c(x) + plot.getPlotOffset().left; 84 | var yy = series.yaxis.p2c(y) - 12 + plot.getPlotOffset().top; 85 | 86 | if ( Math.abs(yy - last_y) > 20 || last_x < xx) { 87 | last_val = val; 88 | last_x = xx + val.length * 8; 89 | last_y = yy; 90 | if (!showAsHtml) { 91 | // Little 5 px padding here helps the number to get 92 | // closer to points 93 | x_pos = xx; 94 | y_pos = yy + 6; 95 | 96 | // If the value is on the top of the canvas, we need 97 | // to push it down a little 98 | if (yy <= 0) y_pos = 18; 99 | 100 | // The same happens with the x axis 101 | if (xx >= plot.width()) { 102 | x_pos = plot.width(); 103 | } 104 | 105 | if (font) { 106 | ctx.font = font; 107 | } 108 | if(typeof(fontcolor) != 'undefined'){ 109 | ctx.fillStyle = fontcolor; 110 | } 111 | ctx.shadowOffsetX = 0; 112 | ctx.shadowOffsetY = 0; 113 | ctx.shadowBlur = 1.5; 114 | if(typeof(fontcolor) != 'undefined'){ 115 | ctx.shadowColor = fontcolor; 116 | } 117 | ctx.textAlign = align; 118 | 119 | ctx.fillText(val, x_pos, y_pos); 120 | } 121 | else { 122 | var head = '
' + val + '
'; 124 | html += head + "Light" + tail + head + tail; 125 | } 126 | } 127 | } 128 | } 129 | if (showAsHtml) { 130 | html += "
"; 131 | plot.getPlaceholder().append(html); 132 | } 133 | }); 134 | }); 135 | } 136 | 137 | $.plot.plugins.push({ 138 | init: init, 139 | options: options, 140 | name: 'valueLabels', 141 | version: '1.2' 142 | }); 143 | })(jQuery); 144 | 145 | -------------------------------------------------------------------------------- /web/static/js/jquery.peity.min.js: -------------------------------------------------------------------------------- 1 | // Peity jQuery plugin version 2.0.3 2 | // (c) 2014 Ben Pickles 3 | // 4 | // http://benpickles.github.io/peity 5 | // 6 | // Released under MIT license. 7 | (function(e,q,h){var o=function(a,b){var c=q.createElementNS("http://www.w3.org/2000/svg",a);e.each(b,function(a,b){c.setAttribute(a,b)});return c},t="createElementNS"in q&&o("svg",{}).createSVGRect,r=1/(window.devicePixelRatio||1),j=e.fn.peity=function(a,b){t&&this.each(function(){var c=e(this),d=c.data("peity");if(d)a&&(d.type=a),e.extend(d.opts,b);else{var f=j.defaults[a],g={};e.each(c.data(),function(a,b){a in f&&(g[a]=b)});var h=e.extend({},f,g,b),d=new s(c,a,h);c.change(function(){d.draw()}).data("peity", 8 | d)}d.draw()});return this},s=function(a,b,c){this.$el=a;this.type=b;this.opts=c},m=s.prototype;m.draw=function(){j.graphers[this.type].call(this,this.opts)};m.fill=function(){var a=this.opts.fill,b=a;e.isFunction(b)||(b=function(b,d){return a[d%a.length]});return b};m.prepare=function(a,b){var c;this.svg?c=e(this.svg).empty():(this.svg=o("svg",{"class":"peity"}),this.$el.hide().after(this.svg),c=e(this.svg).data("peity",this));this.svg.setAttribute("height",b);this.svg.setAttribute("width",a);return c}; 9 | m.values=function(){return e.map(this.$el.text().split(this.opts.delimiter),function(a){return parseFloat(a)})};j.defaults={};j.graphers={};j.register=function(a,b,c){this.defaults[a]=b;this.graphers[a]=c};j.register("pie",{delimiter:null,diameter:16,fill:["#ff9900","#fff4dd","#ffc66e"]},function(a){if(!a.delimiter){var b=this.$el.text().match(/[^0-9\.]/);a.delimiter=b?b[0]:","}b=this.values();if("/"==a.delimiter)var c=b[0],b=[c,h.max(0,b[1]-c)];for(var d=0,c=b.length,f=0;de?1:0,1,q,r,"Z"].join(" ")});i=l}k.setAttribute("fill",j.call(this,n,d,b));this.svg.appendChild(k)}}});j.register("line",{delimiter:",",fill:"#c6d9fd",height:16,max:null, 11 | min:0,stroke:"#4d89f9",strokeWidth:1,width:32},function(a){var b=this.values();1==b.length&&b.push(b[0]);for(var c=h.max.apply(h,b.concat([a.max])),d=h.min.apply(h,b.concat([a.min])),f=this.prepare(a.width,a.height),g=f.width(),f=f.height()-a.strokeWidth,e=g/(b.length-1),c=c-d,j=0==c?f:f/c,m=f+d*j,c=[0,m],i=0;i=d&&0k&&(l+=k,k=-k);n=o("rect",{fill:m.call(this,n,i,b),x:i*g,y:l,width:g-a,height:k});this.svg.appendChild(n)}})})(jQuery,document,Math); 14 | -------------------------------------------------------------------------------- /web/static/js/torquehelpers.js: -------------------------------------------------------------------------------- 1 | function onSubmitIt() { 2 | var fields = $("li.search-choice").serializeArray(); 3 | if (fields.length <= 1) 4 | { 5 | return false; 6 | } 7 | else 8 | { 9 | $('#formplotdata').submit(); 10 | } 11 | } 12 | 13 | $(document).ready(function(){ 14 | var previousPoint = null; 15 | $("#placeholder").bind("plothover", function (event, pos, item) { 16 | var a_p = ""; 17 | var d = new Date(parseInt(pos.x.toFixed(0))); 18 | var curr_hour = d.getHours(); 19 | if (curr_hour < 12) { 20 | a_p = "AM"; 21 | } 22 | else { 23 | a_p = "PM"; 24 | } 25 | if (curr_hour == 0) { 26 | curr_hour = 12; 27 | } 28 | if (curr_hour > 12) { 29 | curr_hour = curr_hour - 12; 30 | } 31 | var curr_min = d.getMinutes() + ""; 32 | if (curr_min.length == 1) { 33 | curr_min = "0" + curr_min; 34 | } 35 | var curr_sec = d.getSeconds() + ""; 36 | if (curr_sec.length == 1) { 37 | curr_sec = "0" + curr_sec; 38 | } 39 | var formattedTime = curr_hour + ":" + curr_min + ":" + curr_sec + " " + a_p; 40 | $(".x").text(formattedTime); 41 | $("#y1").text(pos.y1.toFixed(2)); 42 | $("#y2").text(pos.y2.toFixed(2)); 43 | 44 | if ($("#enableTooltip:checked").length > 0) { 45 | if (item) { 46 | if (previousPoint != item.dataIndex) { 47 | previousPoint = item.dataIndex; 48 | 49 | $("#tooltip").remove(); 50 | var x = item.datapoint[0].toFixed(2), 51 | y = item.datapoint[1].toFixed(2); 52 | 53 | showTooltip(item.pageX, item.pageY, 54 | item.series.label + " of " + x + " = " + y); 55 | } 56 | } 57 | else { 58 | $("#tooltip").remove(); 59 | previousPoint = null; 60 | } 61 | } 62 | }); 63 | }); 64 | 65 | $(document).ready(function(){ 66 | // Activate Chosen on the selection drop down 67 | $("select#seshidtag").chosen({width: "100%"}); 68 | $("select#plot_data").chosen({width: "100%"}); 69 | // Center the selected element 70 | $("div#seshidtag_chosen a.chosen-single span").attr('align', 'center'); 71 | // Limit number of multi selects to 2 72 | $("select#plot_data").chosen({max_selected_options: 2, no_results_text: "Oops, nothing found!"}); 73 | $("select#plot_data").chosen({placeholder_text_multiple: "Choose OBD2 data.."}); 74 | // When the selection drop down is open, force all elements to align left with padding 75 | $('select#seshidtag').on('chosen:showing_dropdown', function() { $('li.active-result').attr('align', 'left');}); 76 | $('select#seshidtag').on('chosen:showing_dropdown', function() { $('li.active-result').css('padding-left', '20px');}); 77 | $('select#plot_data').on('chosen:showing_dropdown', function() { $('li.active-result').attr('align', 'left');}); 78 | $('select#plot_data').on('chosen:showing_dropdown', function() { $('li.active-result').css('padding-left', '20px');}); 79 | }); 80 | 81 | $(document).on('click', '.panel-heading span.clickable', function(e){ 82 | var $this = $(this); 83 | if(!$this.hasClass('panel-collapsed')) { 84 | $this.parents('.panel').find('.panel-body').slideUp(); 85 | $this.addClass('panel-collapsed'); 86 | $this.find('i').removeClass('glyphicon-chevron-up').addClass('glyphicon-chevron-down'); 87 | } else { 88 | $this.parents('.panel').find('.panel-body').slideDown(); 89 | $this.removeClass('panel-collapsed'); 90 | $this.find('i').removeClass('glyphicon-chevron-down').addClass('glyphicon-chevron-up'); 91 | } 92 | }); 93 | 94 | $(document).ready(function(){ 95 | $(".line").peity("line") 96 | }); 97 | -------------------------------------------------------------------------------- /web/timezone.php: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /web/upload_data.php: -------------------------------------------------------------------------------- 1 | 0) { 12 | while ($row = mysql_fetch_assoc($result)) { 13 | $dbfields[]=($row['Field']); 14 | } 15 | } 16 | 17 | // Iterate over all the k* _GET arguments to check that a field exists 18 | if (sizeof($_GET) > 0) { 19 | $keys = array(); 20 | $values = array(); 21 | foreach ($_GET as $key => $value) { 22 | // Keep columns starting with k 23 | if (preg_match("/^k/", $key)) { 24 | $keys[] = $key; 25 | $values[] = $value; 26 | $submitval = 1; 27 | } 28 | else if (in_array($key, array("v", "eml", "time", "id", "session"))) { 29 | $keys[] = $key; 30 | $values[] = "'".$value."'"; 31 | $submitval = 1; 32 | } 33 | // Skip columns matching userUnit*, defaultUnit*, and profile* 34 | else if (preg_match("/^userUnit/", $key) or preg_match("/^defaultUnit/", $key) or (preg_match("/^profile/", $key) and (!preg_match("/^profileName/", $key)))) { 35 | $submitval = 0; 36 | } 37 | else { 38 | $submitval = 0; 39 | } 40 | // NOTE: Use the following "else" statement instead of the one above 41 | // if you want to keep anything else. 42 | //else { 43 | // $keys[] = $key; 44 | // $values[] = "'".$value."'"; 45 | // $submitval = 1; 46 | //} 47 | // If the field doesn't already exist, add it to the database 48 | if (!in_array($key, $dbfields) and $submitval == 1) { 49 | $sqlalter = "ALTER TABLE $db_table ADD $key VARCHAR(255) NOT NULL default '0'"; 50 | mysql_query($sqlalter, $con) or die(mysql_error()); 51 | } 52 | } 53 | if ((sizeof($keys) === sizeof($values)) && sizeof($keys) > 0) { 54 | // Now insert the data for all the fields 55 | $sql = "INSERT INTO $db_table (".implode(",", $keys).") VALUES (".implode(",", $values).")"; 56 | mysql_query($sql, $con) or die(mysql_error()); 57 | } 58 | } 59 | 60 | mysql_close($con); 61 | 62 | // Return the response required by Torque 63 | echo "OK!"; 64 | 65 | ?> 66 | -------------------------------------------------------------------------------- /web/url.php: -------------------------------------------------------------------------------- 1 | 43 | --------------------------------------------------------------------------------