Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Git dump after removing private data

  • Loading branch information...
commit f2f166285ab7b42a3ee07629a9a9cf234137183c 0 parents
Jamie Brandon authored
39 README.markdown
@@ -0,0 +1,39 @@
+# Installation (on Ubuntu 11.04)
+
+ # install dependencies
+ sudo apt-get install build-essential erlang erlang-dev python python-dev python-django python-numpy python-scipy python-simplejson python-pyparsing python-flask nginx libcmph-dev git-core
+
+ # under large workloads disco runs out of file descriptors
+ cat 'fs.file-max = 1000000' >> /etc/sysctl.conf
+ sysctl -p
+ cat '* soft nofile 800000' >> /etc/security/limits.conf
+ cat '* hard nofile 800000' >> /etc/security/limits.conf
+ cat 'root soft nofile 800000' >> /etc/security/limits.conf
+ cat 'root hard nofile 800000' >> /etc/security/limits.conf
+ reboot
+
+ # build disco (with my changes)
+ git clone git://github.com/jamii/disco.git
+ git checkout deploy
+ cd disco
+ make
+ sudo make install
+ sudo make install-core
+ sudo make install-discodb
+
+ # setup disco
+ ssh-keygen (default options)
+ cat .ssh/id_rsa.pub >> .ssh/authorized_keys
+ disco start
+ in web config:
+ change node name from 'localhost' to hostname
+ change workers to 4
+
+ # build and setup springer-analytics
+ git clone git@github.com:scattered-thoughts/springer-analytics.git
+ cd springer-analytics
+ cp analytics.conf /etc/nginx/sites-enabled/
+ wget http://trac.edgewall.org/export/10732/trunk/contrib/htpasswd.py
+ python htpasswd.py -c -b /etc/nginx/htpasswd $username $password
+ service nginx restart
+ nohup python server.py > server_log &
11 analytics.conf
@@ -0,0 +1,11 @@
+server {
+ listen 80;
+
+ location / {
+ proxy_pass http://localhost:8000/;
+ proxy_redirect off;
+
+ auth_basic "Restricted";
+ auth_basic_user_file htpasswd;
+ }
+}
139 data.py
@@ -0,0 +1,139 @@
+import datetime
+import math
+
+import util
+
+class Histogram():
+ def __init__(self, items, min_key, max_key):
+ # min_key and max_key are inclusive
+ self.min_key = min_key
+ self.max_key = max_key
+ self.counts = {}
+ for item in items:
+ self.counts[item] = self.counts.get(item, 0) + 1
+
+ def __str__(self):
+ return str(dict([(k, self[k]) for k in self]))
+
+ def __repr__(self):
+ return repr(dict([(k, self[k]) for k in self]))
+
+ def __contains__(self, item):
+ if item in self.counts:
+ return True
+ elif self.min_key <= item <= self.max_key:
+ return True
+ else:
+ return False
+
+ def __getitem__(self, item):
+ if item in self.counts:
+ return self.counts[item]
+ elif self.min_key <= item <= self.max_key:
+ return 0
+ else:
+ raise KeyError(item)
+
+ def __iter__(self):
+ if type(self.min_key) is int:
+ return iter(xrange(self.min_key, self.max_key+1))
+ elif type(self.min_key) is datetime.date:
+ return util.date_range(self.min_key, self.max_key)
+
+ def group_by(self, fun):
+ # require that fun is monotonic
+ self.min_key = fun(self.min_key)
+ self.max_key = fun(self.max_key)
+ counts = {}
+ for key, count in self.counts.items():
+ counts[fun(key)] = counts.get(key, 0) + count
+ self.counts = counts
+
+ def total(self):
+ return sum(self.counts.values())
+
+class SparseList():
+ def __init__(self):
+ self.sorted = False
+ self.elems = []
+ self.num_zeros = 0
+ self.num_elems = 0
+
+ def append(self, elem):
+ assert(elem >= 0)
+ self.num_elems += 1
+ if elem == 0:
+ self.num_zeros += 1
+ else:
+ self.elems.append(elem)
+ self.sorted = False
+
+ def sort(self):
+ if not self.sorted:
+ self.elems = sorted(self.elems)
+ self.sorted = True
+
+ def __len__(self):
+ return self.num_elems
+
+ def __iter__(self):
+ for i in range(0, self.num_zeros):
+ yield 0
+ for elem in self.elems:
+ yield elem
+
+ def __getitem__(self, i):
+ if i < self.num_zeros:
+ return 0
+ else:
+ return self.elems[i - self.num_zeros]
+
+ def mean(self):
+ return sum(self.elems) / float(self.num_elems)
+
+ def min(self):
+ if self.num_zeros > 0:
+ return 0
+ else:
+ return min(self.elems)
+
+ def max(self):
+ if self.elems:
+ return max(self.elems)
+ else:
+ return 0
+
+ def percentile(self, percentile):
+ self.sort()
+ index = (self.num_elems - 1) * (percentile / 100.)
+ decimal = index % 1
+ if decimal == 0:
+ return self[int(index)]
+ else:
+ lower = int(math.floor(index))
+ upper = int(math.ceil(index))
+ return (1-decimal)*self[lower] + decimal*self[upper]
+
+def summary(histograms):
+ keys = set(util.flatten(histograms))
+ summary = {
+ 'elems' : len(histograms),
+ 'mean' : dict([(k,0.0) for k in keys]),
+ 'min' : dict([(k,0.0) for k in keys]),
+ 'max' : dict([(k,0.0) for k in keys]),
+ '25%' : dict([(k,0.0) for k in keys]),
+ '50%' : dict([(k,0.0) for k in keys]),
+ '75%' : dict([(k,0.0) for k in keys]),
+ }
+ for key in keys:
+ values = SparseList()
+ for histogram in histograms:
+ if key in histogram:
+ values.append(histogram[key])
+ summary['mean'][key] = float(values.mean())
+ summary['min'][key] = float(values.min())
+ summary['max'][key] = float(values.max())
+ summary['25%'][key] = float(values.percentile(25))
+ summary['50%'][key] = float(values.percentile(50))
+ summary['75%'][key] = float(values.percentile(75))
+ return summary
139 db.py
@@ -0,0 +1,139 @@
+from disco.core import classic_iterator
+from disco.worker.classic.func import chain_reader, nop_map
+from disco.util import kvgroup
+from discodb import DiscoDB
+
+import os
+import os.path
+import shutil
+import random
+import datetime
+import pickle
+
+from util import Job, map_with_errors, reduce_with_errors
+import util
+
+dbs = {}
+
+def dirname(name):
+ return os.path.join('/usr/local/var/springer-analytics', name)
+
+def bckname(name):
+ return dirname(name + '.' + datetime.datetime.now().strftime('%Y-%m-%d:%H-%M-%S') + '.bck')
+
+def filename(name, partition):
+ return os.path.join(dirname(name), str(partition))
+
+class CreateDB(Job):
+ map_reader = staticmethod(chain_reader)
+
+ sort = True
+
+ @staticmethod
+ @map_with_errors
+ def map((key, value), params):
+ yield key, pickle.dumps(value)
+
+ @staticmethod
+ @reduce_with_errors
+ def reduce(iter, params):
+ partitions = params['partitions']
+ name = params['name']
+ discodb = DiscoDB(kvgroup(iter))
+ try:
+ # figure out what partition we are in
+ key = discodb.keys().__iter__().next()
+ partition = util.default_partition(key, partitions, params)
+ discodb.dump(open(filename(name, partition), 'w'))
+ yield partition, None
+ except StopIteration:
+ # no keys, nothing to write
+ pass
+
+partition_size = 1 * 1024 * 1024
+
+def create(name, input):
+ # move the existing dir to a backup dir
+ dir = dirname(name)
+ bck = bckname(name)
+ if os.path.exists(dir):
+ shutil.move(dir, bck)
+ os.makedirs(dir)
+
+ input_size = sum([util.result_size(url) for url in input])
+ partitions = 1 + (input_size / partition_size) # close enough
+ with open(os.path.join(dir, 'partitions'), 'w') as file:
+ file.write(str(partitions))
+ job = CreateDB().run(
+ input = input,
+ partitions = partitions,
+ params = {'name':name, 'partitions':partitions}
+ )
+ created = [key for key, value in classic_iterator(job.wait())]
+
+ load(name)
+
+ # successful - purge job and delete the backup dir
+ job.purge()
+ if os.path.exists(bck):
+ shutil.rmtree(bck)
+
+ return created
+
+def load(name):
+ dir = dirname(name)
+ with open(os.path.join(dir, 'partitions')) as file:
+ partitions = int(file.read())
+ discodbs = [DiscoDB()] * partitions
+ for partition in xrange(0,partitions):
+ path = filename(name, partition)
+ if os.path.exists(path):
+ discodbs[partition] = DiscoDB.load(open(path))
+ dbs[name] = discodbs
+
+def ensure(name):
+ if not dbs.has_key(name):
+ load(name)
+
+def get(name, key):
+ ensure(name)
+ discodbs = dbs[name]
+ partition = util.default_partition(key, len(discodbs), None)
+ results = discodbs[partition].get(key)
+ if results == None:
+ raise NotFound('db:' + name, key)
+ else:
+ results = list(results)
+ if len(results) == 1:
+ return util.encode(pickle.loads(results[0]))
+ else:
+ raise MultipleValues(name, key, results)
+
+def items(name):
+ ensure(name)
+ for discodb in dbs[name]:
+ for key, value in discodb.items():
+ yield key, value
+
+class NotFound(Exception):
+ def __init__(self, source, key):
+ self.source = source
+ self.key = key
+
+ def __str__(self):
+ return 'NotFound(%s, %s)' % (self.source, self.key)
+
+ def __repr__(self):
+ return 'NotFound(%s, %s)' % (self.source, self.key)
+
+class MultipleValues(Exception):
+ def __init__(self, name, key, values):
+ self.name = name
+ self.key = key
+ self.values = values
+
+ def __str__(self):
+ return 'MultipleValues(%s, %s, %s)' % (self.name, self.key, self.values)
+
+ def __repr__(self):
+ return 'MultipleValues(%s, %s, %s)' % (self.name, self.key, self.values)
179 jobs.py
@@ -0,0 +1,179 @@
+from disco.util import kvgroup
+from disco.error import CommError
+from disco.core import result_iterator
+
+import re
+import datetime
+import random
+
+from util import Job, map_with_errors, reduce_with_errors, print_errors
+import metadata
+import db
+import query
+import data
+
+download_pattern = re.compile("{ _id: ObjectId\('([^']*)'\), d: ([^,]*), doi: \"([^\"]*)\", i: \"([^\"]*)\", s: ([^,]*), ip: \"([^\"]*)\" }")
+
+class ParseDownloads(Job):
+ @staticmethod
+ def map(line, params):
+ match = jobs.download_pattern.match(line)
+ if match:
+ (id, date, doi, _, _, ip) = match.groups()
+ download = {
+ 'id':id.decode('latin1').encode('utf8'),
+ 'doi':doi.decode('latin1').encode('utf8'),
+ 'date':datetime.date(int(date[0:4]), int(date[4:6]), int(date[6:8])),
+ 'ip':ip.decode('latin1').encode('utf8')
+ }
+ yield id, download
+ else:
+ yield 'error', line
+
+class FindDataRange(Job):
+ partitions = 1
+
+ @staticmethod
+ @map_with_errors
+ def map((id, download), params):
+ yield download['date'], None
+
+ @staticmethod
+ @reduce_with_errors
+ def reduce(iter, params):
+ date, _ = iter.next()
+ min_date = date
+ max_date = date
+ for date, _ in iter:
+ min_date = min(min_date, date)
+ max_date = max(max_date, date)
+ yield 'min_date', min_date
+ yield 'max_date', max_date
+
+class PullMetadata(Job):
+ sort = True
+
+ partitions = 1
+
+ @staticmethod
+ @map_with_errors
+ def map((id, download), params):
+ yield download['doi'], None
+
+ @staticmethod
+ @reduce_with_errors
+ def reduce(iter, params):
+ for doi, nones in kvgroup(iter):
+ try:
+ yield doi, metadata.get(doi)
+ except db.NotFound:
+ try:
+ yield doi, metadata.fetch(doi)
+ except CommError, exc:
+ yield 'error', str(exc) # CommError has useless repr
+ except Exception, exc:
+ yield 'error', repr(exc)
+
+class BuildHistograms(Job):
+ sort = True
+
+ @staticmethod
+ @map_with_errors
+ def map((id, download), params):
+ doi = download['doi']
+ date = download['date']
+ yield doi, date
+
+ @staticmethod
+ @reduce_with_errors
+ def reduce(iter, params):
+ for doi, dates in kvgroup(iter):
+ yield doi, data.Histogram(dates, params['min_date'], params['max_date'])
+
+class InvertFeatures(Job):
+ sort = True
+
+ @staticmethod
+ @map_with_errors
+ def map((doi, meta), params):
+ for feature in metadata.features(doi, meta):
+ yield feature, doi
+
+ @staticmethod
+ @reduce_with_errors
+ def reduce(iter, params):
+ for feature, dois in kvgroup(iter):
+ yield feature, list(dois)
+
+class TopDownloads(Job):
+ @staticmethod
+ @map_with_errors
+ def map((feature, dois), params):
+ totals = [(db.get('histograms', doi).total(), doi) for doi in dois]
+ totals.sort()
+ top5 = [doi for (total, doi) in totals[-5:]]
+ yield feature, top5
+
+ @staticmethod
+ @reduce_with_errors
+ def reduce(iter, params):
+ return iter
+
+class PrecalculateSummaries(Job):
+ sort = True
+
+ @staticmethod
+ @map_with_errors
+ def map((feature, dois), params):
+ yield feature, None
+
+ @staticmethod
+ @reduce_with_errors
+ def reduce(iter, params):
+ for feature, nones in kvgroup(iter):
+ try:
+ yield feature, query.evaluate([feature]).next()
+ except Exception, exc:
+ yield 'error', 'feature %s: %s' % (feature, exc)
+
+def build(dump='dump:downloads'):
+ downloads = ParseDownloads().run(input=[dump])
+ print_errors(downloads)
+
+ find_data_range = FindDataRange().run(input=[downloads.wait()])
+ print_errors(find_data_range)
+ data_range = dict(result_iterator(find_data_range.results()))
+ print data_range
+ find_data_range.purge()
+
+ histograms = BuildHistograms().run(input=[downloads.wait()], params=data_range)
+ print_errors(histograms)
+
+ db.create('histograms', histograms.wait())
+ histograms.purge()
+
+ metadata = PullMetadata().run(input=[downloads.wait()])
+ print_errors(metadata)
+ downloads.purge()
+
+ db.create('metadata', metadata.wait())
+
+ features = InvertFeatures().run(input=[metadata.wait()])
+ print_errors(features)
+ metadata.purge()
+
+ db.create('features', features.wait())
+
+ top = TopDownloads().run(input=[features.wait()])
+ print_errors(top)
+
+ db.create('top-new', top.wait())
+ top.purge()
+
+ summaries = PrecalculateSummaries().run(input=[features.wait()])
+ print_errors(summaries)
+ features.purge()
+
+ db.create('summaries-new', summaries.wait())
+ summaries.purge()
+
104 metadata.py
@@ -0,0 +1,104 @@
+import httplib, urllib
+import json
+import datetime
+import time
+import random
+
+from discodb import DiscoDB
+import os
+from disco.error import CommError
+
+import db
+import util
+
+keys = json.load(open('keys'))
+
+def fetch(doi, delay=0.3):
+ before = time.time()
+
+ # conn = httplib.HTTPConnection('api.springer.com')
+ conn = httplib.HTTPConnection('springer.api.mashery.com')
+ path = '/metadata/json?%s' % urllib.urlencode({'q':'doi:'+doi, 'api_key':keys['metadata']})
+ conn.request('GET', path)
+ response = conn.getresponse()
+ status = response.status
+ data = response.read()
+ conn.close()
+
+ after = time.time()
+ time.sleep(max(0, delay + before - after))
+
+ if status == 200:
+ meta = util.encode(json.loads(data)) # !!! metadata encoding?
+ if meta['records']:
+ return meta
+ else:
+ # sometimes get an empty response rather than a 404
+ raise db.NotFound('fetch:empty', doi)
+ elif status == 404:
+ raise db.NotFound('fetch:404', doi)
+ else:
+ raise CommError(data, 'http://api.springer.com' + path, code=status)
+
+# for testing
+def fake(doi):
+ def string():
+ return random.choice('qwertyuiop')
+ def keyed(key, gen):
+ return {key: gen()}
+ def value(gen):
+ return {'value':gen(), 'count':'1'}
+ def some(gen, args):
+ return [gen(*args) for i in xrange(0,int(random.expovariate(0.5)))]
+ def date():
+ year = 2010
+ month = random.randint(10,12)
+ day = random.randint(1,28)
+ return '%04d-%02d-%02d' % (year, month, day)
+ return {
+ 'records':[{
+ 'identifier':'doi:%s' % doi,
+ 'title': string(),
+ 'publicationDate': date(),
+ 'creators': some(keyed, ['creator', string]),
+ 'publicationName': string(),
+ 'issn': string(),
+ }],
+ 'facets':[{
+ 'name':'subject',
+ 'values':some(value, [string])
+ }]
+ }
+
+# raises db.NotFound
+def get(doi):
+ return db.get('metadata', doi)
+
+def features(doi, meta):
+ if not meta['records'][0].has_key('doi'):
+ yield 'doi:%s' % doi
+ for key, value in meta['records'][0].items():
+ if (type(value) is str):
+ yield '%s:%s' % (key, value)
+ elif type(value) is list:
+ for subvalue in value:
+ yield '%s:%s' % (key, subvalue.values()[0])
+ for facet in meta['facets']:
+ key = facet['name']
+ for value in facet['values']:
+ value = value['value']
+ yield '%s:%s' % (key, value)
+
+#raises db.NotFound
+def publication_date(doi):
+ return util.date(get(doi)['records'][0]['publicationDate'])
+
+# raises db.NotFound
+def publication(doi):
+ record = get(doi)['records'][0]
+ if record.has_key('isbn'):
+ return 'isbn:' + record['isbn']
+ elif record.has_key('issn'):
+ return 'issn:' + record['issn']
+ else:
+ raise NotFound('publication', doi)
36 query.py
@@ -0,0 +1,36 @@
+from pyparsing import *
+import math
+
+import db
+import metadata
+import util
+import data
+
+p_key = Word(alphanums)
+p_value = Word(alphanums + '-' + '/' + '_') ^ QuotedString('"', escChar='\\')
+p_pair = Group(p_key + ':' + p_value)
+p_query = delimitedList(p_pair) + LineEnd()
+
+# raises pyparsing.ParseException
+def parse(string):
+ return [''.join(pair) for pair in p_query.parseString(string)]
+
+# raises db.NotFound
+def evaluate(query):
+ for feature in query:
+ histograms = []
+ for doi in db.get('features', feature):
+ histogram = db.get('histograms', doi)
+ pubdate = metadata.publication_date(doi)
+ histogram.group_by(lambda (date): (date-pubdate).days / 30)
+ histograms.append(histogram)
+ summary = data.summary(histograms)
+ summary['feature'] = feature
+ yield summary
+
+# raises db.NotFound
+def fetch(query):
+ summaries = [db.get('summaries', feature) for feature in query]
+ top_downloads = [db.get('top', feature) for feature in query]
+ return (summaries, top_downloads)
+
42 server.py
@@ -0,0 +1,42 @@
+from flask import Flask, render_template, request
+import urllib
+import time
+import datetime
+
+import query
+import db
+import util
+
+app = Flask('Springer Analytics')
+
+app.template_filter('quote')(urllib.quote)
+
+@app.route('/')
+def root():
+ return render_template('examples.html')
+
+def format_date(date):
+ seconds = time.mktime(date.timetuple())
+ return int(seconds * 1000)
+
+def format_summaries(summaries):
+ # flot wants key-value pairs instead of a dict
+ for summary in summaries:
+ for key in summary:
+ if key in ['min', '25%', '50%', '75%', 'max', 'mean']:
+ summary[key] = sorted(summary[key].items())
+
+@app.route('/search')
+def search():
+ try:
+ summaries, top_downloads = list(query.fetch(query.parse(request.args['query'])))
+ format_summaries(summaries)
+ return render_template('results.html', summaries=summaries, top_downloads=top_downloads)
+ except query.ParseException, exc:
+ return render_template('error.html', message=repr(exc))
+ except db.NotFound, exc:
+ return render_template('error.html', message=repr(exc))
+
+if __name__ == '__main__':
+ app.debug = True
+ app.run(port=8000)
653 static/960.css
@@ -0,0 +1,653 @@
+/*
+ 960 Grid System ~ Core CSS.
+ Learn more ~ http://960.gs/
+
+ Licensed under GPL and MIT.
+*/
+
+/*
+ Forces backgrounds to span full width,
+ even if there is horizontal scrolling.
+ Increase this if your layout is wider.
+
+ Note: IE6 works fine without this fix.
+*/
+
+body {
+ min-width: 960px;
+}
+
+/* `Container
+----------------------------------------------------------------------------------------------------*/
+
+.container_12,
+.container_16 {
+ margin-left: auto;
+ margin-right: auto;
+ width: 960px;
+}
+
+/* `Grid >> Global
+----------------------------------------------------------------------------------------------------*/
+
+.grid_1,
+.grid_2,
+.grid_3,
+.grid_4,
+.grid_5,
+.grid_6,
+.grid_7,
+.grid_8,
+.grid_9,
+.grid_10,
+.grid_11,
+.grid_12,
+.grid_13,
+.grid_14,
+.grid_15,
+.grid_16 {
+ display: inline;
+ float: left;
+ margin-left: 10px;
+ margin-right: 10px;
+}
+
+.push_1, .pull_1,
+.push_2, .pull_2,
+.push_3, .pull_3,
+.push_4, .pull_4,
+.push_5, .pull_5,
+.push_6, .pull_6,
+.push_7, .pull_7,
+.push_8, .pull_8,
+.push_9, .pull_9,
+.push_10, .pull_10,
+.push_11, .pull_11,
+.push_12, .pull_12,
+.push_13, .pull_13,
+.push_14, .pull_14,
+.push_15, .pull_15 {
+ position: relative;
+}
+
+.container_12 .grid_3,
+.container_16 .grid_4 {
+ width: 220px;
+}
+
+.container_12 .grid_6,
+.container_16 .grid_8 {
+ width: 460px;
+}
+
+.container_12 .grid_9,
+.container_16 .grid_12 {
+ width: 700px;
+}
+
+.container_12 .grid_12,
+.container_16 .grid_16 {
+ width: 940px;
+}
+
+/* `Grid >> Children (Alpha ~ First, Omega ~ Last)
+----------------------------------------------------------------------------------------------------*/
+
+.alpha {
+ margin-left: 0;
+}
+
+.omega {
+ margin-right: 0;
+}
+
+/* `Grid >> 12 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_12 .grid_1 {
+ width: 60px;
+}
+
+.container_12 .grid_2 {
+ width: 140px;
+}
+
+.container_12 .grid_4 {
+ width: 300px;
+}
+
+.container_12 .grid_5 {
+ width: 380px;
+}
+
+.container_12 .grid_7 {
+ width: 540px;
+}
+
+.container_12 .grid_8 {
+ width: 620px;
+}
+
+.container_12 .grid_10 {
+ width: 780px;
+}
+
+.container_12 .grid_11 {
+ width: 860px;
+}
+
+/* `Grid >> 16 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_16 .grid_1 {
+ width: 40px;
+}
+
+.container_16 .grid_2 {
+ width: 100px;
+}
+
+.container_16 .grid_3 {
+ width: 160px;
+}
+
+.container_16 .grid_5 {
+ width: 280px;
+}
+
+.container_16 .grid_6 {
+ width: 340px;
+}
+
+.container_16 .grid_7 {
+ width: 400px;
+}
+
+.container_16 .grid_9 {
+ width: 520px;
+}
+
+.container_16 .grid_10 {
+ width: 580px;
+}
+
+.container_16 .grid_11 {
+ width: 640px;
+}
+
+.container_16 .grid_13 {
+ width: 760px;
+}
+
+.container_16 .grid_14 {
+ width: 820px;
+}
+
+.container_16 .grid_15 {
+ width: 880px;
+}
+
+/* `Prefix Extra Space >> Global
+----------------------------------------------------------------------------------------------------*/
+
+.container_12 .prefix_3,
+.container_16 .prefix_4 {
+ padding-left: 240px;
+}
+
+.container_12 .prefix_6,
+.container_16 .prefix_8 {
+ padding-left: 480px;
+}
+
+.container_12 .prefix_9,
+.container_16 .prefix_12 {
+ padding-left: 720px;
+}
+
+/* `Prefix Extra Space >> 12 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_12 .prefix_1 {
+ padding-left: 80px;
+}
+
+.container_12 .prefix_2 {
+ padding-left: 160px;
+}
+
+.container_12 .prefix_4 {
+ padding-left: 320px;
+}
+
+.container_12 .prefix_5 {
+ padding-left: 400px;
+}
+
+.container_12 .prefix_7 {
+ padding-left: 560px;
+}
+
+.container_12 .prefix_8 {
+ padding-left: 640px;
+}
+
+.container_12 .prefix_10 {
+ padding-left: 800px;
+}
+
+.container_12 .prefix_11 {
+ padding-left: 880px;
+}
+
+/* `Prefix Extra Space >> 16 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_16 .prefix_1 {
+ padding-left: 60px;
+}
+
+.container_16 .prefix_2 {
+ padding-left: 120px;
+}
+
+.container_16 .prefix_3 {
+ padding-left: 180px;
+}
+
+.container_16 .prefix_5 {
+ padding-left: 300px;
+}
+
+.container_16 .prefix_6 {
+ padding-left: 360px;
+}
+
+.container_16 .prefix_7 {
+ padding-left: 420px;
+}
+
+.container_16 .prefix_9 {
+ padding-left: 540px;
+}
+
+.container_16 .prefix_10 {
+ padding-left: 600px;
+}
+
+.container_16 .prefix_11 {
+ padding-left: 660px;
+}
+
+.container_16 .prefix_13 {
+ padding-left: 780px;
+}
+
+.container_16 .prefix_14 {
+ padding-left: 840px;
+}
+
+.container_16 .prefix_15 {
+ padding-left: 900px;
+}
+
+/* `Suffix Extra Space >> Global
+----------------------------------------------------------------------------------------------------*/
+
+.container_12 .suffix_3,
+.container_16 .suffix_4 {
+ padding-right: 240px;
+}
+
+.container_12 .suffix_6,
+.container_16 .suffix_8 {
+ padding-right: 480px;
+}
+
+.container_12 .suffix_9,
+.container_16 .suffix_12 {
+ padding-right: 720px;
+}
+
+/* `Suffix Extra Space >> 12 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_12 .suffix_1 {
+ padding-right: 80px;
+}
+
+.container_12 .suffix_2 {
+ padding-right: 160px;
+}
+
+.container_12 .suffix_4 {
+ padding-right: 320px;
+}
+
+.container_12 .suffix_5 {
+ padding-right: 400px;
+}
+
+.container_12 .suffix_7 {
+ padding-right: 560px;
+}
+
+.container_12 .suffix_8 {
+ padding-right: 640px;
+}
+
+.container_12 .suffix_10 {
+ padding-right: 800px;
+}
+
+.container_12 .suffix_11 {
+ padding-right: 880px;
+}
+
+/* `Suffix Extra Space >> 16 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_16 .suffix_1 {
+ padding-right: 60px;
+}
+
+.container_16 .suffix_2 {
+ padding-right: 120px;
+}
+
+.container_16 .suffix_3 {
+ padding-right: 180px;
+}
+
+.container_16 .suffix_5 {
+ padding-right: 300px;
+}
+
+.container_16 .suffix_6 {
+ padding-right: 360px;
+}
+
+.container_16 .suffix_7 {
+ padding-right: 420px;
+}
+
+.container_16 .suffix_9 {
+ padding-right: 540px;
+}
+
+.container_16 .suffix_10 {
+ padding-right: 600px;
+}
+
+.container_16 .suffix_11 {
+ padding-right: 660px;
+}
+
+.container_16 .suffix_13 {
+ padding-right: 780px;
+}
+
+.container_16 .suffix_14 {
+ padding-right: 840px;
+}
+
+.container_16 .suffix_15 {
+ padding-right: 900px;
+}
+
+/* `Push Space >> Global
+----------------------------------------------------------------------------------------------------*/
+
+.container_12 .push_3,
+.container_16 .push_4 {
+ left: 240px;
+}
+
+.container_12 .push_6,
+.container_16 .push_8 {
+ left: 480px;
+}
+
+.container_12 .push_9,
+.container_16 .push_12 {
+ left: 720px;
+}
+
+/* `Push Space >> 12 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_12 .push_1 {
+ left: 80px;
+}
+
+.container_12 .push_2 {
+ left: 160px;
+}
+
+.container_12 .push_4 {
+ left: 320px;
+}
+
+.container_12 .push_5 {
+ left: 400px;
+}
+
+.container_12 .push_7 {
+ left: 560px;
+}
+
+.container_12 .push_8 {
+ left: 640px;
+}
+
+.container_12 .push_10 {
+ left: 800px;
+}
+
+.container_12 .push_11 {
+ left: 880px;
+}
+
+/* `Push Space >> 16 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_16 .push_1 {
+ left: 60px;
+}
+
+.container_16 .push_2 {
+ left: 120px;
+}
+
+.container_16 .push_3 {
+ left: 180px;
+}
+
+.container_16 .push_5 {
+ left: 300px;
+}
+
+.container_16 .push_6 {
+ left: 360px;
+}
+
+.container_16 .push_7 {
+ left: 420px;
+}
+
+.container_16 .push_9 {
+ left: 540px;
+}
+
+.container_16 .push_10 {
+ left: 600px;
+}
+
+.container_16 .push_11 {
+ left: 660px;
+}
+
+.container_16 .push_13 {
+ left: 780px;
+}
+
+.container_16 .push_14 {
+ left: 840px;
+}
+
+.container_16 .push_15 {
+ left: 900px;
+}
+
+/* `Pull Space >> Global
+----------------------------------------------------------------------------------------------------*/
+
+.container_12 .pull_3,
+.container_16 .pull_4 {
+ left: -240px;
+}
+
+.container_12 .pull_6,
+.container_16 .pull_8 {
+ left: -480px;
+}
+
+.container_12 .pull_9,
+.container_16 .pull_12 {
+ left: -720px;
+}
+
+/* `Pull Space >> 12 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_12 .pull_1 {
+ left: -80px;
+}
+
+.container_12 .pull_2 {
+ left: -160px;
+}
+
+.container_12 .pull_4 {
+ left: -320px;
+}
+
+.container_12 .pull_5 {
+ left: -400px;
+}
+
+.container_12 .pull_7 {
+ left: -560px;
+}
+
+.container_12 .pull_8 {
+ left: -640px;
+}
+
+.container_12 .pull_10 {
+ left: -800px;
+}
+
+.container_12 .pull_11 {
+ left: -880px;
+}
+
+/* `Pull Space >> 16 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_16 .pull_1 {
+ left: -60px;
+}
+
+.container_16 .pull_2 {
+ left: -120px;
+}
+
+.container_16 .pull_3 {
+ left: -180px;
+}
+
+.container_16 .pull_5 {
+ left: -300px;
+}
+
+.container_16 .pull_6 {
+ left: -360px;
+}
+
+.container_16 .pull_7 {
+ left: -420px;
+}
+
+.container_16 .pull_9 {
+ left: -540px;
+}
+
+.container_16 .pull_10 {
+ left: -600px;
+}
+
+.container_16 .pull_11 {
+ left: -660px;
+}
+
+.container_16 .pull_13 {
+ left: -780px;
+}
+
+.container_16 .pull_14 {
+ left: -840px;
+}
+
+.container_16 .pull_15 {
+ left: -900px;
+}
+
+/* `Clear Floated Elements
+----------------------------------------------------------------------------------------------------*/
+
+/* http://sonspring.com/journal/clearing-floats */
+
+.clear {
+ clear: both;
+ display: block;
+ overflow: hidden;
+ visibility: hidden;
+ width: 0;
+ height: 0;
+}
+
+/* http://www.yuiblog.com/blog/2010/09/27/clearfix-reloaded-overflowhidden-demystified */
+
+.clearfix:before,
+.clearfix:after,
+.container_12:before,
+.container_12:after,
+.container_16:before,
+.container_16:after {
+ content: '.';
+ display: block;
+ overflow: hidden;
+ visibility: hidden;
+ font-size: 0;
+ line-height: 0;
+ width: 0;
+ height: 0;
+}
+
+.clearfix:after,
+.container_12:after,
+.container_16:after {
+ clear: both;
+}
+
+/*
+ The following zoom:1 rule is specifically for IE6 + IE7.
+ Move to separate stylesheet if invalid CSS is a problem.
+*/
+
+.clearfix,
+.container_12,
+.container_16 {
+ zoom: 1;
+}
29 static/analytics.css
@@ -0,0 +1,29 @@
+html, body {
+ height: 100%;
+}
+
+#query, #submit {
+ width: 100%;
+}
+
+.graph_holder {
+ padding-left: 1%;
+ padding-right: 1%;
+ padding-top: 1%;
+ padding-bottom: 2%;
+ width: 98%;
+ height: 25%;
+}
+
+.feature {
+ text-align: center;
+}
+
+.graph {
+ width: 100%;
+ height: 100%;
+}
+
+.subpane {
+ padding-top: 1em;
+}
168 static/downloads_graph.js
@@ -0,0 +1,168 @@
+var xaxis = "log";
+
+function plotAll() {
+ var plots = [];
+ for (i in summaries) {
+ $(".feature")[i].textContent = summaries[i]['feature'];
+ plot = plotDownloadsGraph($(".graph")[i], summaries[i]);
+ plots.push(plot);
+ };
+ enableCrosshair(plots, $(".graph"));
+ linkPlots(plots, $(".graph"));
+}
+
+function plotDownloadsGraph(graph, downloads) {
+ var dataset =
+ [
+ { label: 'max =', id: 'max', data: downloads['max'], lines: { show: true, lineWidth: 0, fill: 0.3 }, color: "rgba(50,50,255,0.3)", fillBetween: '75%' },
+ { label: '3/4 =', id: '75%', data: downloads['75%'], lines: { show: true, lineWidth: 0, fill: 0.6 }, color: "rgba(50,50,255,0.6)", fillBetween: '25%' },
+ { label: '2/4 =', id: '50%', data: downloads['50%'], lines: { show: true, lineWidth: 0.5, shadowSize: 0 }, color: "rgb(0,0,0)"},
+ { label: '1/4 =', id: '25%', data: downloads['25%'], lines: { show: true, lineWidth: 0, fill: 0.3 }, color: "rgba(50,50,255,0.6)", fillBetween: 'min' },
+ { label: 'min =', id: 'min', data: downloads['min'], lines: { show: true, lineWidth: 0, fill: 0.0 }, color: "rgba(50,50,255,0.3)" },
+ { label: 'mean =', data: downloads['mean'], lines: { show: true }, color: "rgb(255,100,100)" }
+ ];
+
+ var transform;
+ var inverseTransform;
+ var ticks;
+ if (xaxis == "lin") {
+ transform = function (v) {return v};
+ inverseTransform = function (v) {return v};
+ ticks = null;
+ }
+ else if (xaxis == "log") {
+ transform = function (v) { return Math.log(1 + v); };
+ inverseTransform = function (v) { return Math.exp(v) - 1; };
+ ticks =
+ function (axis) {
+ var res = [0];
+ var tick = 1;
+ while (tick <= axis.max) {
+ if (tick >= axis.min) res.push(tick);
+ tick *= 10
+ }
+ return res
+ };
+ }
+
+ var plot = $.plot($(graph),
+ dataset,
+ { xaxis: { tickDecimals: 0 },
+ yaxis: {
+ transform: transform,
+ inverseTransform: inverseTransform,
+ ticks: ticks,
+ },
+ legend: { position: 'ne' },
+ grid: { hoverable: true, autoHighlight: false },
+ crosshair: {mode: 'x'}
+ });
+
+ function showTooltip(x, y, contents) {
+ $('<div id="tooltip">' + contents + '</div>').css(
+ {
+ position: 'absolute',
+ display: 'none',
+ top: y + 5,
+ left: x + 5,
+ border: '1px solid #fdd',
+ padding: '2px',
+ 'background-color': '#fee',
+ opacity: 0.80
+ }).appendTo("body").fadeIn(200);
+ }
+
+ return plot
+};
+
+function enableCrosshair(plots, graphs) {
+ var plot = plots[0];
+
+ function updateLegend(event, pos, item) {
+ var axes = plot.getAxes();
+ if (pos.x < axes.xaxis.min || pos.x > axes.xaxis.max ||
+ pos.y < axes.yaxis.min || pos.y > axes.yaxis.max)
+ return;
+
+ var x = Math.round(pos.x);
+
+ for (var i in plots) {
+ plots[i].lockCrosshair({x:x, y:0});
+
+ var dataset = plots[i].getData();
+ var series0 = dataset[0];
+ var index = null; // index of x in dataset
+ for (var j in series0.data)
+ if (series0.data[j][0] == x) {
+ index = j;
+ break;
+ }
+ $.each(plots[i].getData(),
+ function(j, series) {
+ var y = 0.0;
+ if (index != null) y = series.data[index][1];
+ $(graphs[i]).find(".legendLabel").eq(j).text(series.label.replace(/=.*/, "= " + y.toFixed(2)));
+ }
+ );
+ }
+ }
+
+ graphs.bind("plothover", updateLegend);
+}
+
+function linkPlots(plots, graphs) {
+
+ var xmin = Math.min.apply(Math, $.map(plots, function(plot, _) {return plot.getAxes().xaxis.min;}));
+ var xmax = Math.max.apply(Math, $.map(plots, function(plot, _) {return plot.getAxes().xaxis.max;}));
+ var ymin = 0;
+ var ymax = Math.max.apply(Math, $.map(plots, function(plot, _) {return plot.getAxes().yaxis.max;}));
+
+ $.each(plots,
+ function(_, plot) {
+ var axes = plot.getAxes();
+ axes.xaxis.options.min = xmin;
+ axes.xaxis.options.max = xmax;
+ axes.yaxis.options.min = ymin;
+ axes.yaxis.options.max = ymax;
+ plot.setupGrid();
+ plot.draw();
+ }
+ );
+
+ var downX = null;
+
+ graphs.bind('mousedown',
+ function(event) {
+ event.preventDefault();
+ downX = event.pageX;
+ document.body.style.cursor = 'move';
+ });
+
+ graphs.bind('mouseup',
+ function(event) {
+ event.preventDefault();
+ $.each(plots,
+ function(_, plot) {
+ var left = downX - event.pageX;
+ plot.pan({left: left, top: 0});
+ plot.setupGrid();
+ plot.draw();
+ });
+ document.body.style.cursor = 'default';
+ });
+
+ graphs.bind('mousewheel',
+ function(event, delta) {
+ event.preventDefault();
+ $.each(plots,
+ function(_, plot) {
+ var axes = plot.getAxes();
+ var xmin = axes.xaxis.options.min;
+ var xmax = axes.xaxis.options.max;
+ axes.xaxis.options.min = xmin + 0.05 * delta * (xmax - xmin);
+ axes.xaxis.options.max = xmax - 0.05 * delta * (xmax - xmin);
+ plot.setupGrid();
+ plot.draw();
+ });
+ });
+}
924 static/excanvas.js
@@ -0,0 +1,924 @@
+// Copyright 2006 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+// Known Issues:
+//
+// * Patterns are not implemented.
+// * Radial gradient are not implemented. The VML version of these look very
+// different from the canvas one.
+// * Clipping paths are not implemented.
+// * Coordsize. The width and height attribute have higher priority than the
+// width and height style values which isn't correct.
+// * Painting mode isn't implemented.
+// * Canvas width/height should is using content-box by default. IE in
+// Quirks mode will draw the canvas using border-box. Either change your
+// doctype to HTML5
+// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
+// or use Box Sizing Behavior from WebFX
+// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
+// * Non uniform scaling does not correctly scale strokes.
+// * Optimize. There is always room for speed improvements.
+
+// Only add this code if we do not already have a canvas implementation
+if (!document.createElement('canvas').getContext) {
+
+(function() {
+
+ // alias some functions to make (compiled) code shorter
+ var m = Math;
+ var mr = m.round;
+ var ms = m.sin;
+ var mc = m.cos;
+ var abs = m.abs;
+ var sqrt = m.sqrt;
+
+ // this is used for sub pixel precision
+ var Z = 10;
+ var Z2 = Z / 2;
+
+ /**
+ * This funtion is assigned to the <canvas> elements as element.getContext().
+ * @this {HTMLElement}
+ * @return {CanvasRenderingContext2D_}
+ */
+ function getContext() {
+ return this.context_ ||
+ (this.context_ = new CanvasRenderingContext2D_(this));
+ }
+
+ var slice = Array.prototype.slice;
+
+ /**
+ * Binds a function to an object. The returned function will always use the
+ * passed in {@code obj} as {@code this}.
+ *
+ * Example:
+ *
+ * g = bind(f, obj, a, b)
+ * g(c, d) // will do f.call(obj, a, b, c, d)
+ *
+ * @param {Function} f The function to bind the object to
+ * @param {Object} obj The object that should act as this when the function
+ * is called
+ * @param {*} var_args Rest arguments that will be used as the initial
+ * arguments when the function is called
+ * @return {Function} A new function that has bound this
+ */
+ function bind(f, obj, var_args) {
+ var a = slice.call(arguments, 2);
+ return function() {
+ return f.apply(obj, a.concat(slice.call(arguments)));
+ };
+ }
+
+ var G_vmlCanvasManager_ = {
+ init: function(opt_doc) {
+ if (/MSIE/.test(navigator.userAgent) && !window.opera) {
+ var doc = opt_doc || document;
+ // Create a dummy element so that IE will allow canvas elements to be
+ // recognized.
+ doc.createElement('canvas');
+ doc.attachEvent('onreadystatechange', bind(this.init_, this, doc));
+ }
+ },
+
+ init_: function(doc) {
+ // create xmlns
+ if (!doc.namespaces['g_vml_']) {
+ doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml',
+ '#default#VML');
+
+ }
+ if (!doc.namespaces['g_o_']) {
+ doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office',
+ '#default#VML');
+ }
+
+ // Setup default CSS. Only add one style sheet per document
+ if (!doc.styleSheets['ex_canvas_']) {
+ var ss = doc.createStyleSheet();
+ ss.owningElement.id = 'ex_canvas_';
+ ss.cssText = 'canvas{display:inline-block;overflow:hidden;' +
+ // default size is 300x150 in Gecko and Opera
+ 'text-align:left;width:300px;height:150px}' +
+ 'g_vml_\\:*{behavior:url(#default#VML)}' +
+ 'g_o_\\:*{behavior:url(#default#VML)}';
+
+ }
+
+ // find all canvas elements
+ var els = doc.getElementsByTagName('canvas');
+ for (var i = 0; i < els.length; i++) {
+ this.initElement(els[i]);
+ }
+ },
+
+ /**
+ * Public initializes a canvas element so that it can be used as canvas
+ * element from now on. This is called automatically before the page is
+ * loaded but if you are creating elements using createElement you need to
+ * make sure this is called on the element.
+ * @param {HTMLElement} el The canvas element to initialize.
+ * @return {HTMLElement} the element that was created.
+ */
+ initElement: function(el) {
+ if (!el.getContext) {
+
+ el.getContext = getContext;
+
+ // Remove fallback content. There is no way to hide text nodes so we
+ // just remove all childNodes. We could hide all elements and remove
+ // text nodes but who really cares about the fallback content.
+ el.innerHTML = '';
+
+ // do not use inline function because that will leak memory
+ el.attachEvent('onpropertychange', onPropertyChange);
+ el.attachEvent('onresize', onResize);
+
+ var attrs = el.attributes;
+ if (attrs.width && attrs.width.specified) {
+ // TODO: use runtimeStyle and coordsize
+ // el.getContext().setWidth_(attrs.width.nodeValue);
+ el.style.width = attrs.width.nodeValue + 'px';
+ } else {
+ el.width = el.clientWidth;
+ }
+ if (attrs.height && attrs.height.specified) {
+ // TODO: use runtimeStyle and coordsize
+ // el.getContext().setHeight_(attrs.height.nodeValue);
+ el.style.height = attrs.height.nodeValue + 'px';
+ } else {
+ el.height = el.clientHeight;
+ }
+ //el.getContext().setCoordsize_()
+ }
+ return el;
+ }
+ };
+
+ function onPropertyChange(e) {
+ var el = e.srcElement;
+
+ switch (e.propertyName) {
+ case 'width':
+ el.style.width = el.attributes.width.nodeValue + 'px';
+ el.getContext().clearRect();
+ break;
+ case 'height':
+ el.style.height = el.attributes.height.nodeValue + 'px';
+ el.getContext().clearRect();
+ break;
+ }
+ }
+
+ function onResize(e) {
+ var el = e.srcElement;
+ if (el.firstChild) {
+ el.firstChild.style.width = el.clientWidth + 'px';
+ el.firstChild.style.height = el.clientHeight + 'px';
+ }
+ }
+
+ G_vmlCanvasManager_.init();
+
+ // precompute "00" to "FF"
+ var dec2hex = [];
+ for (var i = 0; i < 16; i++) {
+ for (var j = 0; j < 16; j++) {
+ dec2hex[i * 16 + j] = i.toString(16) + j.toString(16);
+ }
+ }
+
+ function createMatrixIdentity() {
+ return [
+ [1, 0, 0],
+ [0, 1, 0],
+ [0, 0, 1]
+ ];
+ }
+
+ function matrixMultiply(m1, m2) {
+ var result = createMatrixIdentity();
+
+ for (var x = 0; x < 3; x++) {
+ for (var y = 0; y < 3; y++) {
+ var sum = 0;
+
+ for (var z = 0; z < 3; z++) {
+ sum += m1[x][z] * m2[z][y];
+ }
+
+ result[x][y] = sum;
+ }
+ }
+ return result;
+ }
+
+ function copyState(o1, o2) {
+ o2.fillStyle = o1.fillStyle;
+ o2.lineCap = o1.lineCap;
+ o2.lineJoin = o1.lineJoin;
+ o2.lineWidth = o1.lineWidth;
+ o2.miterLimit = o1.miterLimit;
+ o2.shadowBlur = o1.shadowBlur;
+ o2.shadowColor = o1.shadowColor;
+ o2.shadowOffsetX = o1.shadowOffsetX;
+ o2.shadowOffsetY = o1.shadowOffsetY;
+ o2.strokeStyle = o1.strokeStyle;
+ o2.globalAlpha = o1.globalAlpha;
+ o2.arcScaleX_ = o1.arcScaleX_;
+ o2.arcScaleY_ = o1.arcScaleY_;
+ o2.lineScale_ = o1.lineScale_;
+ }
+
+ function processStyle(styleString) {
+ var str, alpha = 1;
+
+ styleString = String(styleString);
+ if (styleString.substring(0, 3) == 'rgb') {
+ var start = styleString.indexOf('(', 3);
+ var end = styleString.indexOf(')', start + 1);
+ var guts = styleString.substring(start + 1, end).split(',');
+
+ str = '#';
+ for (var i = 0; i < 3; i++) {
+ str += dec2hex[Number(guts[i])];
+ }
+
+ if (guts.length == 4 && styleString.substr(3, 1) == 'a') {
+ alpha = guts[3];
+ }
+ } else {
+ str = styleString;
+ }
+
+ return {color: str, alpha: alpha};
+ }
+
+ function processLineCap(lineCap) {
+ switch (lineCap) {
+ case 'butt':
+ return 'flat';
+ case 'round':
+ return 'round';
+ case 'square':
+ default:
+ return 'square';
+ }
+ }
+
+ /**
+ * This class implements CanvasRenderingContext2D interface as described by
+ * the WHATWG.
+ * @param {HTMLElement} surfaceElement The element that the 2D context should
+ * be associated with
+ */
+ function CanvasRenderingContext2D_(surfaceElement) {
+ this.m_ = createMatrixIdentity();
+
+ this.mStack_ = [];
+ this.aStack_ = [];
+ this.currentPath_ = [];
+
+ // Canvas context properties
+ this.strokeStyle = '#000';
+ this.fillStyle = '#000';
+
+ this.lineWidth = 1;
+ this.lineJoin = 'miter';
+ this.lineCap = 'butt';
+ this.miterLimit = Z * 1;
+ this.globalAlpha = 1;
+ this.canvas = surfaceElement;
+
+ var el = surfaceElement.ownerDocument.createElement('div');
+ el.style.width = surfaceElement.clientWidth + 'px';
+ el.style.height = surfaceElement.clientHeight + 'px';
+ el.style.overflow = 'hidden';
+ el.style.position = 'absolute';
+ surfaceElement.appendChild(el);
+
+ this.element_ = el;
+ this.arcScaleX_ = 1;
+ this.arcScaleY_ = 1;
+ this.lineScale_ = 1;
+ }
+
+ var contextPrototype = CanvasRenderingContext2D_.prototype;
+ contextPrototype.clearRect = function() {
+ this.element_.innerHTML = '';
+ };
+
+ contextPrototype.beginPath = function() {
+ // TODO: Branch current matrix so that save/restore has no effect
+ // as per safari docs.
+ this.currentPath_ = [];
+ };
+
+ contextPrototype.moveTo = function(aX, aY) {
+ var p = this.getCoords_(aX, aY);
+ this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y});
+ this.currentX_ = p.x;
+ this.currentY_ = p.y;
+ };
+
+ contextPrototype.lineTo = function(aX, aY) {
+ var p = this.getCoords_(aX, aY);
+ this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y});
+
+ this.currentX_ = p.x;
+ this.currentY_ = p.y;
+ };
+
+ contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,
+ aCP2x, aCP2y,
+ aX, aY) {
+ var p = this.getCoords_(aX, aY);
+ var cp1 = this.getCoords_(aCP1x, aCP1y);
+ var cp2 = this.getCoords_(aCP2x, aCP2y);
+ bezierCurveTo(this, cp1, cp2, p);
+ };
+
+ // Helper function that takes the already fixed cordinates.
+ function bezierCurveTo(self, cp1, cp2, p) {
+ self.currentPath_.push({
+ type: 'bezierCurveTo',
+ cp1x: cp1.x,
+ cp1y: cp1.y,
+ cp2x: cp2.x,
+ cp2y: cp2.y,
+ x: p.x,
+ y: p.y
+ });
+ self.currentX_ = p.x;
+ self.currentY_ = p.y;
+ }
+
+ contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
+ // the following is lifted almost directly from
+ // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes
+
+ var cp = this.getCoords_(aCPx, aCPy);
+ var p = this.getCoords_(aX, aY);
+
+ var cp1 = {
+ x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_),
+ y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_)
+ };
+ var cp2 = {
+ x: cp1.x + (p.x - this.currentX_) / 3.0,
+ y: cp1.y + (p.y - this.currentY_) / 3.0
+ };
+
+ bezierCurveTo(this, cp1, cp2, p);
+ };
+
+ contextPrototype.arc = function(aX, aY, aRadius,
+ aStartAngle, aEndAngle, aClockwise) {
+ aRadius *= Z;
+ var arcType = aClockwise ? 'at' : 'wa';
+
+ var xStart = aX + mc(aStartAngle) * aRadius - Z2;
+ var yStart = aY + ms(aStartAngle) * aRadius - Z2;
+
+ var xEnd = aX + mc(aEndAngle) * aRadius - Z2;
+ var yEnd = aY + ms(aEndAngle) * aRadius - Z2;
+
+ // IE won't render arches drawn counter clockwise if xStart == xEnd.
+ if (xStart == xEnd && !aClockwise) {
+ xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something
+ // that can be represented in binary
+ }
+
+ var p = this.getCoords_(aX, aY);
+ var pStart = this.getCoords_(xStart, yStart);
+ var pEnd = this.getCoords_(xEnd, yEnd);
+
+ this.currentPath_.push({type: arcType,
+ x: p.x,
+ y: p.y,
+ radius: aRadius,
+ xStart: pStart.x,
+ yStart: pStart.y,
+ xEnd: pEnd.x,
+ yEnd: pEnd.y});
+
+ };
+
+ contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
+ this.moveTo(aX, aY);
+ this.lineTo(aX + aWidth, aY);
+ this.lineTo(aX + aWidth, aY + aHeight);
+ this.lineTo(aX, aY + aHeight);
+ this.closePath();
+ };
+
+ contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
+ var oldPath = this.currentPath_;
+ this.beginPath();
+
+ this.moveTo(aX, aY);
+ this.lineTo(aX + aWidth, aY);
+ this.lineTo(aX + aWidth, aY + aHeight);
+ this.lineTo(aX, aY + aHeight);
+ this.closePath();
+ this.stroke();
+
+ this.currentPath_ = oldPath;
+ };
+
+ contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
+ var oldPath = this.currentPath_;
+ this.beginPath();
+
+ this.moveTo(aX, aY);
+ this.lineTo(aX + aWidth, aY);
+ this.lineTo(aX + aWidth, aY + aHeight);
+ this.lineTo(aX, aY + aHeight);
+ this.closePath();
+ this.fill();
+
+ this.currentPath_ = oldPath;
+ };
+
+ contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
+ var gradient = new CanvasGradient_('gradient');
+ gradient.x0_ = aX0;
+ gradient.y0_ = aY0;
+ gradient.x1_ = aX1;
+ gradient.y1_ = aY1;
+ return gradient;
+ };
+
+ contextPrototype.createRadialGradient = function(aX0, aY0, aR0,
+ aX1, aY1, aR1) {
+ var gradient = new CanvasGradient_('gradientradial');
+ gradient.x0_ = aX0;
+ gradient.y0_ = aY0;
+ gradient.r0_ = aR0;
+ gradient.x1_ = aX1;
+ gradient.y1_ = aY1;
+ gradient.r1_ = aR1;
+ return gradient;
+ };
+
+ contextPrototype.drawImage = function(image, var_args) {
+ var dx, dy, dw, dh, sx, sy, sw, sh;
+
+ // to find the original width we overide the width and height
+ var oldRuntimeWidth = image.runtimeStyle.width;
+ var oldRuntimeHeight = image.runtimeStyle.height;
+ image.runtimeStyle.width = 'auto';
+ image.runtimeStyle.height = 'auto';
+
+ // get the original size
+ var w = image.width;
+ var h = image.height;
+
+ // and remove overides
+ image.runtimeStyle.width = oldRuntimeWidth;
+ image.runtimeStyle.height = oldRuntimeHeight;
+
+ if (arguments.length == 3) {
+ dx = arguments[1];
+ dy = arguments[2];
+ sx = sy = 0;
+ sw = dw = w;
+ sh = dh = h;
+ } else if (arguments.length == 5) {
+ dx = arguments[1];
+ dy = arguments[2];
+ dw = arguments[3];
+ dh = arguments[4];
+ sx = sy = 0;
+ sw = w;
+ sh = h;
+ } else if (arguments.length == 9) {
+ sx = arguments[1];
+ sy = arguments[2];
+ sw = arguments[3];
+ sh = arguments[4];
+ dx = arguments[5];
+ dy = arguments[6];
+ dw = arguments[7];
+ dh = arguments[8];
+ } else {
+ throw Error('Invalid number of arguments');
+ }
+
+ var d = this.getCoords_(dx, dy);
+
+ var w2 = sw / 2;
+ var h2 = sh / 2;
+
+ var vmlStr = [];
+
+ var W = 10;
+ var H = 10;
+
+ // For some reason that I've now forgotten, using divs didn't work
+ vmlStr.push(' <g_vml_:group',
+ ' coordsize="', Z * W, ',', Z * H, '"',
+ ' coordorigin="0,0"' ,
+ ' style="width:', W, 'px;height:', H, 'px;position:absolute;');
+
+ // If filters are necessary (rotation exists), create them
+ // filters are bog-slow, so only create them if abbsolutely necessary
+ // The following check doesn't account for skews (which don't exist
+ // in the canvas spec (yet) anyway.
+
+ if (this.m_[0][0] != 1 || this.m_[0][1]) {
+ var filter = [];
+
+ // Note the 12/21 reversal
+ filter.push('M11=', this.m_[0][0], ',',
+ 'M12=', this.m_[1][0], ',',
+ 'M21=', this.m_[0][1], ',',
+ 'M22=', this.m_[1][1], ',',
+ 'Dx=', mr(d.x / Z), ',',
+ 'Dy=', mr(d.y / Z), '');
+
+ // Bounding box calculation (need to minimize displayed area so that
+ // filters don't waste time on unused pixels.
+ var max = d;
+ var c2 = this.getCoords_(dx + dw, dy);
+ var c3 = this.getCoords_(dx, dy + dh);
+ var c4 = this.getCoords_(dx + dw, dy + dh);
+
+ max.x = m.max(max.x, c2.x, c3.x, c4.x);
+ max.y = m.max(max.y, c2.y, c3.y, c4.y);
+
+ vmlStr.push('padding:0 ', mr(max.x / Z), 'px ', mr(max.y / Z),
+ 'px 0;filter:progid:DXImageTransform.Microsoft.Matrix(',
+ filter.join(''), ", sizingmethod='clip');")
+ } else {
+ vmlStr.push('top:', mr(d.y / Z), 'px;left:', mr(d.x / Z), 'px;');
+ }
+
+ vmlStr.push(' ">' ,
+ '<g_vml_:image src="', image.src, '"',
+ ' style="width:', Z * dw, 'px;',
+ ' height:', Z * dh, 'px;"',
+ ' cropleft="', sx / w, '"',
+ ' croptop="', sy / h, '"',
+ ' cropright="', (w - sx - sw) / w, '"',
+ ' cropbottom="', (h - sy - sh) / h, '"',
+ ' />',
+ '</g_vml_:group>');
+
+ this.element_.insertAdjacentHTML('BeforeEnd',
+ vmlStr.join(''));
+ };
+
+ contextPrototype.stroke = function(aFill) {
+ var lineStr = [];
+ var lineOpen = false;
+ var a = processStyle(aFill ? this.fillStyle : this.strokeStyle);
+ var color = a.color;
+ var opacity = a.alpha * this.globalAlpha;
+
+ var W = 10;
+ var H = 10;
+
+ lineStr.push('<g_vml_:shape',
+ ' filled="', !!aFill, '"',
+ ' style="position:absolute;width:', W, 'px;height:', H, 'px;"',
+ ' coordorigin="0 0" coordsize="', Z * W, ' ', Z * H, '"',
+ ' stroked="', !aFill, '"',
+ ' path="');
+
+ var newSeq = false;
+ var min = {x: null, y: null};
+ var max = {x: null, y: null};
+
+ for (var i = 0; i < this.currentPath_.length; i++) {
+ var p = this.currentPath_[i];
+ var c;
+
+ switch (p.type) {
+ case 'moveTo':
+ c = p;
+ lineStr.push(' m ', mr(p.x), ',', mr(p.y));
+ break;
+ case 'lineTo':
+ lineStr.push(' l ', mr(p.x), ',', mr(p.y));
+ break;
+ case 'close':
+ lineStr.push(' x ');
+ p = null;
+ break;
+ case 'bezierCurveTo':
+ lineStr.push(' c ',
+ mr(p.cp1x), ',', mr(p.cp1y), ',',
+ mr(p.cp2x), ',', mr(p.cp2y), ',',
+ mr(p.x), ',', mr(p.y));
+ break;
+ case 'at':
+ case 'wa':
+ lineStr.push(' ', p.type, ' ',
+ mr(p.x - this.arcScaleX_ * p.radius), ',',
+ mr(p.y - this.arcScaleY_ * p.radius), ' ',
+ mr(p.x + this.arcScaleX_ * p.radius), ',',
+ mr(p.y + this.arcScaleY_ * p.radius), ' ',
+ mr(p.xStart), ',', mr(p.yStart), ' ',
+ mr(p.xEnd), ',', mr(p.yEnd));
+ break;
+ }
+
+
+ // TODO: Following is broken for curves due to
+ // move to proper paths.
+
+ // Figure out dimensions so we can do gradient fills
+ // properly
+ if (p) {
+ if (min.x == null || p.x < min.x) {
+ min.x = p.x;
+ }
+ if (max.x == null || p.x > max.x) {
+ max.x = p.x;
+ }
+ if (min.y == null || p.y < min.y) {
+ min.y = p.y;
+ }
+ if (max.y == null || p.y > max.y) {
+ max.y = p.y;
+ }
+ }
+ }
+ lineStr.push(' ">');
+
+ if (!aFill) {
+ var lineWidth = this.lineScale_ * this.lineWidth;
+
+ // VML cannot correctly render a line if the width is less than 1px.
+ // In that case, we dilute the color to make the line look thinner.
+ if (lineWidth < 1) {
+ opacity *= lineWidth;
+ }
+
+ lineStr.push(
+ '<g_vml_:stroke',
+ ' opacity="', opacity, '"',
+ ' joinstyle="', this.lineJoin, '"',
+ ' miterlimit="', this.miterLimit, '"',
+ ' endcap="', processLineCap(this.lineCap), '"',
+ ' weight="', lineWidth, 'px"',
+ ' color="', color, '" />'
+ );
+ } else if (typeof this.fillStyle == 'object') {
+ var fillStyle = this.fillStyle;
+ var angle = 0;
+ var focus = {x: 0, y: 0};
+
+ // additional offset
+ var shift = 0;
+ // scale factor for offset
+ var expansion = 1;
+
+ if (fillStyle.type_ == 'gradient') {
+ var x0 = fillStyle.x0_ / this.arcScaleX_;
+ var y0 = fillStyle.y0_ / this.arcScaleY_;
+ var x1 = fillStyle.x1_ / this.arcScaleX_;
+ var y1 = fillStyle.y1_ / this.arcScaleY_;
+ var p0 = this.getCoords_(x0, y0);
+ var p1 = this.getCoords_(x1, y1);
+ var dx = p1.x - p0.x;
+ var dy = p1.y - p0.y;
+ angle = Math.atan2(dx, dy) * 180 / Math.PI;
+
+ // The angle should be a non-negative number.
+ if (angle < 0) {
+ angle += 360;
+ }
+
+ // Very small angles produce an unexpected result because they are
+ // converted to a scientific notation string.
+ if (angle < 1e-6) {
+ angle = 0;
+ }
+ } else {
+ var p0 = this.getCoords_(fillStyle.x0_, fillStyle.y0_);
+ var width = max.x - min.x;
+ var height = max.y - min.y;
+ focus = {
+ x: (p0.x - min.x) / width,
+ y: (p0.y - min.y) / height
+ };
+
+ width /= this.arcScaleX_ * Z;
+ height /= this.arcScaleY_ * Z;
+ var dimension = m.max(width, height);
+ shift = 2 * fillStyle.r0_ / dimension;
+ expansion = 2 * fillStyle.r1_ / dimension - shift;
+ }
+
+ // We need to sort the color stops in ascending order by offset,
+ // otherwise IE won't interpret it correctly.
+ var stops = fillStyle.colors_;
+ stops.sort(function(cs1, cs2) {
+ return cs1.offset - cs2.offset;
+ });
+
+ var length = stops.length;
+ var color1 = stops[0].color;
+ var color2 = stops[length - 1].color;
+ var opacity1 = stops[0].alpha * this.globalAlpha;
+ var opacity2 = stops[length - 1].alpha * this.globalAlpha;
+
+ var colors = [];
+ for (var i = 0; i < length; i++) {
+ var stop = stops[i];
+ colors.push(stop.offset * expansion + shift + ' ' + stop.color);
+ }
+
+ // When colors attribute is used, the meanings of opacity and o:opacity2
+ // are reversed.
+ lineStr.push('<g_vml_:fill type="', fillStyle.type_, '"',
+ ' method="none" focus="100%"',
+ ' color="', color1, '"',
+ ' color2="', color2, '"',
+ ' colors="', colors.join(','), '"',
+ ' opacity="', opacity2, '"',
+ ' g_o_:opacity2="', opacity1, '"',
+ ' angle="', angle, '"',
+ ' focusposition="', focus.x, ',', focus.y, '" />');
+ } else {
+ lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity,
+ '" />');
+ }
+
+ lineStr.push('</g_vml_:shape>');
+
+ this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));
+ };
+
+ contextPrototype.fill = function() {
+ this.stroke(true);
+ }
+
+ contextPrototype.closePath = function() {
+ this.currentPath_.push({type: 'close'});
+ };
+
+ /**
+ * @private
+ */
+ contextPrototype.getCoords_ = function(aX, aY) {
+ var m = this.m_;
+ return {
+ x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2,
+ y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2
+ }
+ };
+
+ contextPrototype.save = function() {
+ var o = {};
+ copyState(this, o);
+ this.aStack_.push(o);
+ this.mStack_.push(this.m_);
+ this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);
+ };
+
+ contextPrototype.restore = function() {
+ copyState(this.aStack_.pop(), this);
+ this.m_ = this.mStack_.pop();
+ };
+
+ function matrixIsFinite(m) {
+ for (var j = 0; j < 3; j++) {
+ for (var k = 0; k < 2; k++) {
+ if (!isFinite(m[j][k]) || isNaN(m[j][k])) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ function setM(ctx, m, updateLineScale) {
+ if (!matrixIsFinite(m)) {
+ return;
+ }
+ ctx.m_ = m;
+
+ if (updateLineScale) {
+ // Get the line scale.
+ // Determinant of this.m_ means how much the area is enlarged by the
+ // transformation. So its square root can be used as a scale factor
+ // for width.
+ var det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
+ ctx.lineScale_ = sqrt(abs(det));
+ }
+ }
+
+ contextPrototype.translate = function(aX, aY) {
+ var m1 = [
+ [1, 0, 0],
+ [0, 1, 0],
+ [aX, aY, 1]
+ ];
+
+ setM(this, matrixMultiply(m1, this.m_), false);
+ };
+
+ contextPrototype.rotate = function(aRot) {
+ var c = mc(aRot);
+ var s = ms(aRot);
+
+ var m1 = [
+ [c, s, 0],
+ [-s, c, 0],
+ [0, 0, 1]
+ ];
+
+ setM(this, matrixMultiply(m1, this.m_), false);
+ };
+
+ contextPrototype.scale = function(aX, aY) {
+ this.arcScaleX_ *= aX;
+ this.arcScaleY_ *= aY;
+ var m1 = [
+ [aX, 0, 0],
+ [0, aY, 0],
+ [0, 0, 1]
+ ];
+
+ setM(this, matrixMultiply(m1, this.m_), true);
+ };
+
+ contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) {
+ var m1 = [
+ [m11, m12, 0],
+ [m21, m22, 0],
+ [dx, dy, 1]
+ ];
+
+ setM(this, matrixMultiply(m1, this.m_), true);
+ };
+
+ contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) {
+ var m = [
+ [m11, m12, 0],
+ [m21, m22, 0],
+ [dx, dy, 1]
+ ];
+
+ setM(this, m, true);
+ };
+
+ /******** STUBS ********/
+ contextPrototype.clip = function() {
+ // TODO: Implement
+ };
+
+ contextPrototype.arcTo = function() {
+ // TODO: Implement
+ };
+
+ contextPrototype.createPattern = function() {
+ return new CanvasPattern_;
+ };
+
+ // Gradient / Pattern Stubs
+ function CanvasGradient_(aType) {
+ this.type_ = aType;
+ this.x0_ = 0;
+ this.y0_ = 0;
+ this.r0_ = 0;
+ this.x1_ = 0;
+ this.y1_ = 0;
+ this.r1_ = 0;
+ this.colors_ = [];
+ }
+
+ CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {
+ aColor = processStyle(aColor);
+ this.colors_.push({offset: aOffset,
+ color: aColor.color,
+ alpha: aColor.alpha});
+ };
+
+ function CanvasPattern_() {}
+
+ // set up externs
+ G_vmlCanvasManager = G_vmlCanvasManager_;
+ CanvasRenderingContext2D = CanvasRenderingContext2D_;
+ CanvasGradient = CanvasGradient_;
+ CanvasPattern = CanvasPattern_;
+
+})();
+
+} // if
167 static/jquery.flot.crosshair.js
@@ -0,0 +1,167 @@
+/*
+Flot plugin for showing crosshairs, thin lines, when the mouse hovers
+over the plot.
+
+ crosshair: {
+ mode: null or "x" or "y" or "xy"
+ color: color
+ lineWidth: number
+ }
+
+Set the mode to one of "x", "y" or "xy". The "x" mode enables a
+vertical crosshair that lets you trace the values on the x axis, "y"
+enables a horizontal crosshair and "xy" enables them both. "color" is
+the color of the crosshair (default is "rgba(170, 0, 0, 0.80)"),
+"lineWidth" is the width of the drawn lines (default is 1).
+
+The plugin also adds four public methods:
+
+ - setCrosshair(pos)
+
+ Set the position of the crosshair. Note that this is cleared if
+ the user moves the mouse. "pos" is in coordinates of the plot and
+ should be on the form { x: xpos, y: ypos } (you can use x2/x3/...
+ if you're using multiple axes), which is coincidentally the same
+ format as what you get from a "plothover" event. If "pos" is null,
+ the crosshair is cleared.
+
+ - clearCrosshair()
+
+ Clear the crosshair.
+
+ - lockCrosshair(pos)
+
+ Cause the crosshair to lock to the current location, no longer
+ updating if the user moves the mouse. Optionally supply a position
+ (passed on to setCrosshair()) to move it to.
+
+ Example usage:
+ var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
+ $("#graph").bind("plothover", function (evt, position, item) {
+ if (item) {
+ // Lock the crosshair to the data point being hovered
+ myFlot.lockCrosshair({ x: item.datapoint[0], y: item.datapoint[1] });
+ }
+ else {
+ // Return normal crosshair operation
+ myFlot.unlockCrosshair();
+ }
+ });
+
+ - unlockCrosshair()
+
+ Free the crosshair to move again after locking it.
+*/
+
+(function ($) {
+ var options = {
+ crosshair: {
+ mode: null, // one of null, "x", "y" or "xy",
+ color: "rgba(170, 0, 0, 0.80)",
+ lineWidth: 1
+ }
+ };
+
+ function init(plot) {
+ // position of crosshair in pixels
+ var crosshair = { x: -1, y: -1, locked: false };
+
+ plot.setCrosshair = function setCrosshair(pos) {
+ if (!pos)
+ crosshair.x = -1;
+ else {
+ var o = plot.p2c(pos);
+ crosshair.x = Math.max(0, Math.min(o.left, plot.width()));
+ crosshair.y = Math.max(0, Math.min(o.top, plot.height()));
+ }
+
+ plot.triggerRedrawOverlay();
+ };
+
+ plot.clearCrosshair = plot.setCrosshair; // passes null for pos
+
+ plot.lockCrosshair = function lockCrosshair(pos) {
+ if (pos)
+ plot.setCrosshair(pos);
+ crosshair.locked = true;
+ }
+
+ plot.unlockCrosshair = function unlockCrosshair() {
+ crosshair.locked = false;
+ }
+
+ function onMouseOut(e) {
+ if (crosshair.locked)
+ return;
+
+ if (crosshair.x != -1) {
+ crosshair.x = -1;
+ plot.triggerRedrawOverlay();
+ }
+ }
+
+ function onMouseMove(e) {
+ if (crosshair.locked)
+ return;
+
+ if (plot.getSelection && plot.getSelection()) {
+ crosshair.x = -1; // hide the crosshair while selecting
+ return;
+ }
+
+ var offset = plot.offset();
+ crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
+ crosshair.y = Math.max(0, Math.