├── k_nearest_neighbors ├── __init__.py ├── test_5669.npz ├── train_51022.npz ├── evaluate_model_51022.tar.gz ├── train_evaluate.py ├── preprocess.py ├── train_recommend.py ├── kfcv_d.py ├── test.py └── k_nearest_neighbors.py ├── logistic_regression ├── __init__.py ├── test_5669.npz ├── train_51022.npz ├── train.py ├── f1score.py ├── logistic_regression.py ├── preprocess.py ├── learning_curve.py └── model.pkl ├── docs └── final_report.pdf ├── static ├── lib │ ├── close.gif │ └── TextBoxList.js ├── css │ ├── cloud.css │ ├── global.css │ └── textboxlist.css ├── src │ ├── ScaleColor.js │ └── ProtoCloud.js ├── index.js └── data │ └── heroes.json ├── requirements.txt ├── .gitignore ├── LICENSE ├── data_collection ├── stats.py ├── README.md ├── util.py ├── dotabot2.py └── dotabot.py ├── app.py ├── engine.py ├── templates └── index.html ├── README.md └── heroes.json /k_nearest_neighbors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logistic_regression/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/final_report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevincon/dotaml/HEAD/docs/final_report.pdf -------------------------------------------------------------------------------- /static/lib/close.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevincon/dotaml/HEAD/static/lib/close.gif -------------------------------------------------------------------------------- /k_nearest_neighbors/test_5669.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevincon/dotaml/HEAD/k_nearest_neighbors/test_5669.npz -------------------------------------------------------------------------------- /logistic_regression/test_5669.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevincon/dotaml/HEAD/logistic_regression/test_5669.npz -------------------------------------------------------------------------------- /k_nearest_neighbors/train_51022.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevincon/dotaml/HEAD/k_nearest_neighbors/train_51022.npz -------------------------------------------------------------------------------- /logistic_regression/train_51022.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevincon/dotaml/HEAD/logistic_regression/train_51022.npz -------------------------------------------------------------------------------- /k_nearest_neighbors/evaluate_model_51022.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevincon/dotaml/HEAD/k_nearest_neighbors/evaluate_model_51022.tar.gz -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | dota2py==0.1.3 3 | numpy==1.10.1 4 | progressbar==2.3 5 | pymongo==2.6.3 6 | scikit-learn==0.14.1 7 | scipy==0.16.1 8 | -------------------------------------------------------------------------------- /static/css/cloud.css: -------------------------------------------------------------------------------- 1 | .ProtoCloud 2 | { 3 | margin-left: auto; 4 | margin-right: auto; 5 | color: #002A8B; 6 | } 7 | 8 | .ProtoCloud ul 9 | { 10 | text-align: justify; 11 | } 12 | 13 | .ProtoCloud a:link, .ProtoCloud a:visited 14 | { 15 | text-decoration: none; 16 | vertical-align: baseline; 17 | line-height: 1em; 18 | } 19 | 20 | .ProtoCloud a:hover 21 | { 22 | background-color: #0044EE; 23 | color: white !important; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib64 19 | __pycache__ 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | venv 37 | last_match 38 | date_max 39 | log.txt 40 | -------------------------------------------------------------------------------- /logistic_regression/train.py: -------------------------------------------------------------------------------- 1 | from sklearn.linear_model import LogisticRegression 2 | import pickle 3 | import numpy as np 4 | 5 | def train(X, Y, num_samples): 6 | print 'Training using data from %d matches...' % num_samples 7 | return LogisticRegression().fit(X[0:num_samples], Y[0:num_samples]) 8 | 9 | def main(): 10 | # Import the preprocessed training X matrix and Y vector 11 | preprocessed = np.load('train_51022.npz') 12 | X_train = preprocessed['X'] 13 | Y_train = preprocessed['Y'] 14 | 15 | model = train(X_train, Y_train, len(X_train)) 16 | 17 | with open('model.pkl', 'w') as output_file: 18 | pickle.dump(model, output_file) 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /static/css/global.css: -------------------------------------------------------------------------------- 1 | .PrimaryInfo, 2 | .SecondaryInfo { 3 | width: 100%; 4 | display: block; 5 | } 6 | 7 | .PrimaryInfo { 8 | font-weight: bold; 9 | font-size: 110%; 10 | } 11 | 12 | .SecondaryInfo { 13 | font-style: italic; 14 | text-indent: 15px; 15 | } 16 | 17 | 18 | .SubmitInfo, 19 | .SubmitCode { 20 | display: none; 21 | } 22 | 23 | html {margin-left: auto; 24 | margin-right: auto; 25 | width: 90% 26 | } 27 | 28 | .content#info:target{ 29 | display:block; 30 | } 31 | 32 | .content#suggest:target{ 33 | display:block; 34 | } 35 | 36 | .content#counters:target{ 37 | display:block; 38 | } 39 | 40 | 41 | #separator{ 42 | border:0; 43 | background-color:#CAD8F3; 44 | height:.2em; 45 | } 46 | 47 | 48 | #ctw { 49 | vertical-align:middle; 50 | font-size:2em; 51 | } 52 | 53 | #chero { 54 | font-size:2em; 55 | width:10em; 56 | } 57 | 58 | 59 | .infocolumn{ 60 | vertical-align:top; 61 | width:49% 62 | } 63 | 64 | .infocolumnseparator{ 65 | width:2%; 66 | } 67 | -------------------------------------------------------------------------------- /logistic_regression/f1score.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sklearn.metrics import precision_recall_fscore_support 3 | from logistic_regression import D2LogisticRegression 4 | 5 | POSITIVE_LABEL = 1 6 | NEGATIVE_LABEL = 0 7 | 8 | def make_prediction(algo, query): 9 | prob = algo.score(query) 10 | return POSITIVE_LABEL if prob > 0.5 else NEGATIVE_LABEL 11 | 12 | algo = D2LogisticRegression(model_root='.') 13 | 14 | testing_data = np.load('test_5669.npz') 15 | X = testing_data['X'] 16 | Y_true = testing_data['Y'] 17 | num_matches = len(Y_true) 18 | 19 | Y_pred = np.zeros(num_matches) 20 | for i, match in enumerate(X): 21 | Y_pred[i] = make_prediction(algo, match) 22 | 23 | prec, recall, f1, support = precision_recall_fscore_support(Y_true, Y_pred, average='macro') 24 | 25 | print 'Precision: ',prec 26 | print 'Recall: ',recall 27 | print 'F1 Score: ',f1 28 | print 'Support: ',support 29 | 30 | # Precision: 0.781616907078 31 | # Recall: 0.68468997943 32 | # F1 Score: 0.729949874687 33 | # Support: 3403 34 | -------------------------------------------------------------------------------- /k_nearest_neighbors/train_evaluate.py: -------------------------------------------------------------------------------- 1 | from sklearn.neighbors import KNeighborsClassifier 2 | import pickle 3 | import numpy as np 4 | 5 | # Import the preprocessed x matrix and Y vector 6 | preprocessed = np.load('train_51022.npz') 7 | X = preprocessed['X'] 8 | Y = preprocessed['Y'] 9 | 10 | relevant_indices = range(0, 10000) 11 | X = X[relevant_indices] 12 | Y = Y[relevant_indices] 13 | 14 | def my_distance(vec1,vec2): 15 | '''Returns a count of the elements that were 1 in both vec1 and vec2.''' 16 | #dummy return value to pass pyfuncdistance check 17 | return 0.0 18 | 19 | def poly_weights_evaluate(distances): 20 | '''Returns a list of weights for the provided list of distances.''' 21 | pass 22 | 23 | NUM_HEROES = 108 24 | NUM_MATCHES = len(X) 25 | 26 | print 'Training evaluation model using data from %d matches...' % NUM_MATCHES 27 | 28 | model = KNeighborsClassifier(n_neighbors=NUM_MATCHES,metric=my_distance,weights=poly_weights_evaluate).fit(X, Y) 29 | 30 | # Populate model pickle 31 | with open('evaluate_model_%d.pkl' % NUM_MATCHES, 'w') as output_file: 32 | pickle.dump(model, output_file) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Kevin Conley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /data_collection/stats.py: -------------------------------------------------------------------------------- 1 | import os, time 2 | from util import send_email 3 | from pymongo import MongoClient 4 | 5 | client = MongoClient(os.getenv('DOTABOT_DB_SERVER', 'localhost'), 27017) 6 | db = client[os.getenv('DOTABOT_DB_NAME', 'dotabot')] 7 | match_collection = db.matches 8 | 9 | def main(): 10 | '''The main entry point of stats.''' 11 | most_recent_match_id = 0 12 | for post in match_collection.find({}).sort('_id', direction=-1).limit(1): 13 | most_recent_match_id = post['match_id'] 14 | most_recent_match_time = post['start_time'] 15 | 16 | total_matches = match_collection.count() 17 | human_readable_time = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.localtime(most_recent_match_time)) 18 | 19 | disk_stats = os.statvfs('/') 20 | mb_remaining = disk_stats.f_bavail * disk_stats.f_frsize/1024.0/1024.0/1024.0 21 | 22 | msg = ''' 23 | Hello! 24 | 25 | The database currently contains %s matches. 26 | 27 | The most recent match_id added to the database was %s. 28 | 29 | The date of that match was %s. 30 | 31 | There are %.2f remaining GB on the hard drive. 32 | 33 | <3 dotabot 34 | ''' % (total_matches, most_recent_match_id, human_readable_time, mb_remaining) 35 | 36 | send_email(msg, subject='DOTAbot Update') 37 | 38 | if __name__ == '__main__': 39 | main() 40 | -------------------------------------------------------------------------------- /data_collection/README.md: -------------------------------------------------------------------------------- 1 | # dotabot 2 | 3 | dotabot is a Python script that collects data from the DOTA 2 web API and stores it in a local database. 4 | 5 | ### Setup 6 | 7 | Follow the instructions for the Dependencies and Installing Required Packages sections in the [main README](../README.md). 8 | 9 | ### Running dotabot.py 10 | 11 | dotabot.py was our first attempt at using the Steam Web API to collect data about public Dota 2 matches. It was written with the assumption that you can continuously request matches from the web API with no limits. It turns out you can only request the 500 most recent matches (using the GetMatchHistory API query). Therefore, we abandoned dotabot.py and ended up using dotabot2.py along with a cron job. 12 | 13 | Run dotabot with: 14 | 15 | python dotabot.py 16 | 17 | There are also some command line arguments you can specify, which you can view by running: 18 | 19 | python dotabot.py -h 20 | 21 | ### Running dotabot2.py 22 | 23 | dotabot2.py was the script we actually used with our project. We had it set up on a cron job to record the 500 most recent public Dota 2 matches to a Mongo DB database every 20 minutes. Run it using: 24 | 25 | python dotabot2.py 26 | 27 | ### Running stats.py 28 | 29 | stats.py is a simple script that we set up on a cron job to email us every 12 hours with the latest count of matches recorded in our MongoDB database. Run it using: 30 | 31 | python stats.py 32 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request 2 | from engine import Engine 3 | from k_nearest_neighbors.k_nearest_neighbors import D2KNearestNeighbors, my_distance, poly_weights_recommend, poly_weights_evaluate 4 | from logistic_regression.logistic_regression import D2LogisticRegression 5 | import json 6 | 7 | URL_PREFIX = '' 8 | 9 | app = Flask(__name__) 10 | engine = Engine(D2KNearestNeighbors()) 11 | #engine = Engine(D2LogisticRegression()) 12 | 13 | def get_api_string(recommendations, prob): 14 | recommendations = map(str, recommendations) 15 | return json.dumps({'x': recommendations, 'prob_x': prob}) 16 | 17 | @app.route("/") 18 | def index(): 19 | return render_template('index.html') 20 | 21 | @app.route(URL_PREFIX + "/api/suggest/") 22 | def api(): 23 | if 'x' not in request.args or 'y' not in request.args: 24 | return 'Invalid request' 25 | my_team = request.args['x'].split(',') 26 | if len(my_team) == 1 and my_team[0] == '': 27 | my_team = [] 28 | else: 29 | my_team = map(int, my_team) 30 | 31 | their_team = request.args['y'].split(',') 32 | if len(their_team) == 1 and their_team[0] == '': 33 | their_team = [] 34 | else: 35 | their_team = map(int, their_team) 36 | 37 | prob_recommendation_pairs = engine.recommend(my_team, their_team) 38 | recommendations = [hero for prob, hero in prob_recommendation_pairs] 39 | prob = engine.predict(my_team, their_team) 40 | return get_api_string(recommendations, prob) 41 | 42 | if __name__ == "__main__": 43 | app.debug = True 44 | app.run() 45 | -------------------------------------------------------------------------------- /data_collection/util.py: -------------------------------------------------------------------------------- 1 | import smtplib, os 2 | from email.mime.text import MIMEText 3 | from email.Utils import formatdate 4 | from datetime import datetime 5 | from dota2py import data 6 | 7 | def print_match_history(gmh_result): 8 | '''Print a summary of a list of matches.''' 9 | for match in gmh_result['matches']: 10 | match_id = match['match_id'] 11 | start_time = datetime.fromtimestamp(int(match['start_time'])) 12 | print 'Match %d - %s' % (match_id, start_time) 13 | 14 | def get_game_mode_string(game_mode_id): 15 | '''Return a human-readable string for a game_mode id.''' 16 | try: 17 | return data.GAME_MODES['dota_game_mode_%s' % game_mode_id] 18 | except KeyError: 19 | return 'Unknown mode %s' % game_mode_id 20 | 21 | def send_email(body, 22 | subject='Quick Message From DOTA2 Python Script', 23 | recipients=['kcon@stanford.edu', 'djperry@stanford.edu']): 24 | '''Send an email.''' 25 | # Credentials 26 | username = os.getenv('DOTABOT_USERNAME') 27 | hostname = os.getenv('DOTABOT_HOSTNAME') 28 | if not username or not hostname: 29 | raise NameError('Please set DOTABOT_USERNAME \ 30 | and DOTABOT_HOSTNAME environment variables.') 31 | 32 | # Message 33 | msg = MIMEText(body) 34 | msg['From'] = username 35 | msg['To'] = ','.join(recipients) 36 | msg['Subject'] = subject 37 | msg['Date'] = formatdate(localtime=True) 38 | 39 | # Send the email 40 | server = smtplib.SMTP(hostname) 41 | server.starttls() 42 | server.sendmail(username, recipients, msg.as_string()) 43 | server.quit() 44 | -------------------------------------------------------------------------------- /logistic_regression/logistic_regression.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pickle, os 3 | 4 | NUM_HEROES = 108 5 | NUM_FEATURES = NUM_HEROES * 2 6 | 7 | class D2LogisticRegression: 8 | def __init__(self, model_root='logistic_regression'): 9 | model_path = os.path.join(model_root, 'model.pkl') 10 | with open(model_path, 'r') as input_file: 11 | self.model = pickle.load(input_file) 12 | 13 | def transform(self, my_team, their_team): 14 | X = np.zeros(NUM_FEATURES, dtype=np.int8) 15 | for hero_id in my_team: 16 | X[hero_id - 1] = 1 17 | for hero_id in their_team: 18 | X[hero_id - 1 + NUM_HEROES] = 1 19 | return X 20 | 21 | def recommend(self, my_team, their_team, hero_candidates): 22 | '''Returns a list of (hero, probablility of winning with hero added) recommended to complete my_team.''' 23 | team_possibilities = [(candidate, my_team + [candidate]) for candidate in hero_candidates] 24 | 25 | prob_candidate_pairs = [] 26 | for candidate, team in team_possibilities: 27 | query = self.transform(team, their_team) 28 | prob = self.score(query) #self.model.predict_proba(query)[0][1] 29 | prob_candidate_pairs.append((prob, candidate)) 30 | prob_candidate_pairs = sorted(prob_candidate_pairs, reverse=True)[0:5 - len(my_team)] 31 | return prob_candidate_pairs 32 | 33 | def score(self, query): 34 | '''Score the query using the model, considering both radiant and dire teams.''' 35 | radiant_query = query 36 | dire_query = np.concatenate((radiant_query[NUM_HEROES:NUM_FEATURES], radiant_query[0:NUM_HEROES])) 37 | rad_prob = self.model.predict_proba(radiant_query)[0][1] 38 | dire_prob = self.model.predict_proba(dire_query)[0][0] 39 | return (rad_prob + dire_prob) / 2 40 | 41 | def predict(self, dream_team, their_team): 42 | '''Returns the probability of the dream_team winning against their_team.''' 43 | dream_team_query = self.transform(dream_team, their_team) 44 | return self.score(dream_team_query) 45 | #return self.model.predict_proba(dream_team_query)[0][1] 46 | -------------------------------------------------------------------------------- /engine.py: -------------------------------------------------------------------------------- 1 | from k_nearest_neighbors.k_nearest_neighbors import D2KNearestNeighbors, my_distance, poly_weights_recommend, poly_weights_evaluate 2 | from logistic_regression.logistic_regression import D2LogisticRegression 3 | import os, json 4 | 5 | with open('heroes.json', 'r') as fp: 6 | heroes = json.load(fp) 7 | hero_ids = [hero['id'] for hero in heroes] 8 | 9 | def get_hero_human_readable(hero_id): 10 | for hero in heroes: 11 | if hero['id'] == hero_id: 12 | return hero['localized_name'] 13 | return 'Unknown hero: %d' % hero_id 14 | 15 | def main(): 16 | # Fill these out using hero IDs (see web API) 17 | my_team = [76, 54] 18 | their_team = [5, 15, 46, 91, 13] 19 | 20 | print 'My Team: %s' % [get_hero_human_readable(hero_id) for hero_id in my_team] 21 | print 'Their Team: %s' % [get_hero_human_readable(hero_id) for hero_id in their_team] 22 | print 'Recommend:' 23 | #engine = Engine(D2KNearestNeighbors()) 24 | engine = Engine(D2LogisticRegression()) 25 | recommendations = engine.recommend(my_team, their_team) 26 | print [(prob, get_hero_human_readable(hero)) for prob, hero in recommendations] 27 | 28 | class Engine: 29 | def __init__(self, algorithm): 30 | self.algorithm = algorithm 31 | 32 | def get_candidates(self, my_team, their_team): 33 | '''Returns a list of hero IDs to consider for recommending.''' 34 | ids = [i for i in hero_ids if i not in my_team and i not in their_team and i not in [24, 104, 105, 108]] 35 | return ids 36 | 37 | def recommend(self, my_team, their_team, human_readable=False): 38 | '''Returns a list of (hero, probablility of winning with hero added) recommended to complete my_team.''' 39 | assert len(my_team) <= 5 40 | assert len(their_team) <= 5 41 | 42 | hero_candidates = self.get_candidates(my_team, their_team) 43 | return self.algorithm.recommend(my_team, their_team, hero_candidates) 44 | 45 | def predict(self, dream_team, their_team): 46 | '''Returns the probability of the dream_team winning against their_team.''' 47 | return self.algorithm.predict(dream_team, their_team) 48 | 49 | if __name__ == "__main__": 50 | main() 51 | 52 | -------------------------------------------------------------------------------- /k_nearest_neighbors/preprocess.py: -------------------------------------------------------------------------------- 1 | from pymongo import MongoClient 2 | from progressbar import ProgressBar, Bar, Percentage, FormatLabel, ETA 3 | import numpy as np 4 | 5 | np.set_printoptions(threshold=np.nan) 6 | 7 | client = MongoClient() 8 | db = client.dotabot 9 | matches = db.matches 10 | 11 | # We're going to create a training matrix, X, where each 12 | # row is a different match and each column is a feature 13 | 14 | # The features are bit vectors indicating whether heroes 15 | # were picked (1) or not picked (0). The first N features 16 | # correspond to radiant, and the last N features are 17 | # for dire. 18 | 19 | NUM_HEROES = 108 20 | NUM_FEATURES = NUM_HEROES * 2 21 | 22 | # Our training label vector, Y, is a bit vector indicating 23 | # whether radiant won (1) or lost (-1) 24 | NUM_MATCHES = matches.count() 25 | 26 | # Initialize training matrix 27 | X = np.zeros((NUM_MATCHES, NUM_FEATURES), dtype=np.int8) 28 | 29 | # Initialize training label vector 30 | Y = np.zeros(NUM_MATCHES, dtype=np.int8) 31 | 32 | widgets = [FormatLabel('Processed: %(value)d/%(max)d matches. '), ETA(), Percentage(), ' ', Bar()] 33 | pbar = ProgressBar(widgets=widgets, maxval=NUM_MATCHES).start() 34 | 35 | for i, record in enumerate(matches.find()): 36 | pbar.update(i) 37 | Y[i] = 1 if record['radiant_win'] else -1 38 | players = record['players'] 39 | for player in players: 40 | hero_id = player['hero_id'] - 1 41 | 42 | # If the left-most bit of player_slot is set, 43 | # this player is on dire, so push the index accordingly 44 | player_slot = player['player_slot'] 45 | if player_slot >= 128: 46 | hero_id += NUM_HEROES 47 | 48 | X[i, hero_id] = 1 49 | 50 | pbar.finish() 51 | 52 | print "Permuting, generating train and test sets." 53 | indices = np.random.permutation(NUM_MATCHES) 54 | test_indices = indices[0:NUM_MATCHES/10] 55 | train_indices = indices[NUM_MATCHES/10:NUM_MATCHES] 56 | 57 | X_test = X[test_indices] 58 | Y_test = Y[test_indices] 59 | 60 | X_train = X[train_indices] 61 | Y_train = Y[train_indices] 62 | 63 | print "Saving output file now..." 64 | np.savez_compressed('test_%d.npz' % len(test_indices), X=X_test, Y=Y_test) 65 | np.savez_compressed('train_%d.npz' % len(train_indices), X=X_train, Y=Y_train) 66 | 67 | -------------------------------------------------------------------------------- /logistic_regression/preprocess.py: -------------------------------------------------------------------------------- 1 | from pymongo import MongoClient 2 | from progressbar import ProgressBar, Bar, Percentage, FormatLabel, ETA 3 | import numpy as np 4 | 5 | np.set_printoptions(threshold=np.nan) 6 | 7 | client = MongoClient() 8 | db = client.dotabot 9 | matches = db.matches 10 | 11 | # We're going to create a training matrix, X, where each 12 | # row is a different match and each column is a feature 13 | 14 | # The features are bit vectors indicating whether heroes 15 | # were picked (1) or not picked (0). The first N features 16 | # correspond to radiant, and the last N features are 17 | # for dire. 18 | 19 | NUM_HEROES = 108 20 | NUM_FEATURES = NUM_HEROES * 2 21 | 22 | # Our training label vector, Y, is a bit vector indicating 23 | # whether radiant won (1) or lost (-1) 24 | NUM_MATCHES = matches.count() 25 | 26 | # Initialize training matrix 27 | X = np.zeros((NUM_MATCHES, NUM_FEATURES), dtype=np.int8) 28 | 29 | # Initialize training label vector 30 | Y = np.zeros(NUM_MATCHES, dtype=np.int8) 31 | 32 | widgets = [FormatLabel('Processed: %(value)d/%(max)d matches. '), ETA(), Percentage(), ' ', Bar()] 33 | pbar = ProgressBar(widgets=widgets, maxval=NUM_MATCHES).start() 34 | 35 | for i, record in enumerate(matches.find()): 36 | pbar.update(i) 37 | Y[i] = 1 if record['radiant_win'] else 0 38 | players = record['players'] 39 | for player in players: 40 | hero_id = player['hero_id'] - 1 41 | 42 | # If the left-most bit of player_slot is set, 43 | # this player is on dire, so push the index accordingly 44 | player_slot = player['player_slot'] 45 | if player_slot >= 128: 46 | hero_id += NUM_HEROES 47 | 48 | X[i, hero_id] = 1 49 | 50 | pbar.finish() 51 | 52 | print "Permuting, generating train and test sets." 53 | indices = np.random.permutation(NUM_MATCHES) 54 | test_indices = indices[0:NUM_MATCHES/10] 55 | train_indices = indices[NUM_MATCHES/10:NUM_MATCHES] 56 | 57 | X_test = X[test_indices] 58 | Y_test = Y[test_indices] 59 | 60 | X_train = X[train_indices] 61 | Y_train = Y[train_indices] 62 | 63 | print "Saving output file now..." 64 | np.savez_compressed('test_%d.npz' % len(test_indices), X=X_test, Y=Y_test) 65 | np.savez_compressed('train_%d.npz' % len(train_indices), X=X_train, Y=Y_train) 66 | 67 | -------------------------------------------------------------------------------- /k_nearest_neighbors/train_recommend.py: -------------------------------------------------------------------------------- 1 | from sklearn.neighbors import KNeighborsClassifier 2 | import pickle 3 | import numpy as np 4 | 5 | # Import the preprocessed x matrix and Y vector 6 | preprocessed = np.load('train_51022.npz') 7 | X = preprocessed['X'] 8 | Y = preprocessed['Y'] 9 | 10 | relevant_indices = range(0, 10000) 11 | X = X[relevant_indices] 12 | Y = Y[relevant_indices] 13 | 14 | def my_distance(vec1,vec2): 15 | '''Returns a count of the elements that were 1 in both vec1 and vec2.''' 16 | #dummy return value to pass pyfuncdistance check 17 | return 0.0 18 | 19 | def poly_weights_recommend(distances): 20 | '''Returns a list of weights for the provided list of distances.''' 21 | pass 22 | 23 | NUM_HEROES = 108 24 | NUM_MATCHES = len(X) 25 | 26 | print 'Training recommendation models using data from %d matches...' % NUM_MATCHES 27 | 28 | models = [] 29 | 30 | # Radiant Loop 31 | for hero_id in range(1, 109): 32 | if hero_id in [24,104,105,108]: 33 | models.append(None) 34 | continue 35 | X_filtered = [] 36 | Y_filtered = [] 37 | for i,row in enumerate(X): 38 | if row[hero_id-1] == 1: 39 | X_filtered.append(row) 40 | Y_filtered.append(Y[i]) 41 | X_filtered = np.array(X_filtered) 42 | Y_filtered = np.array(Y_filtered) 43 | try: 44 | models.append(KNeighborsClassifier(n_neighbors=len(X_filtered),metric=my_distance,weights=poly_weights_recommend).fit(X_filtered, Y_filtered)) 45 | except Exception,e: 46 | print "Radiant fit error!!! %s" % e 47 | 48 | # Dire Loop 49 | for hero_id in range(1, 109): 50 | if hero_id in [24,104,105,108]: 51 | models.append(None) 52 | continue 53 | X_filtered = [] 54 | Y_filtered = [] 55 | for i,row in enumerate(X): 56 | if row[hero_id-1+NUM_HEROES] == 1: 57 | X_filtered.append(row) 58 | Y_filtered.append(Y[i]) 59 | X_filtered = np.array(X_filtered) 60 | Y_filtered = np.array(Y_filtered) 61 | try: 62 | models.append(KNeighborsClassifier(n_neighbors=len(X_filtered),metric=my_distance,weights=poly_weights_recommend).fit(X_filtered, Y_filtered)) 63 | except Exception,e: 64 | print "Dire fit error!!! %s" % e 65 | 66 | # Populate model pickle 67 | with open('recommend_models_%d.pkl' % NUM_MATCHES, 'w') as output_file: 68 | pickle.dump(models, output_file) 69 | -------------------------------------------------------------------------------- /k_nearest_neighbors/kfcv_d.py: -------------------------------------------------------------------------------- 1 | from sklearn.neighbors import KNeighborsClassifier 2 | from sklearn import cross_validation 3 | import numpy as np 4 | from progressbar import ProgressBar, Bar, Percentage, FormatLabel, ETA 5 | 6 | def my_distance(vec1,vec2): 7 | #return np.sum(np.multiply(vec1,vec2)) 8 | return np.sum(np.logical_and(vec1,vec2)) 9 | 10 | def poly_param(d): 11 | def poly_weights(distances): 12 | '''Returns a list of weights given a polynomial weighting function''' 13 | weights = np.power(np.multiply(distances[0], 0.1), d) 14 | return np.array([weights]) 15 | return poly_weights 16 | 17 | def score(estimator, X, y): 18 | global pbar, FOLDS_FINISHED 19 | correct_predictions = 0 20 | for i, radiant_query in enumerate(X): 21 | pbar.update(FOLDS_FINISHED) 22 | dire_query = np.concatenate((radiant_query[NUM_HEROES:NUM_FEATURES], radiant_query[0:NUM_HEROES])) 23 | rad_prob = estimator.predict_proba(radiant_query)[0][1] 24 | dire_prob = estimator.predict_proba(dire_query)[0][0] 25 | overall_prob = (rad_prob + dire_prob) / 2 26 | prediction = 1 if (overall_prob > 0.5) else -1 27 | result = 1 if prediction == y[i] else 0 28 | correct_predictions += result 29 | FOLDS_FINISHED += 1 30 | accuracy = float(correct_predictions) / len(X) 31 | print 'Accuracy: %f' % accuracy 32 | return accuracy 33 | 34 | NUM_HEROES = 108 35 | NUM_FEATURES = NUM_HEROES*2 36 | K = 2 37 | FOLDS_FINISHED = 0 38 | 39 | # Import the preprocessed X matrix and Y vector 40 | preprocessed = np.load('train_51022.npz') 41 | X = preprocessed['X'] 42 | Y = preprocessed['Y'] 43 | 44 | NUM_MATCHES = 20000 45 | X = X[0:NUM_MATCHES] 46 | Y = Y[0:NUM_MATCHES] 47 | 48 | print 'Training using data from %d matches...' % NUM_MATCHES 49 | 50 | k_fold = cross_validation.KFold(n=NUM_MATCHES, n_folds=K, indices=True) 51 | 52 | d_tries = [3, 4, 5] 53 | 54 | widgets = [FormatLabel('Processed: %(value)d/%(max)d folds. '), ETA(), Percentage(), ' ', Bar()] 55 | pbar = ProgressBar(widgets=widgets, maxval=(len(d_tries) * K)).start() 56 | 57 | d_accuracy_pairs = [] 58 | for d_index, d in enumerate(d_tries): 59 | model = KNeighborsClassifier(n_neighbors=NUM_MATCHES/K,metric=my_distance,weights=poly_param(d)) 60 | model_accuracies = cross_validation.cross_val_score(model, X, Y, scoring=score, cv=k_fold) 61 | model_accuracy = model_accuracies.mean() 62 | d_accuracy_pairs.append((d, model_accuracy)) 63 | pbar.finish() 64 | print d_accuracy_pairs 65 | -------------------------------------------------------------------------------- /k_nearest_neighbors/test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pickle 3 | from progressbar import ProgressBar, Bar, Percentage, FormatLabel, ETA 4 | from sklearn.metrics import precision_recall_fscore_support 5 | 6 | NUM_HEROES = 108 7 | NUM_FEATURES = NUM_HEROES*2 8 | 9 | # Import the test x matrix and Y vector 10 | preprocessed = np.load('test_5669.npz') 11 | X = preprocessed['X'] 12 | Y = preprocessed['Y'] 13 | 14 | NUM_MATCHES = len(X) 15 | 16 | def my_distance(vec1,vec2): 17 | return np.sum(np.logical_and(vec1,vec2)) 18 | 19 | def poly_weights_evaluate(distances): 20 | '''Returns a list of weights given a polynomial weighting function''' 21 | # distances = distances[0] 22 | # weights = (distances * 0.1) 23 | # weights = weights ** 15 24 | weights = np.power(np.multiply(distances[0], 0.1), 4) 25 | return np.array([weights]) 26 | 27 | def test(): 28 | with open('evaluate_model_51022.pkl', 'r') as input_file: 29 | model = pickle.load(input_file) 30 | 31 | widgets = [FormatLabel('Processed: %(value)d/%(max)d matches. '), ETA(), Percentage(), ' ', Bar()] 32 | pbar = ProgressBar(widgets=widgets, maxval=NUM_MATCHES).start() 33 | 34 | correct_predictions = 0 35 | Y_pred = np.zeros(NUM_MATCHES) 36 | for i, radiant_query in enumerate(X): 37 | pbar.update(i) 38 | dire_query = np.concatenate((radiant_query[NUM_HEROES:NUM_FEATURES], radiant_query[0:NUM_HEROES])) 39 | rad_prob = model.predict_proba(radiant_query)[0][1] 40 | dire_prob = model.predict_proba(dire_query)[0][0] 41 | overall_prob = (rad_prob + dire_prob) / 2 42 | prediction = 1 if (overall_prob > 0.5) else -1 43 | Y_pred[i] = 1 if prediction == 1 else 0 44 | result = 1 if prediction == Y[i] else 0 45 | #print 'Predicted: ',prediction 46 | #print 'Result: ',result 47 | correct_predictions += result 48 | 49 | pbar.finish() 50 | 51 | accuracy = float(correct_predictions) / NUM_MATCHES 52 | print 'Accuracy of KNN model: %f' % accuracy 53 | 54 | # flip all -1 true labels to 0 for f1 scoring 55 | for i, match in enumerate(Y): 56 | if match == -1: 57 | Y[i] = 0 58 | 59 | prec, recall, f1, support = precision_recall_fscore_support(Y, Y_pred, average='macro') 60 | print 'Precision: ',prec 61 | print 'Recall: ',recall 62 | print 'F1 Score: ',f1 63 | print 'Support: ',support 64 | 65 | # Accuracy of KNN model: 0.678074 66 | # Precision: 0.764119601329 67 | # Recall: 0.673499267936 68 | # F1 Score: 0.715953307393 69 | # Support: 3415 70 | 71 | if __name__ == '__main__': 72 | test() 73 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dota2 Recommendation Engine 6 | 7 | 8 | 9 | 11 | 13 | 15 | 17 | 19 | 21 | 23 | 24 | 25 |

DOTA2 Recommendation Engine

26 |

By Kevin Conley and Daniel Perry

27 |

Note: queries have about a 5 second delay between responses.

28 |
29 |
30 |

Our team's heroes / Recommended heroes:

31 | 32 | 33 | 38 | 39 | 44 |
34 | 35 | 36 | 37 | / 40 | 41 | 42 | 43 |
45 |

Opposing team's heroes:

46 | 47 | 48 | 53 | 54 |
49 | 50 | 51 | 52 |
55 | 56 | 72 | 73 |

Chance we win based on picks:   __%

74 |
75 |
76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /logistic_regression/learning_curve.py: -------------------------------------------------------------------------------- 1 | from train import train 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import pylab 5 | 6 | NUM_HEROES = 108 7 | NUM_FEATURES = NUM_HEROES * 2 8 | 9 | def score(model, radiant_query): 10 | '''Return the probability of the query being in the positive class.''' 11 | dire_query = np.concatenate((radiant_query[NUM_HEROES:NUM_FEATURES], radiant_query[0:NUM_HEROES])) 12 | rad_prob = model.predict_proba(radiant_query)[0][1] 13 | dire_prob = model.predict_proba(dire_query)[0][0] 14 | return (rad_prob + dire_prob) / 2 15 | 16 | def evaluate(model, X, Y, positive_class, negative_class): 17 | '''Return the accuracy of running the model on the given set.''' 18 | correct_predictions = 0.0 19 | for i, radiant_query in enumerate(X): 20 | overall_prob = score(model, radiant_query) 21 | prediction = positive_class if (overall_prob > 0.5) else negative_class 22 | result = 1 if prediction == Y[i] else 0 23 | correct_predictions += result 24 | 25 | return correct_predictions / len(X) 26 | 27 | def plot_learning_curve(num_points, X_train, Y_train, X_test, Y_test, positive_class=1, negative_class=0): 28 | total_num_matches = len(X_train) 29 | training_set_sizes = [] 30 | for div in list(reversed(range(1, num_points + 1))): 31 | training_set_sizes.append(total_num_matches / div) 32 | 33 | accuracies = [] 34 | for training_set_size in training_set_sizes: 35 | model = train(X_train, Y_train, training_set_size) 36 | accuracy = evaluate(model, X_test, Y_test, positive_class, negative_class) 37 | accuracies.append(accuracy) 38 | print 'Accuracy for %d training examples: %f' % (training_set_size, accuracy) 39 | 40 | plt.plot(np.array(training_set_sizes), np.array(accuracies)) 41 | plt.ylabel('Accuracy') 42 | plt.xlabel('Number of training samples') 43 | plt.title('Logistic Regression Learning Curve') 44 | pylab.show() 45 | 46 | def plot_learning_curves(num_points, X_train, Y_train, X_test, Y_test, positive_class=1, negative_class=0): 47 | total_num_matches = len(X_train) 48 | training_set_sizes = [] 49 | for div in list(reversed(range(1, num_points + 1))): 50 | training_set_sizes.append(total_num_matches / div) 51 | 52 | test_errors = [] 53 | training_errors = [] 54 | for training_set_size in training_set_sizes: 55 | model = train(X_train, Y_train, training_set_size) 56 | test_error = evaluate(model, X_test, Y_test, positive_class, negative_class) 57 | training_error = evaluate(model, X_train, Y_train, positive_class, negative_class) 58 | test_errors.append(test_error) 59 | training_errors.append(training_error) 60 | 61 | plt.plot(training_set_sizes, training_errors, 'bs-', label='Training accuracy') 62 | plt.plot(training_set_sizes, test_errors, 'g^-', label='Test accuracy') 63 | plt.ylabel('Accuracy') 64 | plt.xlabel('Number of training samples') 65 | plt.title('Logistic Regression Learning Curve') 66 | plt.legend(loc='lower right') 67 | pylab.show() 68 | 69 | def main(): 70 | training_data = np.load('train_51022.npz') 71 | X_train = training_data['X'] 72 | Y_train = training_data['Y'] 73 | 74 | testing_data = np.load('test_5669.npz') 75 | X_test = testing_data['X'] 76 | Y_test = testing_data['Y'] 77 | 78 | #plot_learning_curve(30, X_train, Y_train, X_test, Y_test) 79 | plot_learning_curves(100, X_train, Y_train, X_test, Y_test) 80 | 81 | if __name__ == '__main__': 82 | main() 83 | -------------------------------------------------------------------------------- /k_nearest_neighbors/k_nearest_neighbors.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pickle, os 3 | 4 | NUM_HEROES = 108 5 | NUM_FEATURES = NUM_HEROES * 2 6 | NUM_IN_QUERY = 0 7 | # Lower this value to speed up recommendation engine 8 | TRAINING_SET_SIZE = 10000 9 | 10 | def my_distance(vec1,vec2): 11 | '''Returns a count of the elements that were 1 in both vec1 and vec2.''' 12 | return np.sum(np.logical_and(vec1,vec2)) 13 | 14 | def poly_weights_recommend(distances): 15 | '''Returns a list of weights for the provided list of distances.''' 16 | global NUM_IN_QUERY 17 | distances[0] = np.power(np.multiply(distances[0], NUM_IN_QUERY), 4) 18 | return distances 19 | 20 | def poly_weights_evaluate(distances): 21 | '''Returns a list of weights for the provided list of distances.''' 22 | distances[0] = np.power(np.multiply(distances[0], NUM_IN_QUERY), 4) 23 | return distances 24 | # weights = np.power(np.multiply(distances[0], 0.1), 15) 25 | # return np.array([weights]) 26 | 27 | class D2KNearestNeighbors: 28 | def __init__(self, model_root='k_nearest_neighbors'): 29 | recommend_path = os.path.join(model_root, 'recommend_models_%d.pkl' % TRAINING_SET_SIZE) 30 | evaluate_path = os.path.join(model_root, 'evaluate_model_%d.pkl' % TRAINING_SET_SIZE) 31 | 32 | with open(recommend_path, 'r') as input_file: 33 | self.recommend_models = pickle.load(input_file) 34 | with open(evaluate_path, 'r') as input_file: 35 | self.evaluate_model = pickle.load(input_file) 36 | 37 | def transform(self, my_team, their_team): 38 | X = np.zeros(NUM_FEATURES, dtype=np.int8) 39 | for hero_id in my_team: 40 | X[hero_id - 1] = 1 41 | for hero_id in their_team: 42 | X[hero_id - 1 + NUM_HEROES] = 1 43 | return X 44 | 45 | def recommend(self, my_team, their_team, hero_candidates): 46 | '''Returns a list of (hero, probablility of winning with hero added) recommended to complete my_team.''' 47 | global NUM_IN_QUERY 48 | NUM_IN_QUERY = len(my_team) + len(their_team) + 1 49 | team_possibilities = [(candidate, my_team + [candidate]) for candidate in hero_candidates] 50 | 51 | prob_candidate_pairs = [] 52 | for candidate, team in team_possibilities: 53 | query_radiant = self.transform(team, their_team) 54 | query_dire = self.transform(their_team, team) 55 | prob_radiant = self.recommend_models[candidate-1].predict_proba(query_radiant)[0][1] 56 | prob_dire = self.recommend_models[candidate-1+NUM_HEROES].predict_proba(query_dire)[0][0] 57 | prob = (prob_radiant + prob_dire) / 2 58 | prob_candidate_pairs.append((prob, candidate)) 59 | prob_candidate_pairs = sorted(prob_candidate_pairs, reverse=True)[0:5 - len(my_team)] 60 | return prob_candidate_pairs 61 | 62 | def score(self, query): 63 | '''Score the query using the evaluation model, considering both radiant and dire teams.''' 64 | radiant_query = query 65 | dire_query = np.concatenate((radiant_query[NUM_HEROES:NUM_FEATURES], radiant_query[0:NUM_HEROES])) 66 | rad_prob = self.evaluate_model.predict_proba(radiant_query)[0][1] 67 | dire_prob = self.evaluate_model.predict_proba(dire_query)[0][0] 68 | return (rad_prob + dire_prob) / 2 69 | 70 | def predict(self, dream_team, their_team): 71 | '''Returns the probability of the dream_team winning against their_team.''' 72 | dream_team_query = self.transform(dream_team, their_team) 73 | return self.score(dream_team_query) 74 | -------------------------------------------------------------------------------- /data_collection/dotabot2.py: -------------------------------------------------------------------------------- 1 | import os, logging, argparse 2 | from dota2py import api 3 | from util import get_game_mode_string 4 | from pymongo import MongoClient 5 | from time import sleep 6 | from sys import exit 7 | 8 | client = MongoClient(os.getenv('DOTABOT_DB_SERVER', 'localhost'), 27017) 9 | db = client[os.getenv('DOTABOT_DB_NAME', 'dotabot')] 10 | match_collection = db.matches 11 | 12 | logging.basicConfig(filename='/home/kcon/dota2project/log.txt') 13 | logger = logging.getLogger('dotabot') 14 | 15 | def setup(): 16 | '''Setup the API, etc.''' 17 | logger.setLevel(logging.DEBUG) 18 | 19 | API_KEY = os.getenv('DOTABOT_API_KEY') 20 | if not API_KEY: 21 | raise NameError('Please set the DOTABOT_API_KEY environment variables.') 22 | api.set_api_key(API_KEY) 23 | 24 | def is_valid_match(gmd_result): 25 | '''Returns True if the given match details result should be considered, 26 | and False otherwise.''' 27 | for player in gmd_result['players']: 28 | if player['leaver_status'] is not 0: 29 | return False 30 | return True 31 | 32 | def process_replay(match_id): 33 | '''Download, parse, and record data from the replay of the given match_id.''' 34 | # TODO 35 | pass 36 | 37 | def process_match_details(match_id): 38 | '''Get the details of the given match_id, check if it's valid, and 39 | if it is, add it as a record in the database and spawn a thread to 40 | download and parse the corresponding replay.''' 41 | gmd = api.get_match_details(match_id)['result'] 42 | 43 | if not is_valid_match(gmd): 44 | logger.debug('Not considering match %s.' % match_id) 45 | return 46 | 47 | match_collection.insert(gmd) 48 | 49 | game_mode = get_game_mode_string(gmd['game_mode']) 50 | logger.debug('Processed Match ID: %s - Game Mode: %s' % (match_id, game_mode)) 51 | 52 | # TODO: 53 | # Spawn replay parser thread if there aren't too many already 54 | 55 | def main(): 56 | '''The main entry point of dotabot.''' 57 | start_match_id = None 58 | while True: 59 | # Note: GetMatchHistory returns a list of matches in descending order, 60 | # going back in time. 61 | sleep(1.0) 62 | logger.debug('Doing GMH query for start_at_match_id=%s' % start_match_id) 63 | gmh = api.get_match_history(start_at_match_id=start_match_id, 64 | skill=3, 65 | game_mode=2, 66 | min_players=10)['result'] 67 | error_code = gmh['status'] 68 | matches = gmh['matches'] 69 | 70 | if error_code is not 1: 71 | msg = 'GMH query at match_id %s had error code %s. Retrying.' % (start_match_id, error_code) 72 | logger.debug(msg) 73 | continue 74 | 75 | if len(matches) is 0: 76 | logger.debug('Finished processing all 500 most recent matches.') 77 | exit(0) 78 | 79 | for match in matches: 80 | match_id = match['match_id'] 81 | 82 | if match_collection.find_one({'match_id':match_id}) != None: 83 | logger.debug('Encountered match %s already in database, exiting.' % match_id) 84 | exit(0) 85 | 86 | sleep(1.0) 87 | process_match_details(match_id) 88 | 89 | last_match_id = matches[-1]['match_id'] 90 | logger.debug('Match_id of last match of GMH query: %s' % last_match_id) 91 | # We don't want to record the last match twice, so subtract 1 92 | start_match_id = last_match_id - 1 93 | 94 | if __name__ == '__main__': 95 | p = argparse.ArgumentParser(description='Bot for collecting data from 500 most recent DOTA2 matches') 96 | args = p.parse_args() 97 | 98 | setup() 99 | main() 100 | -------------------------------------------------------------------------------- /static/src/ScaleColor.js: -------------------------------------------------------------------------------- 1 | /* 2 | inspired/modified/ported from code created by Joseph Myers | http://www.codelib.net/ 3 | 4 | Copyright (c) 2010 Timothy Fluehr tim@tfluehr.com 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | If you do choose to use this, 28 | please drop me an email at tim@tfluehr.com 29 | I would like to see where this ends up :) 30 | */ 31 | 32 | Object.extend(String.prototype, (function(){ 33 | function colorScale(scalefactor){ 34 | var hexstr = String(this).toLowerCase(); 35 | var r = scalefactor; 36 | var a, i; 37 | if (!(hexstr.length === 4 || hexstr.length === 7) || !hexstr.startsWith('#')) { 38 | throw "'" + hexstr + "' is not in the proper format. Color must be in the form #abc or #aabbcc"; 39 | } 40 | else if (hexstr.match(/[^#0-9a-f]/)) { 41 | throw "'" + hexstr + "' contains an invalid color value. Color must be in the form #abc or #aabbcc"; 42 | } 43 | else if (typeof(r) === 'undefined' || r < 0){ 44 | throw "'" + scalefactor + "' is invalid. The Scale Factor must be a number greater than 0. > 1 Will lighten the color. Between 0 and 1 will darken the color."; 45 | } 46 | else if (r === 1){ 47 | return hexstr; 48 | } 49 | 50 | // start color parsing/setup 51 | hexstr = hexstr.sub("#", '').replace(/[^0-9a-f]+/ig, ''); 52 | if (hexstr.length === 3) { 53 | a = hexstr.split(''); 54 | } 55 | else if (hexstr.length === 6) { 56 | a = hexstr.match(/(\w{2})/g); 57 | } 58 | for (i = 0; i < a.length; i++) { 59 | if (a[i].length == 2) { 60 | a[i] = parseInt(a[i], 16); 61 | } 62 | else { 63 | a[i] = parseInt(a[i], 16); 64 | a[i] = a[i] * 16 + a[i]; 65 | } 66 | } 67 | // end color parsing/setup 68 | 69 | // start color processing 70 | var maxColor = parseInt('ff', 16); 71 | var minColor = parseInt('ff', 16); 72 | function relsize(a){ 73 | if (a == maxColor) { 74 | return Infinity; 75 | } 76 | return a / (maxColor - a); 77 | } 78 | 79 | function relsizeinv(y){ 80 | if (y == Infinity) { 81 | return maxColor; 82 | } 83 | return maxColor * y / (1 + y); 84 | } 85 | 86 | for (i = 0; i < a.length; i++) { 87 | a[i] = relsizeinv(relsize(a[i]) * r); 88 | a[i] = Math.floor(a[i]).toString(16); 89 | if (a[i].length == 1) { 90 | a[i] = '0' + a[i]; 91 | } 92 | } 93 | // end color processing 94 | return '#' + a.join(''); 95 | } 96 | return { 97 | colorScale: colorScale 98 | }; 99 | })()); 100 | 101 | -------------------------------------------------------------------------------- /static/css/textboxlist.css: -------------------------------------------------------------------------------- 1 | 2 | .TextboxList ul.holder 3 | { 4 | margin: 0; 5 | border: 1px none transparent; 6 | overflow: hidden; 7 | height: auto !important; 8 | min-height: 4ex; 9 | padding: 0; 10 | /*font: 11px "Lucida Grande", "Verdana";*/ 11 | position: relative; 12 | display: inline-block; 13 | } 14 | 15 | .TextboxList.Disabled ul.holder 16 | { 17 | margin: 0; 18 | border: 0px none transparent; 19 | } 20 | 21 | 22 | .TextboxList ul.holder li 23 | { 24 | float: left; 25 | list-style-type: none; 26 | margin: 0px; 27 | } 28 | 29 | .TextboxList ul.holder li.bit-box 30 | { 31 | -moz-border-radius: 5px; 32 | -webkit-border-radius: 5px; 33 | border-radius: 5px; 34 | border: 1px solid #CAD8F3; 35 | background: #DEE7F8; 36 | padding: 2px 5px 2px; 37 | margin: 2px 4px; 38 | } 39 | 40 | .TextboxList.Disabled ul.holder li.bit-box 41 | { 42 | background: none; 43 | } 44 | 45 | 46 | 47 | .TextboxList ul.holder li.bit-input 48 | { 49 | margin: 2px 0px 2px 4px; 50 | white-space: nowrap; 51 | padding: 0px; 52 | } 53 | 54 | 55 | .TextboxList ul.holder li.bit-box-focus 56 | { 57 | border-color: #598BEC; 58 | background: #598BEC; 59 | color: #fff; 60 | } 61 | 62 | .TextboxList ul.holder li.bit-input input 63 | { 64 | /* 65 | width:10ex; 66 | border: 1px solid #999; /* IE7 is royally f'ed up in that boder: none is not good enough. we also need to give it a border-width and a border-color */ 67 | /*font: 11px "Lucida Grande", "Verdana"; 68 | outline: 0; 69 | padding: 3px;*/ 70 | 71 | -moz-border-radius: 5px; 72 | -webkit-border-radius: 5px; 73 | border-radius: 5px; 74 | border: 1px solid #CAD8F3; 75 | /*background: white;*/ 76 | padding: 2px 5px 2px; 77 | margin: 2px 4px; 78 | outline: 0; 79 | width:10ex; 80 | } 81 | 82 | 83 | 84 | .TextboxList ul.holder li.bit-box + li.bit-input 85 | { 86 | /*margin: 3px 0px 0px 0px;*/ 87 | } 88 | 89 | .TextboxList.Disabled ul.holder li.bit-box:hover 90 | { 91 | background: none; 92 | border: 1px solid #CAD8F3; 93 | } 94 | 95 | .TextboxList ul.holder li.bit-box:hover 96 | { 97 | background: #BBCEF1; 98 | border: 1px solid #6D95E0; 99 | } 100 | 101 | 102 | .TextboxList ul.holder li.bit-box-focus:hover 103 | { 104 | background: #598BEC; 105 | border: 1px solid #6D95E0; 106 | } 107 | 108 | .TextboxList ul.holder li.bit-box-focus 109 | { 110 | border-color: #598BEC; 111 | background: #598BEC; 112 | color: #fff; 113 | } 114 | 115 | .TextboxList ul.holder li.bit-box a.closebutton 116 | { 117 | position: absolute; 118 | right: 4px; 119 | top: 6px; 120 | display: block; 121 | width: 7px; 122 | height: 7px; 123 | font-size: 1px; 124 | background: url(../lib/close.gif); 125 | } 126 | 127 | 128 | .TextboxList.Disabled ul.holder li.bit-box a.closebutton 129 | { 130 | background: none; 131 | } 132 | 133 | .TextboxList ul.holder li.bit-box a.closebutton:hover 134 | { 135 | background-position: 7px; 136 | } 137 | 138 | .TextboxList ul.holder li.bit-box-focus a.closebutton, .TextboxList ul.holder li.bit-box-focus a.closebutton:hover 139 | { 140 | background-position: bottom; 141 | } 142 | 143 | .TextboxList ul.holder li.bit-box 144 | { 145 | padding-right: 15px; 146 | position: relative; 147 | } 148 | /* Autocompleter */ 149 | .TextboxListAutoComplete 150 | { 151 | position: absolute; 152 | background: #eee; 153 | min-width: 30ex; 154 | } 155 | 156 | .TextboxListAutoComplete .ACMessage 157 | { 158 | padding: 5px 7px; 159 | border: 1px solid #ccc; 160 | border-width: 0 1px 1px; 161 | } 162 | 163 | .TextboxListAutoComplete ul 164 | { 165 | margin: 0; 166 | padding: 0; 167 | overflow: auto; 168 | } 169 | 170 | .TextboxListAutoComplete ul li 171 | { 172 | padding: 5px 12px; 173 | z-index: 1000; 174 | cursor: pointer; 175 | margin: 0; 176 | list-style-type: none; 177 | border: 1px solid #ccc; 178 | border-width: 0 1px 1px; 179 | } 180 | 181 | .TextboxListAutoComplete ul li em 182 | { 183 | font-weight: bold; 184 | font-style: normal; 185 | background: #ccc; 186 | } 187 | 188 | .TextboxListAutoComplete ul li.auto-focus 189 | { 190 | background: #4173CC; 191 | color: #fff; 192 | } 193 | 194 | .TextboxListAutoComplete ul li.auto-focus em 195 | { 196 | background: none; 197 | } 198 | 199 | -------------------------------------------------------------------------------- /static/index.js: -------------------------------------------------------------------------------- 1 | var demos = {}; 2 | var demoData = null; 3 | 4 | /** 5 | * * ReplaceAll by Fagner Brack (MIT Licensed) 6 | * * Replaces all occurrences of a substring in a string 7 | * */ 8 | String.prototype.replaceAll = function( token, newToken, ignoreCase ) { 9 | var _token; 10 | var str = this + ""; 11 | var i = -1; 12 | 13 | if ( typeof token === "string" ) { 14 | 15 | if ( ignoreCase ) { 16 | 17 | _token = token.toLowerCase(); 18 | 19 | while( ( 20 | i = str.toLowerCase().indexOf( 21 | token, i >= 0 ? i + newToken.length : 0 22 | ) ) !== -1 23 | ) { 24 | str = str.substring( 0, i ) + 25 | newToken + 26 | str.substring( i + token.length ); 27 | } 28 | 29 | } else { 30 | return this.split( token ).join( newToken ); 31 | } 32 | 33 | } 34 | return str; 35 | }; 36 | 37 | function ourteampicks(){ 38 | demos.ourteampicks = new TextboxList('ourteampicks', { 39 | opacity: 1, 40 | startsWith: true, 41 | callbacks: { 42 | onAfterUpdateValues: function(values) { 43 | if (values.length > 4){ 44 | demos.ourteampicks.mainInput.hide(); 45 | } else { 46 | demos.ourteampicks.mainInput.show(); 47 | } 48 | requestSuggestions(); 49 | } 50 | 51 | }, 52 | 53 | }, demoData); 54 | } 55 | 56 | function theirteampicks(){ 57 | demos.theirteampicks = new TextboxList('theirteampicks', 58 | {opacity: 1, 59 | startsWith: true, 60 | callbacks: { 61 | onAfterUpdateValues: function(values) { 62 | if (values.length > 4){ 63 | demos.theirteampicks.mainInput.hide(); 64 | } else { 65 | demos.theirteampicks.mainInput.show(); 66 | } 67 | requestSuggestions(); 68 | } 69 | }, 70 | }, demoData); 71 | } 72 | 73 | function ourteamsugg(){ 74 | demos.ourteamsugg = new TextboxList('ourteamsugg', { 75 | opacity: 1}, demoData); 76 | demos.ourteamsugg.disable(); 77 | } 78 | 79 | // function theirteamsugg(){ 80 | // demos.theirteamsugg = new TextboxList('theirteamsugg', { 81 | // opacity: 1}, demoData); 82 | // demos.theirteamsugg.disable(); 83 | // } 84 | 85 | // function ourteambans(){ 86 | // demos.ourteambans = new TextboxList('ourteambans', 87 | // {opacity: 1, 88 | // startsWith: true, 89 | // callbacks: { 90 | // onAfterUpdateValues: requestSuggestions, 91 | // }, 92 | 93 | // }, 94 | // demoData 95 | // ); 96 | // } 97 | 98 | // function globalbans(){ 99 | // demos.globalbans = new TextboxList('globalbans', 100 | // {opacity: 1, 101 | // startsWith: true, 102 | // callbacks: { 103 | // onAfterUpdateValues: requestSuggestions, 104 | // }, 105 | 106 | // }, 107 | // demoData 108 | // ); 109 | // } 110 | 111 | function activateDemos(){ 112 | ourteampicks(); 113 | theirteampicks(); 114 | ourteamsugg(); 115 | //theirteamsugg(); 116 | //ourteambans(); 117 | //globalbans(); 118 | requestSuggestions(); 119 | } 120 | 121 | function processResponse(resp){ 122 | demos.ourteamsugg.removeAllItems(); 123 | Array.from(resp.x).each(function (b){ 124 | var i = parseInt(b); 125 | if (!demos.ourteampicks.hasItem(i)){ 126 | demos.ourteamsugg.addItemByValue(i); 127 | } 128 | } ); 129 | 130 | // demos.theirteamsugg.removeAllItems(); 131 | // Array.from(resp.y).each(function (b){ 132 | // var i = parseInt(b); 133 | // if (!demos.theirteampicks.hasItem(i)){ 134 | // demos.theirteamsugg.addItemByValue(i); 135 | // } 136 | // } ); 137 | 138 | document.getElementById("ctw").innerHTML = (100*resp.prob_x).toFixed(0)+"%"; 139 | } 140 | 141 | function requestSuggestions(){ 142 | var x, y; //, bg, bx, by; 143 | x = String.interpret(demos.ourteampicks.getValues()); 144 | y = String.interpret(demos.theirteampicks.getValues()); 145 | // bg = String.interpret(demos.globalbans.getValues()); 146 | // bx = String.interpret(demos.ourteambans.getValues()); 147 | // by = []; 148 | //new Ajax.Request('api/suggest/?'+'x='+x+',&y='+y+',&bx='+bx+',&by='+by + ',&bg='+bg +',', { 149 | new Ajax.Request('api/suggest/?'+'x='+x+'&y='+y, { 150 | method: 'get', 151 | onSuccess: function(response) { 152 | processResponse(response.responseText.evalJSON(true)); 153 | }}); 154 | } 155 | 156 | function onDocLoad(){ 157 | new Ajax.Request('static/data/heroes.json', { 158 | method: 'get', 159 | onSuccess: function(transport){ 160 | demoData = transport.responseText.evalJSON(true); 161 | activateDemos.defer(); 162 | } 163 | }); 164 | } 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DotaML 2 | 3 | A DOTA 2 hero recommendation engine for Stanford's CS 229 Machine Learning course. 4 | 5 | ## Final Report 6 | 7 | Our final report, which includes a full analysis of our project and our results, can be found here: 8 | [Download Final Report](docs/final_report.pdf) 9 | 10 | ## Blog Post 11 | 12 | A blog post summarizing our project can be found here: [http://kevintechnology.com/post/71621133663/using-machine-learning-to-recommend-heroes-for-dota-2](http://kevintechnology.com/post/71621133663/using-machine-learning-to-recommend-heroes-for-dota-2) 13 | 14 | ## Where's the Live Demo? 15 | 16 | Unfortunately, given the way we used the scikit-learn k-nearest neighbors model with a custom distance/weight function (see final report above), we are not able to provide a heroku-based live demo. It might be possible to provide a live demo via a Linode/EC2 node, but that would be cost-prohibitive for us at this time. 17 | 18 | ## Running locally 19 | 20 | Everything has been tested to work on Mac OSX 10.8. To download our project and run the code locally, follow the following procedure: 21 | 22 | ### Dependencies 23 | 24 | #### VirtualEnv 25 | 26 | We use [VirtualEnv](http://www.virtualenv.org/en/latest/) to help facilitate getting setup on a new machine. There are [a number of ways of installing it](http://www.virtualenv.org/en/latest/virtualenv.html#installation), depending on your operating system. 27 | 28 | #### GFortran 29 | 30 | [GFortran](http://gcc.gnu.org/wiki/GFortranBinaries) is required to install scipy. If you're running Mac OSX, we recommend using [Homebrew](http://brew.sh/) to install GFortran via its gcc formula: 31 | 32 | brew install gcc 33 | 34 | #### MongoDB, Database Backup, and Environment Variables (optional for just running recommendation engine) 35 | 36 | The data on Dota 2 matches we collected was stored in a MongoDB database. To extract the data to train new models, you must first [install MongoDB](http://docs.mongodb.org/manual/installation/). Then, [download the backup of our database](https://www.dropbox.com/s/jgflbwyicd56av7/dotabot_db.zip) and [restore it using this tutorial](https://docs.mongodb.org/manual/tutorial/backup-and-restore-tools/). 37 | 38 | Also, our data collection script, [dotabot2](data_collection/dotabot2.py), uses a few environment variables for configuration so that these sensitive variables are not stored in the public repository. Therefore, you must initialize the following environment variables: 39 | 40 | export DOTABOT_API_KEY=[steam web api key] 41 | export DOTABOT_USERNAME=[email username] 42 | export DOTABOT_PASSWORD=[email password] 43 | export DOTABOT_HOSTNAME=[email outgoing smtp server] 44 | export DOTABOT_DB_SERVER=[mongodb server] 45 | export DOTABOT_DB_NAME=[mongodb database name] 46 | 47 | You may find it helpful to add these commands to your bash profile in your home directory so they are automatically executed each time you open a new terminal window. 48 | 49 | ### Clone the Repository 50 | 51 | git clone git@github.com:kevincon/dotaml.git 52 | 53 | ### Initialize VirtualEnv 54 | 55 | From inside the repository root folder, initialize VirtualEnv by running: 56 | 57 | virtualenv venv 58 | 59 | This creates a new folder in the directory called "venv." You only need to do this once. Don't worry about ever accidentally adding this folder to the repository. There's an entry for it in the .gitignore file. 60 | 61 | Next, activate the VirtualEnv by running: 62 | 63 | source venv/bin/activate 64 | 65 | You should now see "(venv)" as part of your terminal prompt, indicating you are now inside your VirtualEnv. Note that closing the terminal window deactivates VirtualEnv, so you must run ```source venv/bin/activate``` each time you open a new terminal window for development. 66 | 67 | ### Installing required packages 68 | 69 | Now that you're in VirtualEnv, run the following command to automatically install all of the Python modules that are required: 70 | 71 | pip install -r requirements.txt 72 | 73 | ### Running the web app 74 | 75 | From the root folder of the project, run: 76 | 77 | python app.py 78 | 79 | This starts the Flask web app, which you can view in a web browser at [http://127.0.0.1:5000](http://127.0.0.1:5000). 80 | 81 | ### Running the recommendation engine via the command line 82 | 83 | From the root folder of the project, run: 84 | 85 | python engine.py 86 | 87 | This (by default) uses the k-nearest neighbors algorithm to perform a hard-coded hero recommendation query. See the main function of [engine.py](engine.py) for more details. 88 | 89 | ## Contributions and Feedback 90 | 91 | Feel free to submit a pull request if you are interested in continuing development. Sadly, we will probably not develop the project further for the foreseeable future. You can also contact us via email using the addresses in the final report (see above). 92 | 93 | ### License 94 | ``` 95 | The MIT License (MIT) 96 | 97 | Copyright (c) 2015 Kevin Conley 98 | 99 | Permission is hereby granted, free of charge, to any person obtaining a copy of 100 | this software and associated documentation files (the "Software"), to deal in 101 | the Software without restriction, including without limitation the rights to 102 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 103 | the Software, and to permit persons to whom the Software is furnished to do so, 104 | subject to the following conditions: 105 | 106 | The above copyright notice and this permission notice shall be included in all 107 | copies or substantial portions of the Software. 108 | 109 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 110 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 111 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 112 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 113 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 114 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 115 | ``` 116 | -------------------------------------------------------------------------------- /data_collection/dotabot.py: -------------------------------------------------------------------------------- 1 | import os, logging, argparse, calendar#, json 2 | from dota2py import api 3 | from datetime import datetime 4 | from util import print_match_history, get_game_mode_string, send_email 5 | from pymongo import MongoClient 6 | from time import sleep 7 | import atexit 8 | from sys import exit 9 | 10 | DATE_MIN = calendar.timegm(datetime(2013, 10, 1).utctimetuple()) 11 | DATE_MAX = calendar.timegm(datetime(2013, 10, 22).utctimetuple()) 12 | 13 | client = MongoClient(os.getenv('DOTABOT_DB_SERVER', 'localhost'), 27017) 14 | db = client[os.getenv('DOTABOT_DB_NAME', 'dotabot')] 15 | match_collection = db.matches 16 | 17 | logging.basicConfig(level=logging.DEBUG, filename='log.txt') 18 | logger = logging.getLogger('dotabot') 19 | 20 | last_match_id = -1 21 | date_max = DATE_MAX 22 | 23 | @atexit.register 24 | def save_match_id(): 25 | '''Save the last match ID processed to a file on exit.''' 26 | global last_match_id, date_max 27 | if last_match_id != -1 and date_max != -1: 28 | open('last_match', 'w').write('%d' % last_match_id) 29 | open('date_max', 'w').write('%d' % date_max) 30 | 31 | msg = 'Script crashed! Last match id was %s. Date_max was %s' % (last_match_id, date_max) 32 | send_email(msg, subject='Script crashed!') 33 | 34 | def setup(): 35 | '''Setup the API, MongoDB connection, etc.''' 36 | logger.setLevel(logging.DEBUG) 37 | 38 | API_KEY = os.getenv('DOTABOT_API_KEY') 39 | if not API_KEY: 40 | raise NameError('Please set the DOTABOT_API_KEY environment variables.') 41 | api.set_api_key(API_KEY) 42 | 43 | def is_valid_match(gmd_result): 44 | '''Returns True if the given match details result should be considered, 45 | and False otherwise.''' 46 | for player in gmd_result['players']: 47 | if player['leaver_status'] is not 0: 48 | return False 49 | return True 50 | 51 | def process_replay(match_id): 52 | '''Download, parse, and record data from the replay of the 53 | given match_id.''' 54 | # TODO 55 | pass 56 | 57 | def process_match_details(match_id): 58 | '''Get the details of the given match_id, check if it's valid, and 59 | if it is, add it as a record in the database and spawn a thread to 60 | download and parse the corresponding replay.''' 61 | global last_match_id 62 | gmd = api.get_match_details(match_id)['result'] 63 | 64 | if not is_valid_match(gmd): 65 | logger.debug('Not considering match %s.' % match_id) 66 | return 67 | 68 | game_mode = get_game_mode_string(gmd['game_mode']) 69 | 70 | #print 'Match ID: %s - Game Mode: %s' % (match_id, game_mode) 71 | 72 | match_collection.insert(gmd) 73 | logger.debug('Processed match_id=%s' % match_id) 74 | 75 | last_match_id = match_id 76 | 77 | #logging.debug(json.dumps(gmd, sort_keys=True, indent=4)) 78 | 79 | # TODO: 80 | # 1. Spawn replay parser thread if there aren't too many already 81 | 82 | def main(start_match_id): 83 | '''The main entry point of dotabot.''' 84 | global date_max 85 | while True: 86 | # Note: GetMatchHistory returns a list of matches in descending order, 87 | # going back in time. 88 | sleep(1.0) 89 | logger.debug('Doing GMH query for start_at_match_id=%s' % start_match_id) 90 | gmh = api.get_match_history(start_at_match_id=start_match_id, 91 | skill=3, 92 | date_min=DATE_MIN, 93 | date_max=date_max, 94 | game_mode=2, 95 | min_players=10)['result'] 96 | error_code = gmh['status'] 97 | matches = gmh['matches'] 98 | if error_code is not 1: 99 | msg = 'GetMatchHistory query starting at match_id %s returned error code %s. Retrying.' % (start_match_id, error_code) 100 | logger.debug(msg) 101 | send_email(msg, subject='GMH query failed (script still running)') 102 | continue 103 | 104 | if len(matches) is 0: 105 | msg = 'Zero matches for GMH query with start_at_match_id=%s: \n\n %s' % (start_match_id, gmh) 106 | logger.debug(msg) 107 | send_email(msg, subject='GMH query had zero matches (forced script to crash)') 108 | exit(-1) 109 | 110 | for match in matches: 111 | sleep(1.0) 112 | process_match_details(match['match_id']) 113 | 114 | tail_match = matches[-1] 115 | date_max = tail_match['start_time'] 116 | tail_match_id = tail_match['match_id'] 117 | logger.debug('Match_id of last match of GMH query: %s' % last_match_id) 118 | logger.debug('Date of last match of GMH query: %s' % date_max) 119 | # We don't want to record the tail match twice, so subtract 1 120 | start_match_id = tail_match_id - 1 121 | 122 | if __name__ == '__main__': 123 | p = argparse.ArgumentParser(description='Bot for collecting DOTA2 data') 124 | p.add_argument('--match_id', dest='match_id', default=None) 125 | args = p.parse_args() 126 | 127 | match_id = args.match_id 128 | if match_id != None: 129 | match_id = int(match_id) 130 | 131 | try: 132 | with open('last_match') as f: 133 | saved_id = int(f.readline()) 134 | ans = False 135 | try: 136 | ans = raw_input('Start at last_match %d? ' % saved_id) 137 | if ans in ['yes', 'y', 'Y', 'YES', 'Yes']: 138 | ans = True 139 | except KeyboardInterrupt: 140 | ans = False 141 | if ans is True: 142 | try: 143 | with open('date_max') as d: 144 | date_max = int(d.readline()) 145 | match_id = saved_id 146 | except IOError: 147 | print 'Could not open date_max file, ignoring last_match value.' 148 | 149 | except IOError: 150 | pass 151 | 152 | print 'OK, starting at match_id=%s' % match_id 153 | 154 | setup() 155 | main(match_id) 156 | -------------------------------------------------------------------------------- /static/data/heroes.json: -------------------------------------------------------------------------------- 1 | [{"caption": "Anti-Mage", "name": "npc_dota_hero_antimage", "value": 1}, {"caption": "Axe", "name": "npc_dota_hero_axe", "value": 2}, {"caption": "Bane", "name": "npc_dota_hero_bane", "value": 3}, {"caption": "Bloodseeker", "name": "npc_dota_hero_bloodseeker", "value": 4}, {"caption": "Crystal Maiden", "name": "npc_dota_hero_crystal_maiden", "value": 5}, {"caption": "Drow Ranger", "name": "npc_dota_hero_drow_ranger", "value": 6}, {"caption": "Earthshaker", "name": "npc_dota_hero_earthshaker", "value": 7}, {"caption": "Juggernaut", "name": "npc_dota_hero_juggernaut", "value": 8}, {"caption": "Mirana", "name": "npc_dota_hero_mirana", "value": 9}, {"caption": "Shadow Fiend", "name": "npc_dota_hero_nevermore", "value": 11}, {"caption": "Morphling", "name": "npc_dota_hero_morphling", "value": 10}, {"caption": "Phantom Lancer", "name": "npc_dota_hero_phantom_lancer", "value": 12}, {"caption": "Puck", "name": "npc_dota_hero_puck", "value": 13}, {"caption": "Pudge", "name": "npc_dota_hero_pudge", "value": 14}, {"caption": "Razor", "name": "npc_dota_hero_razor", "value": 15}, {"caption": "Sand King", "name": "npc_dota_hero_sand_king", "value": 16}, {"caption": "Storm Spirit", "name": "npc_dota_hero_storm_spirit", "value": 17}, {"caption": "Sven", "name": "npc_dota_hero_sven", "value": 18}, {"caption": "Tiny", "name": "npc_dota_hero_tiny", "value": 19}, {"caption": "Vengeful Spirit", "name": "npc_dota_hero_vengefulspirit", "value": 20}, {"caption": "Windranger", "name": "npc_dota_hero_windrunner", "value": 21}, {"caption": "Zeus", "name": "npc_dota_hero_zuus", "value": 22}, {"caption": "Kunkka", "name": "npc_dota_hero_kunkka", "value": 23}, {"caption": "Lina", "name": "npc_dota_hero_lina", "value": 25}, {"caption": "Lich", "name": "npc_dota_hero_lich", "value": 31}, {"caption": "Lion", "name": "npc_dota_hero_lion", "value": 26}, {"caption": "Shadow Shaman", "name": "npc_dota_hero_shadow_shaman", "value": 27}, {"caption": "Slardar", "name": "npc_dota_hero_slardar", "value": 28}, {"caption": "Tidehunter", "name": "npc_dota_hero_tidehunter", "value": 29}, {"caption": "Witch Doctor", "name": "npc_dota_hero_witch_doctor", "value": 30}, {"caption": "Riki", "name": "npc_dota_hero_riki", "value": 32}, {"caption": "Enigma", "name": "npc_dota_hero_enigma", "value": 33}, {"caption": "Tinker", "name": "npc_dota_hero_tinker", "value": 34}, {"caption": "Sniper", "name": "npc_dota_hero_sniper", "value": 35}, {"caption": "Necrophos", "name": "npc_dota_hero_necrolyte", "value": 36}, {"caption": "Warlock", "name": "npc_dota_hero_warlock", "value": 37}, {"caption": "Beastmaster", "name": "npc_dota_hero_beastmaster", "value": 38}, {"caption": "Queen of Pain", "name": "npc_dota_hero_queenofpain", "value": 39}, {"caption": "Venomancer", "name": "npc_dota_hero_venomancer", "value": 40}, {"caption": "Faceless Void", "name": "npc_dota_hero_faceless_void", "value": 41}, {"caption": "Skeleton King", "name": "npc_dota_hero_skeleton_king", "value": 42}, {"caption": "Death Prophet", "name": "npc_dota_hero_death_prophet", "value": 43}, {"caption": "Phantom Assassin", "name": "npc_dota_hero_phantom_assassin", "value": 44}, {"caption": "Pugna", "name": "npc_dota_hero_pugna", "value": 45}, {"caption": "Templar Assassin", "name": "npc_dota_hero_templar_assassin", "value": 46}, {"caption": "Viper", "name": "npc_dota_hero_viper", "value": 47}, {"caption": "Luna", "name": "npc_dota_hero_luna", "value": 48}, {"caption": "Dragon Knight", "name": "npc_dota_hero_dragon_knight", "value": 49}, {"caption": "Dazzle", "name": "npc_dota_hero_dazzle", "value": 50}, {"caption": "Clockwerk", "name": "npc_dota_hero_rattletrap", "value": 51}, {"caption": "Leshrac", "name": "npc_dota_hero_leshrac", "value": 52}, {"caption": "Nature's Prophet", "name": "npc_dota_hero_furion", "value": 53}, {"caption": "Lifestealer", "name": "npc_dota_hero_life_stealer", "value": 54}, {"caption": "Dark Seer", "name": "npc_dota_hero_dark_seer", "value": 55}, {"caption": "Clinkz", "name": "npc_dota_hero_clinkz", "value": 56}, {"caption": "Omniknight", "name": "npc_dota_hero_omniknight", "value": 57}, {"caption": "Enchantress", "name": "npc_dota_hero_enchantress", "value": 58}, {"caption": "Huskar", "name": "npc_dota_hero_huskar", "value": 59}, {"caption": "Night Stalker", "name": "npc_dota_hero_night_stalker", "value": 60}, {"caption": "Broodmother", "name": "npc_dota_hero_broodmother", "value": 61}, {"caption": "Bounty Hunter", "name": "npc_dota_hero_bounty_hunter", "value": 62}, {"caption": "Weaver", "name": "npc_dota_hero_weaver", "value": 63}, {"caption": "Jakiro", "name": "npc_dota_hero_jakiro", "value": 64}, {"caption": "Batrider", "name": "npc_dota_hero_batrider", "value": 65}, {"caption": "Chen", "name": "npc_dota_hero_chen", "value": 66}, {"caption": "Spectre", "name": "npc_dota_hero_spectre", "value": 67}, {"caption": "Doom", "name": "npc_dota_hero_doom_bringer", "value": 69}, {"caption": "Ancient Apparition", "name": "npc_dota_hero_ancient_apparition", "value": 68}, {"caption": "Ursa", "name": "npc_dota_hero_ursa", "value": 70}, {"caption": "Spirit Breaker", "name": "npc_dota_hero_spirit_breaker", "value": 71}, {"caption": "Gyrocopter", "name": "npc_dota_hero_gyrocopter", "value": 72}, {"caption": "Alchemist", "name": "npc_dota_hero_alchemist", "value": 73}, {"caption": "Invoker", "name": "npc_dota_hero_invoker", "value": 74}, {"caption": "Silencer", "name": "npc_dota_hero_silencer", "value": 75}, {"caption": "Outworld Devourer", "name": "npc_dota_hero_obsidian_destroyer", "value": 76}, {"caption": "Lycan", "name": "npc_dota_hero_lycan", "value": 77}, {"caption": "Brewmaster", "name": "npc_dota_hero_brewmaster", "value": 78}, {"caption": "Shadow Demon", "name": "npc_dota_hero_shadow_demon", "value": 79}, {"caption": "Lone Druid", "name": "npc_dota_hero_lone_druid", "value": 80}, {"caption": "Chaos Knight", "name": "npc_dota_hero_chaos_knight", "value": 81}, {"caption": "Meepo", "name": "npc_dota_hero_meepo", "value": 82}, {"caption": "Treant Protector", "name": "npc_dota_hero_treant", "value": 83}, {"caption": "Ogre Magi", "name": "npc_dota_hero_ogre_magi", "value": 84}, {"caption": "Undying", "name": "npc_dota_hero_undying", "value": 85}, {"caption": "Rubick", "name": "npc_dota_hero_rubick", "value": 86}, {"caption": "Disruptor", "name": "npc_dota_hero_disruptor", "value": 87}, {"caption": "Nyx Assassin", "name": "npc_dota_hero_nyx_assassin", "value": 88}, {"caption": "Naga Siren", "name": "npc_dota_hero_naga_siren", "value": 89}, {"caption": "Keeper of the Light", "name": "npc_dota_hero_keeper_of_the_light", "value": 90}, {"caption": "Io", "name": "npc_dota_hero_wisp", "value": 91}, {"caption": "Visage", "name": "npc_dota_hero_visage", "value": 92}, {"caption": "Slark", "name": "npc_dota_hero_slark", "value": 93}, {"caption": "Medusa", "name": "npc_dota_hero_medusa", "value": 94}, {"caption": "Troll Warlord", "name": "npc_dota_hero_troll_warlord", "value": 95}, {"caption": "Centaur Warrunner", "name": "npc_dota_hero_centaur", "value": 96}, {"caption": "Magnus", "name": "npc_dota_hero_magnataur", "value": 97}, {"caption": "Timbersaw", "name": "npc_dota_hero_shredder", "value": 98}, {"caption": "Bristleback", "name": "npc_dota_hero_bristleback", "value": 99}, {"caption": "Tusk", "name": "npc_dota_hero_tusk", "value": 100}, {"caption": "Skywrath Mage", "name": "npc_dota_hero_skywrath_mage", "value": 101}, {"caption": "Abaddon", "name": "npc_dota_hero_abaddon", "value": 102}, {"caption": "Elder Titan", "name": "npc_dota_hero_elder_titan", "value": 103}, {"caption": "Legion Commander", "name": "npc_dota_hero_legion_commander", "value": 104}, {"caption": "Ember Spirit", "name": "npc_dota_hero_ember_spirit", "value": 106}, {"caption": "Earth Spirit", "name": "npc_dota_hero_earth_spirit", "value": 107}, {"caption": "Abyssal Underlord", "name": "npc_dota_hero_abyssal_underlord", "value": 108}] -------------------------------------------------------------------------------- /heroes.json: -------------------------------------------------------------------------------- 1 | [{"localized_name": "Anti-Mage", "name": "npc_dota_hero_antimage", "id": 1}, {"localized_name": "Axe", "name": "npc_dota_hero_axe", "id": 2}, {"localized_name": "Bane", "name": "npc_dota_hero_bane", "id": 3}, {"localized_name": "Bloodseeker", "name": "npc_dota_hero_bloodseeker", "id": 4}, {"localized_name": "Crystal Maiden", "name": "npc_dota_hero_crystal_maiden", "id": 5}, {"localized_name": "Drow Ranger", "name": "npc_dota_hero_drow_ranger", "id": 6}, {"localized_name": "Earthshaker", "name": "npc_dota_hero_earthshaker", "id": 7}, {"localized_name": "Juggernaut", "name": "npc_dota_hero_juggernaut", "id": 8}, {"localized_name": "Mirana", "name": "npc_dota_hero_mirana", "id": 9}, {"localized_name": "Shadow Fiend", "name": "npc_dota_hero_nevermore", "id": 11}, {"localized_name": "Morphling", "name": "npc_dota_hero_morphling", "id": 10}, {"localized_name": "Phantom Lancer", "name": "npc_dota_hero_phantom_lancer", "id": 12}, {"localized_name": "Puck", "name": "npc_dota_hero_puck", "id": 13}, {"localized_name": "Pudge", "name": "npc_dota_hero_pudge", "id": 14}, {"localized_name": "Razor", "name": "npc_dota_hero_razor", "id": 15}, {"localized_name": "Sand King", "name": "npc_dota_hero_sand_king", "id": 16}, {"localized_name": "Storm Spirit", "name": "npc_dota_hero_storm_spirit", "id": 17}, {"localized_name": "Sven", "name": "npc_dota_hero_sven", "id": 18}, {"localized_name": "Tiny", "name": "npc_dota_hero_tiny", "id": 19}, {"localized_name": "Vengeful Spirit", "name": "npc_dota_hero_vengefulspirit", "id": 20}, {"localized_name": "Windranger", "name": "npc_dota_hero_windrunner", "id": 21}, {"localized_name": "Zeus", "name": "npc_dota_hero_zuus", "id": 22}, {"localized_name": "Kunkka", "name": "npc_dota_hero_kunkka", "id": 23}, {"localized_name": "Lina", "name": "npc_dota_hero_lina", "id": 25}, {"localized_name": "Lich", "name": "npc_dota_hero_lich", "id": 31}, {"localized_name": "Lion", "name": "npc_dota_hero_lion", "id": 26}, {"localized_name": "Shadow Shaman", "name": "npc_dota_hero_shadow_shaman", "id": 27}, {"localized_name": "Slardar", "name": "npc_dota_hero_slardar", "id": 28}, {"localized_name": "Tidehunter", "name": "npc_dota_hero_tidehunter", "id": 29}, {"localized_name": "Witch Doctor", "name": "npc_dota_hero_witch_doctor", "id": 30}, {"localized_name": "Riki", "name": "npc_dota_hero_riki", "id": 32}, {"localized_name": "Enigma", "name": "npc_dota_hero_enigma", "id": 33}, {"localized_name": "Tinker", "name": "npc_dota_hero_tinker", "id": 34}, {"localized_name": "Sniper", "name": "npc_dota_hero_sniper", "id": 35}, {"localized_name": "Necrophos", "name": "npc_dota_hero_necrolyte", "id": 36}, {"localized_name": "Warlock", "name": "npc_dota_hero_warlock", "id": 37}, {"localized_name": "Beastmaster", "name": "npc_dota_hero_beastmaster", "id": 38}, {"localized_name": "Queen of Pain", "name": "npc_dota_hero_queenofpain", "id": 39}, {"localized_name": "Venomancer", "name": "npc_dota_hero_venomancer", "id": 40}, {"localized_name": "Faceless Void", "name": "npc_dota_hero_faceless_void", "id": 41}, {"localized_name": "Skeleton King", "name": "npc_dota_hero_skeleton_king", "id": 42}, {"localized_name": "Death Prophet", "name": "npc_dota_hero_death_prophet", "id": 43}, {"localized_name": "Phantom Assassin", "name": "npc_dota_hero_phantom_assassin", "id": 44}, {"localized_name": "Pugna", "name": "npc_dota_hero_pugna", "id": 45}, {"localized_name": "Templar Assassin", "name": "npc_dota_hero_templar_assassin", "id": 46}, {"localized_name": "Viper", "name": "npc_dota_hero_viper", "id": 47}, {"localized_name": "Luna", "name": "npc_dota_hero_luna", "id": 48}, {"localized_name": "Dragon Knight", "name": "npc_dota_hero_dragon_knight", "id": 49}, {"localized_name": "Dazzle", "name": "npc_dota_hero_dazzle", "id": 50}, {"localized_name": "Clockwerk", "name": "npc_dota_hero_rattletrap", "id": 51}, {"localized_name": "Leshrac", "name": "npc_dota_hero_leshrac", "id": 52}, {"localized_name": "Nature's Prophet", "name": "npc_dota_hero_furion", "id": 53}, {"localized_name": "Lifestealer", "name": "npc_dota_hero_life_stealer", "id": 54}, {"localized_name": "Dark Seer", "name": "npc_dota_hero_dark_seer", "id": 55}, {"localized_name": "Clinkz", "name": "npc_dota_hero_clinkz", "id": 56}, {"localized_name": "Omniknight", "name": "npc_dota_hero_omniknight", "id": 57}, {"localized_name": "Enchantress", "name": "npc_dota_hero_enchantress", "id": 58}, {"localized_name": "Huskar", "name": "npc_dota_hero_huskar", "id": 59}, {"localized_name": "Night Stalker", "name": "npc_dota_hero_night_stalker", "id": 60}, {"localized_name": "Broodmother", "name": "npc_dota_hero_broodmother", "id": 61}, {"localized_name": "Bounty Hunter", "name": "npc_dota_hero_bounty_hunter", "id": 62}, {"localized_name": "Weaver", "name": "npc_dota_hero_weaver", "id": 63}, {"localized_name": "Jakiro", "name": "npc_dota_hero_jakiro", "id": 64}, {"localized_name": "Batrider", "name": "npc_dota_hero_batrider", "id": 65}, {"localized_name": "Chen", "name": "npc_dota_hero_chen", "id": 66}, {"localized_name": "Spectre", "name": "npc_dota_hero_spectre", "id": 67}, {"localized_name": "Doom", "name": "npc_dota_hero_doom_bringer", "id": 69}, {"localized_name": "Ancient Apparition", "name": "npc_dota_hero_ancient_apparition", "id": 68}, {"localized_name": "Ursa", "name": "npc_dota_hero_ursa", "id": 70}, {"localized_name": "Spirit Breaker", "name": "npc_dota_hero_spirit_breaker", "id": 71}, {"localized_name": "Gyrocopter", "name": "npc_dota_hero_gyrocopter", "id": 72}, {"localized_name": "Alchemist", "name": "npc_dota_hero_alchemist", "id": 73}, {"localized_name": "Invoker", "name": "npc_dota_hero_invoker", "id": 74}, {"localized_name": "Silencer", "name": "npc_dota_hero_silencer", "id": 75}, {"localized_name": "Outworld Devourer", "name": "npc_dota_hero_obsidian_destroyer", "id": 76}, {"localized_name": "Lycan", "name": "npc_dota_hero_lycan", "id": 77}, {"localized_name": "Brewmaster", "name": "npc_dota_hero_brewmaster", "id": 78}, {"localized_name": "Shadow Demon", "name": "npc_dota_hero_shadow_demon", "id": 79}, {"localized_name": "Lone Druid", "name": "npc_dota_hero_lone_druid", "id": 80}, {"localized_name": "Chaos Knight", "name": "npc_dota_hero_chaos_knight", "id": 81}, {"localized_name": "Meepo", "name": "npc_dota_hero_meepo", "id": 82}, {"localized_name": "Treant Protector", "name": "npc_dota_hero_treant", "id": 83}, {"localized_name": "Ogre Magi", "name": "npc_dota_hero_ogre_magi", "id": 84}, {"localized_name": "Undying", "name": "npc_dota_hero_undying", "id": 85}, {"localized_name": "Rubick", "name": "npc_dota_hero_rubick", "id": 86}, {"localized_name": "Disruptor", "name": "npc_dota_hero_disruptor", "id": 87}, {"localized_name": "Nyx Assassin", "name": "npc_dota_hero_nyx_assassin", "id": 88}, {"localized_name": "Naga Siren", "name": "npc_dota_hero_naga_siren", "id": 89}, {"localized_name": "Keeper of the Light", "name": "npc_dota_hero_keeper_of_the_light", "id": 90}, {"localized_name": "Io", "name": "npc_dota_hero_wisp", "id": 91}, {"localized_name": "Visage", "name": "npc_dota_hero_visage", "id": 92}, {"localized_name": "Slark", "name": "npc_dota_hero_slark", "id": 93}, {"localized_name": "Medusa", "name": "npc_dota_hero_medusa", "id": 94}, {"localized_name": "Troll Warlord", "name": "npc_dota_hero_troll_warlord", "id": 95}, {"localized_name": "Centaur Warrunner", "name": "npc_dota_hero_centaur", "id": 96}, {"localized_name": "Magnus", "name": "npc_dota_hero_magnataur", "id": 97}, {"localized_name": "Timbersaw", "name": "npc_dota_hero_shredder", "id": 98}, {"localized_name": "Bristleback", "name": "npc_dota_hero_bristleback", "id": 99}, {"localized_name": "Tusk", "name": "npc_dota_hero_tusk", "id": 100}, {"localized_name": "Skywrath Mage", "name": "npc_dota_hero_skywrath_mage", "id": 101}, {"localized_name": "Abaddon", "name": "npc_dota_hero_abaddon", "id": 102}, {"localized_name": "Elder Titan", "name": "npc_dota_hero_elder_titan", "id": 103}, {"localized_name": "Legion Commander", "name": "npc_dota_hero_legion_commander", "id": 104}, {"localized_name": "Ember Spirit", "name": "npc_dota_hero_ember_spirit", "id": 106}, {"localized_name": "Earth Spirit", "name": "npc_dota_hero_earth_spirit", "id": 107}, {"localized_name": "Abyssal Underlord", "name": "npc_dota_hero_abyssal_underlord", "id": 108}] 2 | -------------------------------------------------------------------------------- /logistic_regression/model.pkl: -------------------------------------------------------------------------------- 1 | ccopy_reg 2 | _reconstructor 3 | p0 4 | (csklearn.linear_model.logistic 5 | LogisticRegression 6 | p1 7 | c__builtin__ 8 | object 9 | p2 10 | Ntp3 11 | Rp4 12 | (dp5 13 | S'loss' 14 | p6 15 | S'lr' 16 | p7 17 | sS'C' 18 | p8 19 | F1.0 20 | sS'intercept_' 21 | p9 22 | cnumpy.core.multiarray 23 | _reconstruct 24 | p10 25 | (cnumpy 26 | ndarray 27 | p11 28 | (I0 29 | tp12 30 | S'b' 31 | p13 32 | tp14 33 | Rp15 34 | (I1 35 | (I1 36 | tp16 37 | cnumpy 38 | dtype 39 | p17 40 | (S'f8' 41 | p18 42 | I0 43 | I1 44 | tp19 45 | Rp20 46 | (I3 47 | S'<' 48 | p21 49 | NNNI-1 50 | I-1 51 | I0 52 | tp22 53 | bI00 54 | S'L\xa5\xf3>\xa6\xd3\xd2?' 55 | p23 56 | tp24 57 | bsS'verbose' 58 | p25 59 | I0 60 | sS'dual' 61 | p26 62 | I00 63 | sS'fit_intercept' 64 | p27 65 | I01 66 | sS'class_weight_' 67 | p28 68 | g10 69 | (g11 70 | (I0 71 | tp29 72 | g13 73 | tp30 74 | Rp31 75 | (I1 76 | (I2 77 | tp32 78 | g20 79 | I00 80 | S'\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?' 81 | p33 82 | tp34 83 | bsS'penalty' 84 | p35 85 | S'l2' 86 | p36 87 | sS'multi_class' 88 | p37 89 | S'ovr' 90 | p38 91 | sS'random_state' 92 | p39 93 | NsS'raw_coef_' 94 | p40 95 | g10 96 | (g11 97 | (I0 98 | tp41 99 | g13 100 | tp42 101 | Rp43 102 | (I1 103 | (I1 104 | I217 105 | tp44 106 | g20 107 | I01 108 | S'\x0bL0\xa2\xe7\xb7\xf1\xbfT\xa5\xfe4z\xdb\xd6\xbf\xb1\xb3r\xda:\x89\xac?Ds\xb4ju\xcd\xb7\xbf\xc3\x94\xe6\x9b\xd1e\xe0?\xfc\xb6\xf2\x1f\x92\xe0\xde?\xd8\x18\x1b\x93\x04\x99\x81\xbf\xb9{\xfb\x8b^\xad\x8e\xbf\xa2r\x05>\x004\xd2?t\xc62t\xa1u\xe5\xbf0\xdf@\xc7\x98\x85\xb4\xbf\xa9\x88b\xb6Z>\xd6\xbf\xed\x82[`\xbd\xf1\xc8\xbf\x81s\xe4\xf9\xbc\xb5\x9b?E/\xf5\xb8n\xcd\xb8?\xa7\xe9$\xa3r\xfa\xc7?\x92\xae!J@~\xcd\xbf=\x84\xdb\xd5\x0c8\xda\xbfq\xeb\xee\x87\xbd\x16\xd9\xbfYZ#\xfe\xdc\x81\xdf?,\x1e\xe8X\xab\x07\xbc\xbfw\xd2\xd1\xe3]\x94\xe0?m\xf9;~\xa0\xa8M?\x00\x00\x00\x00\x00\x00\x00\x00\xb5=\x01p\x08\x1a\xc1\xbf\x02|\xff\xfd\xa7\x83\xb4?7NQ\xa4\xd1\r\xe6?\xa9\xda\x96D\x95\xef\x9a?T\x00\x01\xeaa\x94\xd0?8\xa5z/\x16\xa4\xd1?\xded\xdd\xc5\xa6\xfb\xdb?\xe6S\xef\xf4\r\x9a\xc7\xbf\xaf!%H\xfe\x0f\xb3?i\xb8\xcaQ\xaem\xc0\xbfK<\x14\x998\t\xc2\xbfy\xbd^\x8dq7\xe0?L\xf2YJv\xf6\xd6?M\xdcr%xu\xa4?2\xd21o\xf2\xee\xd3\xbf\xe7Z}s\x95C\xeb?\x16\xd1\x11\xfc\xdf*\xef\xbf\xc9\xd1.J\x0b\xb9\x9d\xbf\x8f(\xab_\xed\xc3\xe7?\xc8&&}\x89\xb1\xe8\xbful\x970[\x17\xe3?3H\xfcsY\x8d\x95\xbfGP\xce\x9b\xcd6\xd9?\x98}xn7g\xc7?\xab\xb5\x06v\xf4v\x82?\xa3$S\x7f\xe4\xf0\xe1?=\x85vr\x7f\xc2\xcd?\xa1\xbcO0g\\\xd0?!\xef\x7fOCf\xdc?\x9a\xce~Z\xe6[\xe0\xbf*\xd2\x9e\x06\xbf\x8c\xc4\xbf\xf3\x9bx]\'\xfa\xbc\xbf\xc2"\x9c\x1b\xba\x06\xcf?\xbdK\x8e\x12\x94\xf4\xe0\xbf\xb4\xfa\xa5\x98\xd0\x00\xa4\xbf\xc2\xc4\xfe\x08x*\xb4?\xf8\x96\xc4~1&\xe5?\x11\xd3\xa8\xb2\xbd\x17\xcb\xbf\x8b\x97\xb5T#\xe1\xc2?\xe1H\xd0\xdaA\xbb\xdb?\x997\xfd\x9f8\x17\xe5\xbf\xb4\xdeZ%\x9b\xc4\xd9?\xb4\x97d\x91\xa0\x1b\xe1\xbf\xc2\x93\xc6Zn\xd7\x96?\xd4\x99!\xb7=\xd3\xd3\xbf\xf4\xaf5a\xd8\xed\xc2?s\xa5\xa5\xf6\xdb\t~\xbf\xf9w\xbc\x02T\xc7\xd0\xbf\xb7~\xd8\x90\x82\xfe\x8f?LVM\x05\x06\xb6\xb6\xbf.\x00\xba>\xd6\xa9\x9d?\xc16&\xa7\xf8M\xc8\xbfH\xdb\x88\x03\xb4R\xe5?o\xd1\xba\xd3\xad\r\xc2\xbf\x19"<\x86y\x96\xc5\xbf6\xce\xc4\x8f\xfeK\xd9\xbf\x0b\x7f3\xcb\xdb\xd3\xce\xbfI\xaaE\x19\xd4\xbf\xe9\xbf7\x8b\x1fB\x95L\xe3?H\x1c\xcb\x88\xca*\xd1?\xce"\x91K\x1fY\xd8?!\x168Y(\x05\xbd?\n\xc2\xe8\xb6a\xed\xd0?)\xee6+\nd\xb4\xbf\x02m\xb0\x83<\x87\xd6\xbf\x12(\x1d\xe4\xe2-\xdd?\xcb\x8c\x11\x91,n\xc7\xbf\xf4\x08\xd2+\x119\xd2?\xe2\x06\x8eI4\x9d\x9f?O\xe0\x8d(\x8e\x1e\xe6\xbf\x84\x93\x18\xac\xd5\xdd\xd7\xbf\x15\x03`\xba.\x91\xa9?\x9b\x95\x9bN\x89\xcf\xc4\xbf\xb2M\xaf\xd6\xf5\x1d\xae?]kS\x87\xac\xe9\xac?\xfa\xc8\xe41\r\xc2\xbf\xbf\xad\x9b1\xb9\x1a\x7f\xd5\xbf_Y\xa1N\xa5\xa4\xe4?e\xb8\x9a\xe1\x9bk\xdb?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\xbb\xd5&\x02\xea\xd4\xbf\xf3\xcau\x97\xb5\xd6\x8f?\x00\x00\x00\x00\x00\x00\x00\x00\xd8N\x0c\xad\'\xa7\xf1?\'\x07q]\xea\xfd\xd6?\xef&6d\xdc4\x98?4.\xa5j\x161\xc2?v\xba\x1ez\xf0\xae\xdc\xbf\xe8z\xc1\xfef\xb8\xe0\xbf\xdf\x08t}\x82\xdc\xb6?|>R\xa7\xc4\x1e\xb3?\xa9\x86\xad\x12\n\xdc\xc6\xbf\x9f\xadU{\x02\'\xec?\xd3\xf9\x16G-\x9f\xc9?\x00\xae\xac+\xd2L\xdf?m\x94ov7\xa0\xd0?3\xb3\xe8X\xd9}\xd2?ap\x9ad\xcao\x98?\t\xb8Q\xfa7s\xbc\xbf,v\x7f\x18\x1c\x0c\xd0?\xa3\x0bv#\xd6o\xd9?`\xf5\xd1}\xbb\xe0\xd2?+|Cx0\xf6\xe0\xbf\xe9\xe1*\'\x9c\xb2\xc6?\xdc\xa1\xb5\xcdG`\xd4\xbf\xef\x0f\xc7\xb9A\x92\xc7?\x00\x00\x00\x00\x00\x00\x00\x00\x94a\xbc*\x9a\xd1\xca?\x8f\xb7\xa22IC\xb1?V\x0f\x16\xe0Iz\xde\xbf\x80\xa3\xb6\xc3\xea\x85\xb1\xbf#\x1dg\xb2R\x1e\xbc\xbf\xb1E\xdc$!\x96\xcb\xbf\x97\xcaNK\x9b#\xe1\xbf\x96\x98\x17o\xa0\xaa\xcb?I\xbb\xb8\xbcT \xc7\xbf\xb7\x03\xbcS\xb3~\xc5?\xb0\xf9o\xb5f\xba\xd5?\xa4K\xbe<\x0eh\xd5\xbf\x94,\xec^\xd5\xc7\xdd\xbf\xeeRK\xd5\x999\x92\xbf\x9b\xf6,2\xa4\n\xd6?\x8e\x18^\xa9o\x87\xe9\xbf\x88\xa5\xe7d\x03\x01\xf1?\xc8@\x96\xdc\x11M\x95\xbf\xcc\x9d\xf9QY[\xe7\xbf\xf1\xd1\xb6\x0f\xd3A\xec?\xe3-\xc0\xa1\xdf]\xe1\xbfG\x89\x98>\xe0\xbc\xa6?\xf2K\r\xc5v\x84\xcc\xbf#\xcf\xf6\x1e[y\xb3\xbf\x1f.\x1f\xf8I\x04\xb1?\x95<\x13W\xea\xe7\xe1\xbf\x84P\x8d\x9c\xb7O\xc7\xbfmA\xd8\xbf\xa9\x8e~\xca\xd8\x86\xb2\xbf\xab\xcc\xc2\xec%\xd6\xdd\xbf\x8f\x9bw\xbe\xd5\xbb\x97?\x84\x83\x8d~\')\xe7?e\x04\x91\x95j\xd9\xde? \xb0\x0e\xfdhl\xa3\xbfEb&\xb3l\xcd\xd1?\xae\xe8>\xfe\xe3\x96\xa1\xbfX2v?\xff1\xc8\xbfi\xc5*=\xb7|\xc1?F\x9f\xce\xa6|\r\xcf?tJ"N\x0eH\xe2\xbfq\xd8/g\xc2\xe8\xb8\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12O\xff\xc3\x1b\x18\xe1?S\xa4j,rl\xc4\xbf\x00\x00\x00\x00\x00\x00\x00\x00L\xa5\xf3>\xa6\xd3\xd2?' 109 | p45 110 | tp46 111 | bsS'_enc' 112 | p47 113 | g0 114 | (csklearn.preprocessing.label 115 | LabelEncoder 116 | p48 117 | g2 118 | Ntp49 119 | Rp50 120 | (dp51 121 | S'classes_' 122 | p52 123 | g10 124 | (g11 125 | (I0 126 | tp53 127 | g13 128 | tp54 129 | Rp55 130 | (I1 131 | (I2 132 | tp56 133 | g17 134 | (S'i1' 135 | p57 136 | I0 137 | I1 138 | tp58 139 | Rp59 140 | (I3 141 | S'|' 142 | p60 143 | NNNI-1 144 | I-1 145 | I0 146 | tp61 147 | bI00 148 | S'\x00\x01' 149 | p62 150 | tp63 151 | bsbsS'tol' 152 | p64 153 | F0.0001 154 | sS'coef_' 155 | p65 156 | g10 157 | (g11 158 | (I0 159 | tp66 160 | g13 161 | tp67 162 | Rp68 163 | (I1 164 | (I1 165 | I216 166 | tp69 167 | g20 168 | I01 169 | S'\x0bL0\xa2\xe7\xb7\xf1\xbfT\xa5\xfe4z\xdb\xd6\xbf\xb1\xb3r\xda:\x89\xac?Ds\xb4ju\xcd\xb7\xbf\xc3\x94\xe6\x9b\xd1e\xe0?\xfc\xb6\xf2\x1f\x92\xe0\xde?\xd8\x18\x1b\x93\x04\x99\x81\xbf\xb9{\xfb\x8b^\xad\x8e\xbf\xa2r\x05>\x004\xd2?t\xc62t\xa1u\xe5\xbf0\xdf@\xc7\x98\x85\xb4\xbf\xa9\x88b\xb6Z>\xd6\xbf\xed\x82[`\xbd\xf1\xc8\xbf\x81s\xe4\xf9\xbc\xb5\x9b?E/\xf5\xb8n\xcd\xb8?\xa7\xe9$\xa3r\xfa\xc7?\x92\xae!J@~\xcd\xbf=\x84\xdb\xd5\x0c8\xda\xbfq\xeb\xee\x87\xbd\x16\xd9\xbfYZ#\xfe\xdc\x81\xdf?,\x1e\xe8X\xab\x07\xbc\xbfw\xd2\xd1\xe3]\x94\xe0?m\xf9;~\xa0\xa8M?\x00\x00\x00\x00\x00\x00\x00\x00\xb5=\x01p\x08\x1a\xc1\xbf\x02|\xff\xfd\xa7\x83\xb4?7NQ\xa4\xd1\r\xe6?\xa9\xda\x96D\x95\xef\x9a?T\x00\x01\xeaa\x94\xd0?8\xa5z/\x16\xa4\xd1?\xded\xdd\xc5\xa6\xfb\xdb?\xe6S\xef\xf4\r\x9a\xc7\xbf\xaf!%H\xfe\x0f\xb3?i\xb8\xcaQ\xaem\xc0\xbfK<\x14\x998\t\xc2\xbfy\xbd^\x8dq7\xe0?L\xf2YJv\xf6\xd6?M\xdcr%xu\xa4?2\xd21o\xf2\xee\xd3\xbf\xe7Z}s\x95C\xeb?\x16\xd1\x11\xfc\xdf*\xef\xbf\xc9\xd1.J\x0b\xb9\x9d\xbf\x8f(\xab_\xed\xc3\xe7?\xc8&&}\x89\xb1\xe8\xbful\x970[\x17\xe3?3H\xfcsY\x8d\x95\xbfGP\xce\x9b\xcd6\xd9?\x98}xn7g\xc7?\xab\xb5\x06v\xf4v\x82?\xa3$S\x7f\xe4\xf0\xe1?=\x85vr\x7f\xc2\xcd?\xa1\xbcO0g\\\xd0?!\xef\x7fOCf\xdc?\x9a\xce~Z\xe6[\xe0\xbf*\xd2\x9e\x06\xbf\x8c\xc4\xbf\xf3\x9bx]\'\xfa\xbc\xbf\xc2"\x9c\x1b\xba\x06\xcf?\xbdK\x8e\x12\x94\xf4\xe0\xbf\xb4\xfa\xa5\x98\xd0\x00\xa4\xbf\xc2\xc4\xfe\x08x*\xb4?\xf8\x96\xc4~1&\xe5?\x11\xd3\xa8\xb2\xbd\x17\xcb\xbf\x8b\x97\xb5T#\xe1\xc2?\xe1H\xd0\xdaA\xbb\xdb?\x997\xfd\x9f8\x17\xe5\xbf\xb4\xdeZ%\x9b\xc4\xd9?\xb4\x97d\x91\xa0\x1b\xe1\xbf\xc2\x93\xc6Zn\xd7\x96?\xd4\x99!\xb7=\xd3\xd3\xbf\xf4\xaf5a\xd8\xed\xc2?s\xa5\xa5\xf6\xdb\t~\xbf\xf9w\xbc\x02T\xc7\xd0\xbf\xb7~\xd8\x90\x82\xfe\x8f?LVM\x05\x06\xb6\xb6\xbf.\x00\xba>\xd6\xa9\x9d?\xc16&\xa7\xf8M\xc8\xbfH\xdb\x88\x03\xb4R\xe5?o\xd1\xba\xd3\xad\r\xc2\xbf\x19"<\x86y\x96\xc5\xbf6\xce\xc4\x8f\xfeK\xd9\xbf\x0b\x7f3\xcb\xdb\xd3\xce\xbfI\xaaE\x19\xd4\xbf\xe9\xbf7\x8b\x1fB\x95L\xe3?H\x1c\xcb\x88\xca*\xd1?\xce"\x91K\x1fY\xd8?!\x168Y(\x05\xbd?\n\xc2\xe8\xb6a\xed\xd0?)\xee6+\nd\xb4\xbf\x02m\xb0\x83<\x87\xd6\xbf\x12(\x1d\xe4\xe2-\xdd?\xcb\x8c\x11\x91,n\xc7\xbf\xf4\x08\xd2+\x119\xd2?\xe2\x06\x8eI4\x9d\x9f?O\xe0\x8d(\x8e\x1e\xe6\xbf\x84\x93\x18\xac\xd5\xdd\xd7\xbf\x15\x03`\xba.\x91\xa9?\x9b\x95\x9bN\x89\xcf\xc4\xbf\xb2M\xaf\xd6\xf5\x1d\xae?]kS\x87\xac\xe9\xac?\xfa\xc8\xe41\r\xc2\xbf\xbf\xad\x9b1\xb9\x1a\x7f\xd5\xbf_Y\xa1N\xa5\xa4\xe4?e\xb8\x9a\xe1\x9bk\xdb?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\xbb\xd5&\x02\xea\xd4\xbf\xf3\xcau\x97\xb5\xd6\x8f?\x00\x00\x00\x00\x00\x00\x00\x00\xd8N\x0c\xad\'\xa7\xf1?\'\x07q]\xea\xfd\xd6?\xef&6d\xdc4\x98?4.\xa5j\x161\xc2?v\xba\x1ez\xf0\xae\xdc\xbf\xe8z\xc1\xfef\xb8\xe0\xbf\xdf\x08t}\x82\xdc\xb6?|>R\xa7\xc4\x1e\xb3?\xa9\x86\xad\x12\n\xdc\xc6\xbf\x9f\xadU{\x02\'\xec?\xd3\xf9\x16G-\x9f\xc9?\x00\xae\xac+\xd2L\xdf?m\x94ov7\xa0\xd0?3\xb3\xe8X\xd9}\xd2?ap\x9ad\xcao\x98?\t\xb8Q\xfa7s\xbc\xbf,v\x7f\x18\x1c\x0c\xd0?\xa3\x0bv#\xd6o\xd9?`\xf5\xd1}\xbb\xe0\xd2?+|Cx0\xf6\xe0\xbf\xe9\xe1*\'\x9c\xb2\xc6?\xdc\xa1\xb5\xcdG`\xd4\xbf\xef\x0f\xc7\xb9A\x92\xc7?\x00\x00\x00\x00\x00\x00\x00\x00\x94a\xbc*\x9a\xd1\xca?\x8f\xb7\xa22IC\xb1?V\x0f\x16\xe0Iz\xde\xbf\x80\xa3\xb6\xc3\xea\x85\xb1\xbf#\x1dg\xb2R\x1e\xbc\xbf\xb1E\xdc$!\x96\xcb\xbf\x97\xcaNK\x9b#\xe1\xbf\x96\x98\x17o\xa0\xaa\xcb?I\xbb\xb8\xbcT \xc7\xbf\xb7\x03\xbcS\xb3~\xc5?\xb0\xf9o\xb5f\xba\xd5?\xa4K\xbe<\x0eh\xd5\xbf\x94,\xec^\xd5\xc7\xdd\xbf\xeeRK\xd5\x999\x92\xbf\x9b\xf6,2\xa4\n\xd6?\x8e\x18^\xa9o\x87\xe9\xbf\x88\xa5\xe7d\x03\x01\xf1?\xc8@\x96\xdc\x11M\x95\xbf\xcc\x9d\xf9QY[\xe7\xbf\xf1\xd1\xb6\x0f\xd3A\xec?\xe3-\xc0\xa1\xdf]\xe1\xbfG\x89\x98>\xe0\xbc\xa6?\xf2K\r\xc5v\x84\xcc\xbf#\xcf\xf6\x1e[y\xb3\xbf\x1f.\x1f\xf8I\x04\xb1?\x95<\x13W\xea\xe7\xe1\xbf\x84P\x8d\x9c\xb7O\xc7\xbfmA\xd8\xbf\xa9\x8e~\xca\xd8\x86\xb2\xbf\xab\xcc\xc2\xec%\xd6\xdd\xbf\x8f\x9bw\xbe\xd5\xbb\x97?\x84\x83\x8d~\')\xe7?e\x04\x91\x95j\xd9\xde? \xb0\x0e\xfdhl\xa3\xbfEb&\xb3l\xcd\xd1?\xae\xe8>\xfe\xe3\x96\xa1\xbfX2v?\xff1\xc8\xbfi\xc5*=\xb7|\xc1?F\x9f\xce\xa6|\r\xcf?tJ"N\x0eH\xe2\xbfq\xd8/g\xc2\xe8\xb8\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12O\xff\xc3\x1b\x18\xe1?S\xa4j,rl\xc4\xbf\x00\x00\x00\x00\x00\x00\x00\x00' 170 | p70 171 | tp71 172 | bsS'class_weight' 173 | p72 174 | NsS'intercept_scaling' 175 | p73 176 | I1 177 | sb. -------------------------------------------------------------------------------- /static/src/ProtoCloud.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Prototype based implementation of of a Tag Cloud 3 | http://tfluehr.com 4 | 5 | Copyright (c) 2010 Timothy Fluehr tim@tfluehr.com 6 | 7 | Permission is hereby granted, free of charge, to any person 8 | obtaining a copy of this software and associated documentation 9 | files (the "Software"), to deal in the Software without 10 | restriction, including without limitation the rights to use, 11 | copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following 14 | conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | OTHER DEALINGS IN THE SOFTWARE. 27 | 28 | If you do choose to use this, 29 | please drop me an email at tim@tfluehr.com 30 | I would like to see where this ends up :) 31 | */ 32 | (function(){ 33 | var REQUIRED_PROTOTYPE = '1.6.1'; 34 | var REQUIRED_SCRIPTY2 = '2.0.0_a5'; 35 | var REQUIRED_SCRIPTY1 = '1.8.1'; 36 | var checkRequirements = function(){ 37 | function convertVersionString(versionString){ // taken from script.aculo.us 38 | var v = versionString.replace(/_.*|\./g, ''); 39 | v = parseInt(v + '0'.times(4 - v.length), 10); 40 | return versionString.indexOf('_') > -1 ? v - 1 : v; 41 | } 42 | if ((typeof Prototype == 'undefined') || 43 | (typeof Element == 'undefined') || 44 | (typeof Element.Methods == 'undefined') || 45 | (convertVersionString(Prototype.Version) < 46 | convertVersionString(REQUIRED_PROTOTYPE))) { 47 | throw ("ProtoCloud requires the Prototype JavaScript framework >= " + 48 | REQUIRED_PROTOTYPE + 49 | " from http://prototypejs.org/"); 50 | } 51 | var s2CheckFailed = (typeof S2 == 'undefined' || typeof S2.Version == 'undefined') || 52 | (convertVersionString(S2.Version) < 53 | convertVersionString(REQUIRED_SCRIPTY2)); 54 | 55 | var scriptyCheckFailed = (typeof Scriptaculous == 'undefined') || 56 | (convertVersionString(Scriptaculous.Version) < 57 | convertVersionString(REQUIRED_SCRIPTY1)); 58 | if (s2CheckFailed && scriptyCheckFailed && typeof S2 != 'undefined' && typeof S2.CSS != 'undefined') { 59 | throw ("ProtoCloud requires the script.aculo.us JavaScript framework >= " + 60 | REQUIRED_SCRIPTY1 + 61 | " from http://script.aculo.us/"); 62 | } 63 | if (s2CheckFailed && scriptyCheckFailed) { 64 | throw ("ProtoCloud requires the script.aculo.us JavaScript framework >= " + 65 | REQUIRED_SCRIPTY2 + 66 | " from http://scripty2.com/"); 67 | } 68 | if (!scriptyCheckFailed && (typeof S2 == 'undefined' || typeof S2.CSS == 'undefined')) { 69 | throw ("When using script.aculo.us version " + REQUIRED_SCRIPTY1 + " ProtoCloud requires the use of the S1Addons.js file."); 70 | } 71 | if (typeof String.prototype.colorScale != 'function'){ 72 | throw ("ProtoCloud requires the String.colorScale function that was distributed in ScaleColor.js with ProtoCloud"); 73 | } 74 | }; 75 | checkRequirements(); 76 | // TODO: tests 77 | ProtoCloud = Class.create({ 78 | initialize: function(target, options){ 79 | this.target = $(target); 80 | this.setupOptions(options); 81 | this.target.addClassName(this.options.className); 82 | if (this.options.useEffects) { 83 | this.targetLayout = this.target.getLayout(); 84 | } 85 | this.createTags(this.options.data); 86 | 87 | this.dropExtra(); 88 | this.addEffects(); 89 | }, 90 | dropExtra: function(){ 91 | if (this.options.fitToTarget) { 92 | var ul = this.target.down('ul'); 93 | ul.setStyle({ 94 | overflow: 'hidden' 95 | }); 96 | if (ul.getHeight() < ul.scrollHeight) { 97 | this.options.dataByCount = this.options.data.sortBy((function(tagData){ 98 | return this.getCount(tagData); 99 | }).bind(this)); 100 | var tag, tagEl; 101 | while (ul.getHeight() < ul.scrollHeight) { 102 | tag = this.options.dataByCount.shift(); 103 | ul.removeChild((tagEl = $(tag.id)).nextSibling); 104 | tagEl.remove(); 105 | tagEl = tag = null; 106 | } 107 | } 108 | } 109 | }, 110 | addEffects: function(){ 111 | if (this.options.useEffects) { 112 | var data; 113 | if (this.options.dataByCount) { 114 | data = this.options.dataByCount; 115 | } 116 | else { 117 | data = this.options.data; 118 | } 119 | var layout, tempPos; 120 | var center = { 121 | left: (this.targetLayout.get('width') / 2), 122 | top: (this.targetLayout.get('height') / 2) 123 | }; 124 | var tagEl, tagData; 125 | var i = data.length; 126 | if (i === 0) { 127 | tagEl = this.target.down('li'); 128 | layout = tagEl.getLayout(); 129 | tagData = {}; 130 | tagData.left = layout.get('left'); 131 | tagData.top = layout.get('top'); 132 | tagData.width = layout.get('width'); 133 | tagData.height = layout.get('height'); 134 | tagEl.setStyle({ 135 | left: this.options.effects.position ? ((center.left - tagData.left) - parseInt(tagData.width / 2, 10)) + 'px' : '', 136 | top: this.options.effects.position ? ((center.top - tagData.top) - parseInt(tagData.height / 2, 10)) + 'px' : '', 137 | opacity: this.options.effects.opacity ? 0 : 1 138 | }); 139 | } 140 | else { 141 | while (i > 0) { 142 | i--; 143 | tagData = data[i]; 144 | tagEl = $(tagData.id); 145 | layout = tagEl.getLayout(); 146 | tagData.left = layout.get('left'); 147 | tagData.top = layout.get('top'); 148 | tagData.width = layout.get('width'); 149 | tagData.height = layout.get('height'); 150 | tagEl.setStyle({ 151 | left: this.options.effects.position ? ((center.left - tagData.left) - parseInt(tagData.width / 2, 10)) + 'px' : '', 152 | top: this.options.effects.position ? ((center.top - tagData.top) - parseInt(tagData.height / 2, 10)) + 'px' : '', 153 | opacity: this.options.effects.opacity ? 0 : 1 154 | }); 155 | if (this.options.effects.position) { 156 | tagEl.morph('left:0px;top:0px;', this.options.effectOptions); 157 | } 158 | if (this.options.effects.opacity) { 159 | tagEl.morph('opacity: 1;', this.options.effectOptions); 160 | } 161 | if (this.options.effects.color) { 162 | tagEl.down('a').morph('color:' + tagData.targetColor + ';', this.options.effectOptions); 163 | } 164 | } 165 | tagData = tagEl = data = null; 166 | } 167 | } 168 | }, 169 | getTagData: function(tagData, id){ 170 | return tagData[this.options.dataAttributes[id]]; 171 | }, 172 | getCount: function(tagData){ 173 | return this.getTagData(tagData, 'count'); 174 | }, 175 | getTag: function(tagData, keepSpace){ 176 | // this is so evil, but it was the only way I could come up with to have IE keep multi part items on a single line without expanding the ' ' because of text-align justify 177 | return this.getTagData(tagData, 'tag').replace(/\s/g, keepSpace ? ' ' : '_'); 178 | }, 179 | getSlug: function(tagData){ 180 | return this.getTagData(tagData, 'slug'); 181 | }, 182 | getId: function(){ 183 | var id; 184 | do { 185 | id = 'anonymous_element_' + Element.idCounter++; 186 | } 187 | while ($(id)); 188 | return id; 189 | }, 190 | createTags: function(data){ 191 | var ul = new Element('ul').setStyle({ 192 | position: 'relative', 193 | height: '100%', 194 | padding: 0, 195 | margin: 0 196 | }); 197 | var tag, tagOptions; 198 | if (data.length === 0) { 199 | tag = new Element('li', { 200 | id: this.getId() 201 | }).setStyle({ 202 | display: 'inline', 203 | position: 'relative' 204 | }).insert(new Element('span').setStyle({ 205 | fontSize: (this.options.minFontSize+(this.options.maxFontSize-this.options.minFontSize)/2)+'%', 206 | color: this.options.baseColor 207 | }).update(this.options.noTagsMessage)); 208 | ul.insert(tag); 209 | ul.appendChild(document.createTextNode(' ')); // for proper wrapping we need a text node in between 210 | } 211 | else { 212 | data.each((function(tagData){ 213 | if (this.options.tagForSlug) { 214 | tagData[this.options.dataAttributes.slug] = Object.isUndefined(this.getSlug(tagData)) ? this.getTag(tagData, true) : this.getSlug(tagData); 215 | } 216 | tagOptions = { 217 | 'href': this.options.isHref ? this.getSlug(tagData, true) : this.options.hrefTemplate.evaluate(tagData) 218 | }; 219 | if (this.options.showTooltip) { 220 | tagOptions.title = this.getTag(tagData, true) + ' (' + this.getCount(tagData) + ')'; 221 | } 222 | tagData.targetColor = this.options.scaleColor ? this.getFontColor(this.getCount(tagData)) : this.options.baseColor; 223 | tag = new Element('li', { 224 | id: (tagData.id = this.getId()) 225 | }).setStyle({ 226 | display: 'inline', 227 | position: 'relative' 228 | }).insert(new Element('a', tagOptions).setStyle({ 229 | fontSize: this.getFontSize(this.getCount(tagData)), 230 | color: this.options.useEffects ? this.options.baseColor : tagData.targetColor 231 | }).update(this.getTag(tagData) + (this.options.showCount ? ' (' + this.getCount(tagData) + ')' : ''))); 232 | if (typeof(this.options.linkAttributes) == 'function') { 233 | var attribs = $H(this.options.linkAttributes(tagData)); 234 | attribs.each(function(item){ 235 | tag.down('a').writeAttribute(item.key, item.value); 236 | }); 237 | } 238 | ul.insert(tag); 239 | ul.appendChild(document.createTextNode(' ')); // for proper wrapping we need a text node in between 240 | }).bind(this)); 241 | } 242 | this.target.update(ul); 243 | }, 244 | setupOptions: function(options){ 245 | var defaultOptions = { 246 | dataAttributes: { 247 | count: 'count', 248 | tag: 'name', 249 | slug: 'slug' 250 | }, 251 | useEffects: true, 252 | effects: { 253 | position: true, 254 | color: true, 255 | opacity: false // disabled by default because I don't think it looks good on text in ie 256 | }, 257 | effectOptions: { 258 | duration: 1, 259 | position: 'parallel' 260 | }, 261 | minFontSize: 100, // minimum font size in percent 262 | maxFontSize: 300, // maximum font size in percent 263 | minColorScale: 1, // minimum amount to scaleColor < 1 will actually darken 264 | maxColorScale: 5, // maximum amount to scaleColor < 1 will actually darken 265 | scaleColor: true, 266 | noTagsMessage: 'No Tags Found', 267 | className: 'ProtoCloud', 268 | baseColor: S2.CSS.colorFromString(this.target.getStyle('color')), 269 | tagForSlug: false, // if true and slug is undefined on a tag then tag will be substituted in the hrefTemplate 270 | hrefTemplate: new Template('javascript:alert("name: #{name}, count: #{count}, slug: #{slug}, ");'), 271 | linkAttributes: false, // set to a function that returns the tag attributes required to add a custom tooltip (as an object of key/value pairs) (can also be used to add additional info if it is required) it will receive the tagData as it's parameter 272 | showTooltip: true, // add a title attribute to the link containing the tag and count 273 | showCount: false, // show count with the tag name 274 | isHref: false, // set to true if the 'slug' property will contain the full contents for the link href 275 | fitToTarget: false, // will remove the lowest ranked elements that do not fit in the initial dimentions of 'target' 276 | // ** warning depending on the data set this may cause the smallest item to be larger then minFontSize 277 | data: [] // array of objects to use for each tag 278 | }; 279 | this.options = Object.deepExtend(defaultOptions, options); 280 | 281 | this.options.data.each((function(tagData){ 282 | var count = this.getCount(tagData); 283 | if (!this.options.minCount || count < this.options.minCount) { 284 | this.options.minCount = count; 285 | } 286 | if (!this.options.maxCount || count > this.options.maxCount) { 287 | this.options.maxCount = count; 288 | } 289 | }).bind(this)); 290 | this.options.slope = (this.options.maxFontSize - this.options.minFontSize) / (this.options.maxCount - this.options.minCount); 291 | this.options.yIntercept = (this.options.minFontSize - ((this.options.slope) * this.options.minCount)); 292 | 293 | this.options.cslope = (this.options.maxColorScale - this.options.minColorScale) / (this.options.maxCount - this.options.minCount); 294 | this.options.cyIntercept = (this.options.minColorScale - ((this.options.cslope) * this.options.minCount)); 295 | }, 296 | getFontColor: function(count){ 297 | var val = ((this.options.cslope * count) + this.options.cyIntercept).toFixed(3); 298 | val = this.options.maxColorScale - val + this.options.minColorScale; 299 | return this.options.baseColor.colorScale(val); 300 | }, 301 | getFontSize: function(count){ 302 | var x = ((this.options.slope * count) + this.options.yIntercept); 303 | return (isNaN(x) ? this.options.maxFontSize : x) + '%'; 304 | } 305 | }); 306 | Element.addMethods('div', { 307 | cloudify: function(div, options){ 308 | var ul = div.down('ul'); 309 | var defaultOptions = { 310 | dataAttributes: { 311 | 'count': 'count', 312 | 'tag': 'name', 313 | 'slug': 'href' 314 | }, 315 | data: [] 316 | }; 317 | options = Object.deepExtend(defaultOptions, options); 318 | if (!options.data.size()) { 319 | options.isHref = true; 320 | var tagData = {}; 321 | options.data = ul.select('li a').collect(function(link){ 322 | tagData = {}; 323 | tagData[options.dataAttributes.tag] = link.innerHTML; 324 | tagData[options.dataAttributes.slug] = link.href; 325 | tagData[options.dataAttributes.count] = parseFloat(link.readAttribute(options.dataAttributes.count)); 326 | return tagData; 327 | }); 328 | } 329 | ul = null; 330 | return new ProtoCloud(div, options); 331 | } 332 | }); 333 | })(); 334 | -------------------------------------------------------------------------------- /static/lib/TextBoxList.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Prototype based Text Box List 3 | http://tfluehr.com 4 | 5 | Copyright (c) 2010 Timothy Fluehr tim@tfluehr.com 6 | 7 | Permission is hereby granted, free of charge, to any person 8 | obtaining a copy of this software and associated documentation 9 | files (the "Software"), to deal in the Software without 10 | restriction, including without limitation the rights to use, 11 | copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following 14 | conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | OTHER DEALINGS IN THE SOFTWARE. 27 | 28 | If you do choose to use this, 29 | please drop me an email at tim@tfluehr.com 30 | I would like to see where this ends up :) 31 | */ 32 | /* 33 | * Credits: 34 | * - Idea: Facebook + Apple Mail 35 | * - Caret position method: Diego Perini 36 | * - Guillermo Rauch: Original MooTools script 37 | * - Ran Grushkowsky/InteRiders Inc. : Porting into Prototype and further development 38 | * - Tim Fluehr: Rewrite/modifications 39 | */ 40 | (function(){ 41 | var REQUIRED_PROTOTYPE = '1.6.1'; 42 | var checkRequirements = function(){ 43 | function convertVersionString(versionString){ // taken from script.aculo.us 44 | var v = versionString.replace(/_.*|\./g, ''); 45 | v = parseInt(v + '0'.times(4 - v.length), 10); 46 | return versionString.indexOf('_') > -1 ? v - 1 : v; 47 | } 48 | if ((typeof Prototype == 'undefined') || 49 | (typeof Element == 'undefined') || 50 | (typeof Element.Methods == 'undefined') || 51 | (convertVersionString(Prototype.Version) < 52 | convertVersionString(REQUIRED_PROTOTYPE))) { 53 | throw ("ProtoCloud requires the Prototype JavaScript framework >= " + 54 | REQUIRED_PROTOTYPE + 55 | " from http://prototypejs.org/"); 56 | } 57 | }; 58 | checkRequirements(); 59 | 60 | var ResizableTextbox = Class.create({ 61 | // I think this is for the invisible text box you type into for auto complete 62 | initialize: function(element, options){ 63 | this.options = Object.extend({ 64 | min: 5, 65 | max: 500, 66 | step: 7 67 | }, options); 68 | this.el = $(element); 69 | this.width = this.el.offsetWidth; 70 | this.el.observe('keyup', (function(ev){ 71 | var newsize = this.options.step * $F(this.el).length; 72 | if (newsize <= this.options.min) { 73 | newsize = this.width; 74 | } 75 | if (!($F(this.el).length == this.el.retrieve('rt-value') || newsize <= this.options.min || newsize >= this.options.max)) { 76 | this.el.setStyle({ 77 | 'width': newsize 78 | }); 79 | } 80 | }).bind(this)); 81 | this.el.observe('keydown', (function(ev){ 82 | this.el.store('rt-value', $F(this.el).length); 83 | }).bind(this)); 84 | } 85 | }); 86 | var TextboxLists = $H(); // for caching instances so we only need to add one set of observers for keypress and click 87 | document.observe('dom:loaded', function(ev){ 88 | var nonChar = false; 89 | // from http://santrajan.blogspot.com/2007/03/cross-browser-keyboard-handler.html 90 | var handleKeys = function(ev){ 91 | var ch; 92 | var list = ev.findElement('.TextboxList'); 93 | if (ev.type == "keydown") { 94 | ch = ev.keyCode; 95 | if (ch < 16 || // non printables 96 | (ch > 16 && ch < 32) || // avoid shift 97 | (ch > 32 && ch < 41) || // navigation keys 98 | ch == 46) // Delete Key 99 | { 100 | if (list) { 101 | TextboxLists.get(list.identify()).handleNonChar(ev); 102 | } 103 | nonChar = true; 104 | } 105 | else { 106 | nonChar = false; 107 | } 108 | } 109 | else { // This is keypress 110 | if (nonChar) { 111 | nonChar = false; 112 | //ev.stop(); 113 | return false; // Already Handled on keydown 114 | } 115 | ch = ev.charCode || ev.keyCode; 116 | if (ch > 31 && ch < 256 // printable character 117 | && (typeof(ev.charCode) !== 'undefined' ? ev.charCode : -1) !== 0) {// no function key (Firefox) 118 | if (list) { 119 | TextboxLists.get(list.identify()).handleChar(ev, ch); 120 | } 121 | } 122 | } 123 | }; 124 | document.observe('keypress', handleKeys); 125 | document.observe('keydown', handleKeys); 126 | document.observe('click', function(ev){ 127 | if (!ev.isRightClick() && !ev.isMiddleClick()) { 128 | var el = ev.findElement('.TextboxList, .TextboxListAutoComplete'); 129 | if (el && el.match('.TextboxListAutoComplete')){ 130 | el = $(el.retrieve('parentTextboxList')); 131 | } 132 | if (el) { 133 | var tbl = TextboxLists.get(el.identify()); 134 | tbl.click(ev); 135 | } 136 | else { // not in TextBoxList so hide all 137 | TextboxLists.each(function(item){ 138 | item.value.blur(); 139 | }); 140 | } 141 | } 142 | }); 143 | }); 144 | TextboxList = Class.create({ 145 | initialize: function(element, options, data){ 146 | var callbacks = options.callbacks; 147 | options.callbacks = {}; 148 | this.options = Object.deepExtend({ 149 | autoCompleteActive: true, // set to false to disable autocomple (for use with free typing or to display a list of items) 150 | hideInput: false, // hide the main input box. helpful for using the control for just displaying info. 151 | url: null, // url for ajax request to retrieve data. use this or pass an array with the data as the thrird param to the constructor. 152 | ajaxParamName: 'SearchValue', // name of the parameter to send to the server in ajax requests 153 | opacity: 0.8, // opacity of drop down 154 | maxresults: Infinity, // max results to display in drop down 155 | minchars: 1, // min characters to show dropdown 156 | noResultsMessage: 'No values found', 157 | hintMessage: null, // message to be displayed 158 | requestDelay: 0.3, // delay (in seconds) after last keypress before sending request. 159 | parent: document.body, // parent element for autocomplete dropdown. 160 | startsWith: false, // limit match to starts with 161 | regExp: options.startsWith ? '^{0}' : '{0}', // regular expression to use for matching/highlighting items. 162 | secondaryRegExp: null, // regular expression to use for matching/highlighting items. 163 | selectKeys: options.selectKeys ? options.selectKeys : [{ 164 | keyCode: Event.KEY_RETURN 165 | }, 166 | { 167 | keyCode: Event.KEY_TAB 168 | }], // array of keys to use for selecting an item. 169 | customTagKeys: options.customTagKeys ? options.customTagKeys : [],//, // set to a key(s) to allow adding a selected item with the currently selected text 170 | callbacks: { 171 | onMainFocus: Prototype.emptyFunction, 172 | onMainBlur: Prototype.emptyFunction, 173 | onBeforeAddItem: Prototype.emptyFunction, 174 | onAfterAddItem: Prototype.emptyFunction, 175 | onBeforeUpdateValues: Prototype.emptyFunction, 176 | onAfterUpdateValues: Prototype.emptyFunction, 177 | onControlLoaded: Prototype.emptyFunction, 178 | onBeforeAjaxRequest: Prototype.emptyFunction, 179 | onBeforeRemoveElement: Prototype.emptyFunction 180 | }, 181 | disabledColor: 'silver', // color of shim to put on top when the control is disabled 182 | disabledOpacity: 0.3, // opacity of shim to put on top when the control is disabled 183 | className: 'bit', // common className to pre-pend to created elements. 184 | uniqueValues: true // enforce uniqueness in selected items. 185 | }, options); 186 | 187 | this.input = $(element).hide(); 188 | 189 | this.bits = new Hash(); 190 | this.events = new Hash(); 191 | this.current = false; 192 | this.setupMainElements(); 193 | this.makeResizable(this.mainInput); 194 | this.setupAutoComplete(); 195 | this.setupEvents(); 196 | this.data = data || []; 197 | var tempItems = (this.input.getValue().empty() ? [] : this.input.getValue().evalJSON()); 198 | // create initial items 199 | tempItems.each(this.addItem, this); 200 | this.isDisabled(); 201 | this.options.callbacks = Object.deepExtend(this.options.callbacks, callbacks); 202 | this.options.callbacks.onControlLoaded(); 203 | }, 204 | setupEvents: function(){ 205 | this.setupContainerEvents(); 206 | this.setupMainInputEvents(); 207 | this.setupAutoCompleteEvents(); 208 | }, 209 | disable: function(){ 210 | this.isDisabled(true); 211 | }, 212 | enable: function(){ 213 | this.isDisabled(false); 214 | }, 215 | isDisabled: function(disable){ 216 | if (typeof disable == 'boolean'){ 217 | this.input.disabled = disable; 218 | } 219 | if (this.input.disabled){ 220 | if (!this.disabled){ 221 | // disable control 222 | this.disabled = true; 223 | this.mainInput.hide(); 224 | this.holder.select('.closebutton').invoke('hide'); 225 | this.container.addClassName('Disabled'); 226 | //this.showCover(); 227 | } 228 | return true; 229 | } 230 | else if (this.disabled){ 231 | // enable control 232 | this.disabled = false; 233 | this.mainInput.show(); 234 | this.holder.select('.closebutton').invoke('show'); 235 | this.container.removeClassName('Disabled'); 236 | this.hideCover(); 237 | } 238 | return false; 239 | }, 240 | 241 | showCover: function(){ 242 | if (!this.coverDiv){ 243 | this.coverDiv = new Element('div').setStyle({ 244 | opacity: this.options.disabledOpacity, 245 | backgroundColor: this.options.disabledColor, 246 | position: 'absolute', 247 | top: '0px', 248 | left: '0px', 249 | height: '100%', 250 | width: '100%' 251 | }); 252 | this.container.insert(this.coverDiv); 253 | } 254 | this.coverDiv.show(); 255 | }, 256 | hideCover: function(){ 257 | if (this.coverDiv) { 258 | this.coverDiv.hide(); 259 | } 260 | }, 261 | handleNonChar: function(ev){ 262 | if (this.isDisabled()){ 263 | return; 264 | } 265 | if (this.options.customTagKeys.find(function(item){ 266 | var singleMatch = !this.doubleKey && !item.isPair && item.keyCode === ev.keyCode && !item.printable; 267 | var doubleMatch = item.isPair && !item.printable && item.keyCode === ev.keyCode; 268 | if (!this.doubleKey && doubleMatch){ 269 | if (this.mainInput.value.length == 1){ 270 | this.doubleKey = ev.keyCode; 271 | doubleMatch = false; 272 | } 273 | } 274 | return singleMatch || doubleMatch; 275 | }, this)) { 276 | // customSelectorActive && non printable && key matches 277 | if (!this.mainInput.value.empty()) { // value is non-empty 278 | this.addItem({ //add it 279 | caption: this.mainInput.value, 280 | value: this.mainInput.value 281 | }); 282 | this.autoHide(); // hide autocomplete 283 | this.lastRequestValue = null; 284 | this.mainInput.clear().focus(); 285 | } 286 | } 287 | else if (this.options.autoCompleteActive && this.options.selectKeys.find(function(item){ 288 | return item.keyCode === ev.keyCode && !item.printable; 289 | })) { 290 | if (this.autoresults.visible()) { 291 | ev.stop();// auto complete visible select highlited item 292 | this.autoAdd(this.autocurrent); 293 | this.autocurrent = false; 294 | } 295 | } 296 | else { 297 | switch (ev.keyCode) { 298 | case Event.KEY_LEFT: 299 | if (!this.autoresults.visible()) { // auto complete not visible - highlite selected item to left if it exists 300 | return this.move('left'); 301 | } 302 | break; 303 | case Event.KEY_RIGHT: 304 | if (!this.autoresults.visible()) { 305 | return this.move('right');// auto complete not visible - highlite selected item to right (or input box) if it exists 306 | } 307 | break; 308 | case Event.KEY_DELETE: 309 | case Event.KEY_BACKSPACE: 310 | if (!this.autoresults.visible() && this.mainInput.value.length <= 1) { 311 | return this.moveDispose();// auto complete not visible - delete highlited item if exists 312 | } 313 | else if (this.mainInput.value.length <= 1) { 314 | this.lastRequestValue = null; 315 | this.autoHide();// auto complete visible and input empty so hide auto complete 316 | } 317 | else { 318 | this.handleChar(ev); // remove char so go through search 319 | } 320 | break; 321 | case Event.KEY_UP: 322 | if (this.autoresults.visible()) { 323 | ev.stop(); 324 | return this.autoMove('up');// auto complete visible move highlite up. 325 | } 326 | break; 327 | case Event.KEY_DOWN: 328 | if (this.autoresults.visible()) { 329 | ev.stop(); 330 | return this.autoMove('down');// auto complete visible move highlite down. 331 | } 332 | break; 333 | case Event.KEY_ESC: 334 | if (this.autoresults.visible()) { 335 | this.autoHide();// auto complete visible - hide it and clear the input. 336 | if (this.current) { 337 | this.lastRequestValue = null; 338 | this.mainInput.clear(); 339 | } 340 | } 341 | break; 342 | } 343 | } 344 | }, 345 | handleChar: function(ev, ch){ 346 | if (this.isDisabled()){ 347 | return; 348 | } 349 | var forceSearch = false; 350 | if (ch) { 351 | ch = String.fromCharCode(ch); 352 | } 353 | else { 354 | // from backspace/delete 355 | forceSearch = true; 356 | ch = ''; 357 | } 358 | var key; 359 | if (this.mainInput.value.length === 0 && this.doubleKey) { 360 | this.doubleKey = false; 361 | this.doubleEnd = false; 362 | } 363 | if (this.doubleKey && !this.mainInput.value.startsWith(this.doubleKey)){ 364 | this.doubleKey = false; 365 | this.doubleEnd = false; 366 | } 367 | if ((key = this.options.customTagKeys.find(function(item){ 368 | var singleMatch = !this.doubleKey && !item.isPair && item.character === ch && item.printable; 369 | var doubleMatch = item.isPair && item.printable && item.character === ch; 370 | if (doubleMatch){ 371 | if (!this.doubleKey && this.mainInput.getCaretPosition() === 0){ 372 | this.doubleKey = ch; 373 | doubleMatch = false; 374 | } 375 | else if (this.doubleKey && this.mainInput.value.startsWith(this.doubleKey)){ 376 | this.doubleEnd = true; 377 | } 378 | else{ 379 | doubleMatch = false; 380 | } 381 | } 382 | return singleMatch || doubleMatch; 383 | }, this))){ 384 | if (!this.doubleKey || this.doubleEnd) { 385 | ev.stop(); // stop key from being added to value 386 | if (!this.mainInput.value.empty()) { // value is non-empty 387 | if (this.doubleKey) { 388 | this.mainInput.value = this.mainInput.value.replace(new RegExp("^" + this.encodeSearch(this.doubleKey)), '').replace(new RegExp(this.encodeSearch(this.doubleKey) + "$"), ''); 389 | } 390 | this.addItem({ //add it 391 | caption: this.mainInput.value, 392 | value: this.mainInput.value 393 | }); 394 | this.autoHide(); // hide autocomplete 395 | this.lastRequestValue = null; 396 | this.mainInput.clear().focus(); 397 | return; 398 | } 399 | } 400 | //this.mainInput.value = this.mainInput.value.replace(new RegExp(key.character+'$'), ''); // remove the character from the end of the input string. 401 | } 402 | var sVal = this.mainInput.value+ch; 403 | if (this.doubleKey){ 404 | sVal = sVal.replace(new RegExp("^"+this.encodeSearch(this.doubleKey)), ''); 405 | } 406 | if (this.checkSearch(sVal)) { 407 | this.autoholder.descendants().each(function(ev){ 408 | ev.hide(); 409 | }); 410 | if (this.options.hintMessage) { 411 | this.autoMessage.show(); 412 | } 413 | this.autoresults.update('').hide(); 414 | this.autoPosition.bind(this, true).defer(); 415 | } 416 | else { 417 | this.focus(this.mainInput);// make sure input has focus 418 | if (this.options.url !== null)// ajax auto complete 419 | { 420 | clearTimeout(this.fetchRequest); 421 | this.fetchRequest = (function(){ 422 | if (!this.mainInput.value.empty() && (this.mainInput.value != this.lastRequestValue || forceSearch)) { // only send request if value has changed since last request 423 | this.lastRequestValue = this.mainInput.value; 424 | if (!sVal.empty()) { 425 | var options = { 426 | parameters: {}, 427 | method: 'get', 428 | onSuccess: (function(transport){ 429 | this.data = transport.responseText.evalJSON(true); 430 | this.autoShow(this.mainInput.value); 431 | }).bind(this) 432 | }; 433 | options.parameters[this.options.ajaxParamName] = this.mainInput.value; 434 | this.options.callbacks.onBeforeAjaxRequest(options); 435 | new Ajax.Request(this.options.url, options); 436 | } 437 | } 438 | }).bind(this).delay(this.options.requestDelay); // delay request by "options.requestDelay" seconds to wait for user to finish typing 439 | } 440 | else { 441 | this.autoShow.bind(this).defer(); // non ajax so use local data for auto complete 442 | } 443 | } 444 | }, 445 | click: function(ev){ 446 | if (this.isDisabled()){ 447 | return; 448 | } 449 | var el; 450 | if ((el = ev.findElement('.auto-item'))) { // click on auto complete item 451 | ev.stop(); 452 | this.autoAdd(el); 453 | this.curOn = false; 454 | } 455 | else if ((el = ev.findElement('.closebutton'))) { // x for removing a selected item 456 | ev.stop(); 457 | if (!this.current) { 458 | this.focus(this.mainInput); 459 | } 460 | this.removeElement(el.up('li')); 461 | this.focus(this.mainInput); 462 | return; 463 | } 464 | else if ((el = ev.findElement('.' + this.options.className + '-box'))) { // clicked on a selected item (not the x) 465 | ev.stop(); 466 | this.focus(el); 467 | } 468 | else if (this.mainInput.up('li') != this.current) { // clicked anywhere else so focus the text box for typing 469 | this.focus(this.mainInput); 470 | } 471 | }, 472 | setupContainerEvents: function(){ 473 | this.holder.observe('mouseover', (function(ev){ 474 | var el; // add classname on hover-in (not using :hover because of keyboard support) 475 | if ((el = ev.findElement('.' + this.options.className + '-box'))) { 476 | el.addClassName('bit-hover'); 477 | } 478 | }).bind(this)); 479 | this.holder.observe('mouseout', (function(ev){ 480 | var el;// remove classname on hover-out (not using :hover because of keyboard support) 481 | if ((el = ev.findElement('.' + this.options.className + '-box'))) { 482 | el.removeClassName('bit-hover'); 483 | } 484 | }).bind(this)); 485 | }, 486 | setupMainInputEvents: function(){ 487 | if (!this.options.hideInput) { 488 | this.mainInput.observe('keydown', (function(ev){ 489 | if (this.options.autoCompleteActive && this.autoresults.childElements().size() > 0 && 490 | this.options.selectKeys.find(function(item){ 491 | return item === ev.keyCode; 492 | })) { 493 | ev.stop(); // auto complete visible so stop on Return to prevent form submit 494 | } 495 | }).bind(this)); 496 | this.mainInput.observe('blur', this.mainBlur.bindAsEventListener(this, false)); 497 | this.mainInput.observe('focus', this.mainFocus.bindAsEventListener(this)); 498 | this.mainInput.observe('keydown', (function(ev){ 499 | if (this.isDisabled()) { 500 | return; 501 | } 502 | ev.element().store('lastvalue', ev.element().value).store('lastcaret', ev.element().getCaretPosition()); 503 | }).bind(this)); 504 | } 505 | }, 506 | setupAutoCompleteEvents: function(){ 507 | if (this.options.autoCompleteActive) { 508 | this.autoresults.observe('mouseover', (function(ev){ 509 | var el = ev.findElement('.auto-item'); 510 | if (el) { 511 | this.autoFocus(el); 512 | } 513 | this.curOn = true; 514 | }).bind(this)); 515 | this.autoresults.observe('mouseout', (function(){ 516 | this.curOn = false; 517 | }).bind(this)); 518 | } 519 | }, 520 | /* 521 | * Create/rearrage required elements for the text input box 522 | */ 523 | setupMainElements: function(){ 524 | this.container = new Element('div', { // container to hold all controls 525 | 'class': 'TextboxList' 526 | }); 527 | TextboxLists.set(this.container.identify(), this); 528 | 529 | this.holder = new Element('ul', { // hold the input and all selected items 530 | 'class': 'holder' 531 | }).insert(this.createInput({ // input to type into 532 | 'class': 'maininput' 533 | })[this.options.hideInput ? 'hide' : 'show']()); 534 | this.input.insert({ 535 | 'before': this.container 536 | }); 537 | this.container.insert(this.holder); 538 | this.container.insert(this.input); 539 | }, 540 | /* 541 | * Create required elements for the autocomplete 542 | */ 543 | setupAutoComplete: function(){ 544 | var autoholder = new Element('div', { 545 | 'class': 'TextboxListAutoComplete' 546 | }).hide().store('parentTextboxList', this.container.identify()); 547 | this.autoMessage = new Element('div', { // message to display before user types anything 548 | 'class': 'ACMessage' 549 | }).update(this.options.hintMessage).hide(); 550 | this.autoNoResults = new Element('div', { // message to display when no autocomplete results 551 | 'class': 'ACMessage' 552 | }).update(this.options.noResultsMessage).hide(); 553 | autoholder.insert(this.autoMessage); 554 | autoholder.insert(this.autoNoResults); 555 | this.autoresults = new Element('ul').hide(); 556 | 557 | autoholder.insert(this.autoresults); 558 | $(this.options.parent).insert(autoholder); 559 | 560 | this.autoholder = autoholder.setOpacity(this.options.opacity); 561 | }, 562 | getId: function(){ 563 | var id; 564 | do { 565 | id = 'anonymous_element_' + Element.idCounter++; 566 | } 567 | while ($(id)); 568 | return id; 569 | }, 570 | 571 | /* 572 | * Add a single item to the text list 573 | * val: Object { content: '', val: ''} 574 | */ 575 | addItem: function(val){ 576 | var id = this.getId(); 577 | var el = this.createBox(val, { 578 | 'id': id 579 | }); 580 | if (!this.options.callbacks.onBeforeAddItem(this.bits.values(), val, el)) { 581 | this.mainInput.up('li').insert({ 582 | 'before': el 583 | }); 584 | this.bits.set(id, val); 585 | this.updateInputValue(); 586 | this.options.callbacks.onAfterAddItem(this.bits.values(), val, el); 587 | return el; 588 | } 589 | else { 590 | return null; 591 | } 592 | }, 593 | addItemByValue: function(val){ 594 | for (index = 0; index < this.data.length; ++index) { 595 | if (this.data[index].value == val){break;} 596 | } 597 | this.addItem(this.data[index]); 598 | }, 599 | /* 600 | * update the source input box with current values 601 | * Set as a JSON string 602 | */ 603 | updateInputValue: function(){ 604 | var values = this.bits.values(); 605 | this.options.callbacks.onBeforeUpdateValues(values, this.input); 606 | this.input.setValue(Object.toJSON(values)); 607 | this.options.callbacks.onAfterUpdateValues(values, this.input); 608 | }, 609 | /* 610 | * Remove a single item from the text list 611 | * el: Element - the element to remove 612 | */ 613 | removeElement: function(el){ 614 | if (!this.options.callbacks.onBeforeRemoveElement(this.bits.values(), el)) { 615 | this.bits.unset(el.id); 616 | if (this.current == el) { 617 | this.focus(el.next('.bit-box, .bit-input')); 618 | } 619 | this.autoFeed(el.retrieve('value')); 620 | el.down('a').stopObserving(); 621 | el.stopObserving().remove(); 622 | this.updateInputValue(); 623 | } 624 | // return this; 625 | }, 626 | removeItem: function(obj, all){ 627 | var id, foundObj; 628 | if (typeof(obj.value) != 'undefined' && 629 | typeof(obj.caption) != 'undefined') { 630 | foundObj = this.bits.findAll(function(item){ 631 | return item.value.value === obj.value && item.value.caption === obj.caption; 632 | }); 633 | } 634 | else if (typeof(obj.caption) != 'undefined') { 635 | foundObj = this.bits.findAll(function(item){ 636 | return item.value.caption === obj.caption; 637 | }); 638 | } 639 | else if (typeof(obj.value) != 'undefined') { 640 | foundObj = this.bits.findAll(function(item){ 641 | return item.value.value === obj.value; 642 | }); 643 | } 644 | if (foundObj && foundObj.length > 0) { 645 | if (all) { 646 | foundObj.each(function(item){ 647 | this.removeElement($(item.key)); 648 | }, this); 649 | return foundObj; 650 | } 651 | else { 652 | this.removeElement($(foundObj.first().key)); 653 | return foundObj.first(); 654 | } 655 | } 656 | }, 657 | 658 | hasItem: function(value){ 659 | var foundObj; 660 | foundObj = this.bits.find(function(item){ 661 | return item.value.value === value; 662 | }); 663 | if (typeof(foundObj) != 'undefined'){ 664 | return true;}; 665 | return false; 666 | 667 | }, 668 | 669 | getValues: function(){ 670 | return this.bits.collect(function(it){return it.value.value}); 671 | }, 672 | 673 | 674 | removeAllItems: function(){ 675 | var id, foundObj; 676 | foundObj = this.bits.findAll(function(item){ 677 | return 1==1; 678 | }); 679 | 680 | 681 | if (foundObj && foundObj.length > 0) { 682 | foundObj.each(function(item){ 683 | this.removeElement($(item.key)); 684 | }, this); 685 | return foundObj; 686 | } 687 | }, 688 | 689 | mainFocus: function(ev){ 690 | this.focus(ev.element(), false, true); 691 | this.options.callbacks.onMainFocus(ev); 692 | }, 693 | focus: function(el, nofocus, onFocus){ 694 | if (this.isDisabled()){ 695 | return; 696 | } 697 | if (el == this.mainInput){ 698 | el = el.up('li'); 699 | this.mainInput.setStyle({ 700 | width: '' 701 | }); 702 | } 703 | if (el != this.container) { 704 | if (this.current == el) { 705 | return this; 706 | } 707 | this.blur(null, onFocus); 708 | if (el == this.mainInput.up('li')) { 709 | this.autoShow(this.mainInput.value); 710 | } 711 | el.addClassName(this.options.className + '-' + el.retrieve('type') + '-focus'); 712 | if (!nofocus) { 713 | this.callEvent(el, 'focus', onFocus); 714 | } 715 | this.current = el; 716 | return this; 717 | } 718 | else { 719 | this.callEvent(this.mainInput, 'focus', onFocus); 720 | } 721 | }, 722 | mainBlur: function(ev){ 723 | this.blur(false); 724 | //this.mainInput.setStyle({ 725 | // width: '0px' 726 | //}); 727 | this.options.callbacks.onMainBlur(ev); 728 | }, 729 | blur: function(noblur, onFocus){ 730 | if (this.isDisabled()){ 731 | return; 732 | } 733 | if (!this.current) { 734 | return this; 735 | } 736 | if (this.current == this.mainInput.up('li') || this.current.match('.bit-box')) { 737 | if (!noblur) { 738 | this.callEvent(this.mainInput, 'blur', onFocus); 739 | } 740 | this.inputBlur(this.mainInput); 741 | } 742 | this.current.removeClassName(this.options.className + '-' + this.current.retrieve('type') + '-focus'); 743 | this.current = false; 744 | return this; 745 | }, 746 | inputBlur: function(el){ 747 | if (this.options.hideInput && el == this.mainInput){ 748 | return; 749 | } 750 | if (!this.curOn) { 751 | this.blurhide = this.autoHide.bind(this).delay(0.1); 752 | } 753 | }, 754 | 755 | createBox: function(val, options){ 756 | var li = new Element('li', Object.extend(options, { 757 | 'class': this.options.className + '-box' 758 | })).update(val.caption.escapeHTML()).store('type', 'box'); 759 | var a = new Element('a', { 760 | 'href': '#', 761 | 'class': 'closebutton' 762 | }).observe('focus', function(ev){ 763 | this.focus(ev.findElement('li'), true, true); 764 | }.bind(this)).observe('blur', this.blur.bind(this, true)); 765 | li.insert(a).store('value', val); 766 | return li; 767 | }, 768 | 769 | createInput: function(options){ 770 | return this.createInputLI(options); 771 | }, 772 | 773 | createInputLI: function(options){ 774 | var li = new Element('li', { 775 | 'class': this.options.className + '-input' 776 | }); 777 | this.mainInput = new Element('input', Object.extend(options, { 778 | 'type': 'text' 779 | })); 780 | li.store('type', 'input').insert(this.mainInput); 781 | return li; 782 | }, 783 | 784 | callEvent: function(el, type, onFocus){ 785 | if (this.options.hideInput){ 786 | return; 787 | } 788 | if (!onFocus) { 789 | if (type == 'focus') { 790 | this.mainInput.focus(); 791 | } 792 | } 793 | }, 794 | 795 | makeResizable: function(li){ 796 | this.mainInput.store('resizable', new ResizableTextbox(this.mainInput, { 797 | min: this.mainInput.offsetWidth, 798 | max: (this.input.getWidth() ? this.input.getWidth() : 0) 799 | })); 800 | return this; 801 | }, 802 | 803 | checkInput: function(){ 804 | return (!this.mainInput.retrieve('lastvalue') || (this.mainInput.getCaretPosition() === 0 && this.mainInput.retrieve('lastcaret') === 0)); 805 | }, 806 | 807 | move: function(direction){ 808 | var el = this.current[(direction == 'left' ? 'previous' : 'next')]('.bit-box, .bit-input'); 809 | if (el && (this.checkInput() || direction == 'right')) { 810 | this.focus(el); 811 | } 812 | return this; 813 | }, 814 | 815 | moveDispose: function(){ 816 | if (this.current.retrieve('type') == 'box') { 817 | this.removeElement(this.current); 818 | } 819 | else if (this.checkInput() && this.bits.keys().length && this.current.previous('.bit-box, .bit-input')) { 820 | this.focus(this.current.previous('.bit-box, .bit-input')); 821 | } 822 | this.autoPosition(true); 823 | }, 824 | checkSearch: function(search){ 825 | return typeof search != 'string' || search.strip().empty() || search.length < this.options.minchars; 826 | }, 827 | encodeSearch: function(search){ 828 | return search.replace(/([\^\$\.\*\+\?\=\!\:\|\\\/\(\)\[\]\{\}])/g, '\\$1'); 829 | }, 830 | autoShow: function(search){ 831 | if (typeof search != 'string'){ 832 | search = this.mainInput.value; 833 | } 834 | this.autoPosition(); 835 | this.autoholder.show(); 836 | this.autoholder.descendants().each(function(ev){ 837 | ev.hide(); 838 | }); 839 | if (this.checkSearch(search)) { 840 | if (this.options.hintMessage && !this.blurhide) { 841 | this.autoMessage.show(); 842 | } 843 | this.autoresults.update('').hide(); 844 | this.autoPosition.bind(this, true).defer(); 845 | } 846 | else { 847 | if (this.options.autoCompleteActive) { 848 | this.autoresults.show().update(''); 849 | var count = 0, matchCount = 0; 850 | var regExp = new RegExp(this.options.regExp.replace('{0}', this.encodeSearch(search)), 'i'); 851 | var results = this.data.filter(function(obj){ 852 | if (matchCount === this.options.maxresults) { 853 | throw $break; 854 | } 855 | var returnVal = obj ? regExp.test(obj.caption) : false; 856 | if (returnVal && this.options.uniqueValues) { 857 | returnVal = !this.bits.find(function(item){ 858 | return item.value.caption === obj.caption; 859 | }); 860 | } 861 | if (returnVal) { 862 | matchCount++; 863 | } 864 | return returnVal; 865 | }, this); 866 | var secondaryRegExp; 867 | if (this.options.secondaryRegExp) { 868 | secondaryRegExp = new RegExp(this.options.secondaryRegExp.replace('{0}', this.encodeSearch(search)), 'i'); 869 | var secondaryResults = this.data.filter(function(obj){ 870 | if (matchCount === this.options.maxresults) { 871 | throw $break; 872 | } 873 | var returnVal = obj ? secondaryRegExp.test(obj.caption) && 874 | !results.find(function(item){ 875 | return item.caption === obj.caption; 876 | }) : false; 877 | if (returnVal && this.options.uniqueValues) { 878 | returnVal = !this.bits.find(function(item){ 879 | return item.value.caption === obj.caption; 880 | }); 881 | } 882 | if (returnVal) { 883 | matchCount++; 884 | } 885 | return returnVal; 886 | }, this); 887 | results = results.concat(secondaryResults); 888 | } 889 | 890 | results.each(function(result, ti){ 891 | count++; 892 | if (ti >= this.options.maxresults) { 893 | throw $break; 894 | } 895 | var el = new Element('li', { 896 | 'class': 'auto-item' 897 | }); 898 | el.update(this.autoCompleteItemHTML(result, regExp, secondaryRegExp)); 899 | this.autoresults.insert(el); 900 | el.store('result', result); 901 | if (ti === 0) { 902 | this.autoFocus(el); 903 | } 904 | }, this); 905 | if (count === 0) { 906 | this.autoNoResults.show(); 907 | this.autoresults.hide(); 908 | } 909 | } 910 | } 911 | return this; 912 | }, 913 | 914 | autoCompleteItemHTML: function(result, highlight, secondaryHighlight){ 915 | var retVal = result.caption.gsub(highlight, function(match){ 916 | return '\t\t\t' + match[0] + '\f\f\f'; 917 | }); 918 | if (secondaryHighlight) { 919 | retVal = retVal.gsub(secondaryHighlight, function(match){ 920 | return '\t\t\t' + match[0] + '\f\f\f'; 921 | }); 922 | } 923 | return retVal.replace(/\t\t\t/g, '').replace(/\f\f\f/g, ''); 924 | }, 925 | 926 | autoHide: function(){ 927 | this.autoMessage.hide(); 928 | this.autoresults.update('').hide(); 929 | this.autoholder.hide(); 930 | this.blurhide = null; 931 | this.doubleKey = false; 932 | this.doubleEnd = false; 933 | return this; 934 | }, 935 | 936 | autoFocus: function(el){ 937 | if (!el) { 938 | return; 939 | } 940 | if (this.autocurrent) { 941 | this.autocurrent.removeClassName('auto-focus'); 942 | } 943 | this.autocurrent = el.addClassName('auto-focus'); 944 | }, 945 | 946 | autoMove: function(direction){ 947 | if (this.autoresults.childElements().size() === 0) { 948 | return; 949 | } 950 | this.autoFocus(this.autocurrent[(direction == 'up' ? 'previous' : 'next')]('.auto-item')); 951 | this.autoresults.scrollTop = this.autocurrent.positionedOffset()[1] - this.autocurrent.getHeight(); 952 | return this; 953 | }, 954 | 955 | autoFeed: function(val){ 956 | if (!this.data.find(function(item){ 957 | return item.caption === val.caption; 958 | })) { 959 | this.data.push(val); 960 | } 961 | return this; 962 | }, 963 | 964 | autoAdd: function(el){ 965 | if (!el || !el.retrieve('result')) { 966 | return; 967 | } 968 | if (this.addItem(el.retrieve('result'))) { 969 | delete this.data[this.data.indexOf(Object.toJSON(el.retrieve('result')))]; 970 | } 971 | this.autoHide(); 972 | this.lastRequestValue = null; 973 | this.mainInput.clear().focus(); 974 | return this; 975 | }, 976 | autoPosition: function(force){ 977 | if (force || !this.autoholder.visible()) { 978 | var contOffset = this.holder.viewportOffset(); 979 | var parentOffset = this.options.parent.viewportOffset(); 980 | contOffset.top = contOffset.top - parentOffset.top; 981 | contOffset.left = contOffset.left - parentOffset.left; 982 | this.autoholder.setStyle({ 983 | left: contOffset.left + 'px', 984 | top: (contOffset.top + this.container.getHeight()) + 'px', 985 | width: this.holder.getWidth() + 'px' 986 | }); 987 | } 988 | } 989 | }); 990 | 991 | //helper functions 992 | Element.addMethods({ 993 | getCaretPosition: function(el){ 994 | var pos = 0; 995 | if (el.selectionStart || el.selectionStart == '0') { 996 | pos = el.selectionStart; 997 | } 998 | else { 999 | var r = document.selection.createRange().duplicate(); 1000 | r.moveEnd('character', el.value.length); 1001 | if (r.text === '') { 1002 | pos = el.value.length; 1003 | } 1004 | else { 1005 | pos = el.value.lastIndexOf(r.text); 1006 | } 1007 | } 1008 | return pos; 1009 | } 1010 | }); 1011 | if (typeof(Object.deepExtend) == 'undefined') { 1012 | Object.deepExtend = function(destination, source){ 1013 | for (var property in source) { 1014 | if (source[property] && source[property].constructor && 1015 | source[property].constructor === Object) { 1016 | destination[property] = destination[property] || {}; 1017 | arguments.callee(destination[property], source[property]); 1018 | } 1019 | else { 1020 | destination[property] = source[property]; 1021 | } 1022 | } 1023 | return destination; 1024 | }; 1025 | } 1026 | 1027 | })(); 1028 | --------------------------------------------------------------------------------