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